闭包
1、 如何产生闭包
- 当一个嵌套的内部函数引用了嵌套的外部函数的变量(函数)时,就产生了闭包
2、闭包时什么
- 使用chorme 调试查看
- 理解一: 闭包时嵌套的内部函数(大部分人)
- 理解二:包含被引用变量的对象(少部分人)
3、产生闭包的条件
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
闭包的作用
1、 使用函数内部的变量在执行完后,仍然存活在内存中(延长了局部变量的生命周期)
2、让函数外部可以操作(读写)到函数内部的(变量/函数)
问题:
1、函数执行完后,函数内部变量是否还存在?
- 一般不存在,存在于闭包中的变量才可能存在(要var f = fun())指向内部函数
2、在函数外部能直接访问到内部的局部变量吗
- 不能,但是可以通过闭包让外部操作它,要return内部函数
闭包的生命周期
产生:在嵌套的内部函数定义执行时就产生了(不是在调用)
死亡:在嵌套的内部函数称为垃圾对象时
闭包
开发者通常应该都知道“闭包”这个通用的编程术语。
闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。在某些编程语言中,这是不可能的,或者应该以特殊的方式编写函数来实现。但是如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 “new Function” 语法 中讲到)。
也就是说:JavaScript 中的函数会自动通过隐藏的
[[Environment]]
属性记住创建它们的位置,所以它们都可以访问外部变量。在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于
[[Environment]]
属性和词法环境原理的技术细节。
闭包示例:
Counter 是独立的吗?
在这儿我们用相同的 makeCounter
函数创建了两个计数器(counters):counter
和 counter2
。
它们是独立的吗?第二个 counter 会显示什么?0,1
或 2,3
还是其他?
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
let counter2 = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter2() ); // ?
alert( counter2() ); // ?
答案是:0,1。
函数
counter
和counter2
是通过makeCounter
的不同调用创建的。因此,它们具有独立的外部词法环境,每一个都有自己的
count
。
if 内的函数
看看下面这个代码。最后一行代码的执行结果是什么?
let phrase = "Hello";
if (true) {
let user = "John";
function sayHi() {
alert(`${phrase}, ${user}`);
}
}
sayHi();
答案:error。
函数
sayHi
是在if
内声明的,所以它只存在于if
中。外部是没有sayHi
的。
变量可见吗?
下面这段代码的结果会是什么?
let x = 1;
function func() {
console.log(x); // ?
let x = 2;
}
func();
答案:error。
你运行一下试试:
let x = 1; function func() { console.log(x); // ReferenceError: Cannot access 'x' before initialization let x = 2; } func();
在这个例子中,我们可以观察到“不存在”的变量和“未初始化”的变量之间的特殊差异。
你可能已经在 变量作用域,闭包 中学过了,从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的
let
语句。换句话说,一个变量从技术的角度来讲是存在的,但是在
let
之前还不能使用。下面的这段代码证实了这一点。
function func() { // 引擎从函数开始就知道局部变量 x, // 但是变量 x 一直处于“未初始化”(无法使用)的状态,直到结束 let(“死区”) // 因此答案是 error console.log(x); // ReferenceError: Cannot access 'x' before initialization let x = 2; }
变量暂时无法使用的区域(从代码块的开始到
let
)有时被称为“死区”。
垃圾收集
通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]]
属性。
在下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达。因此,此词法环境仍然有效。
例如:
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
请注意,如果多次调用 f()
,并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:
function f() {
let value = Math.random();
return function() { alert(value); };
}
// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];
当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。
在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value
)也会被从内存中删除:
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
实际开发中的优化
正如我们所看到的,理论上当函数可达时,它外部的所有变量也都将存在。
但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
打开 Chrome 浏览器的开发者工具,并尝试运行下面的代码。
当代码执行暂停时,在控制台中输入 alert(value)
。
function f() {
let value = Math.random();
function g() {
debugger; // 在 Console 中:输入 alert(value); No such variable!
}
return g;
}
let g = f();
g();
正如你所见的 —— No such variable! 理论上,它应该是可以访问的,但引擎把它优化掉了。
这可能会导致有趣的(如果不是那么耗时的)调试问题。其中之一 —— 我们可以看到的是一个同名的外部变量,而不是预期的变量:
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
debugger; // 在 console 中:输入 alert(value); Surprise!
}
return g;
}
let g = f();
g();
V8 引擎的这个特性你真的应该知道。如果你要使用 Chrome/Edge/Opera 进行代码调试,迟早会遇到这样的问题。
这不是调试器的 bug,而是 V8 的一个特别的特性。也许以后会被修改。你始终可以通过运行本文中的示例来进行检查。
内存溢出
- 一种程序出现的错误
- 当程序运行需要的内存超过剩余的内存,就会抛出内存溢出的错误
内存泄漏
- 占用的内存没有及时释放
- 内存泄漏积累多了就容易导致内存溢出
- 常见的内存泄漏:
- 意外的全局变量
- 没有及时清理的计时器和回调函数
- 闭包