一、先说结论
闭包是什么?闭包有什么作用?闭包的缺点又是什么?
- 闭包是一个函数和函数能访问到的变量(也称词法环境)的总和;也可以将闭包理解为定义在一个函数内部的函数。
- 闭包具有以下三个作用:
- 可以读取外层函数内部的变量;
- 可以让这些变量保存在内存中;
- 可以封装对象的私有属性和私有方法。
- 闭包的缺点:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存损耗很大。因此不能滥用闭包,否则会造成网页的性能问题。另外,网上说闭包会造成内存泄露的问题的真实原因是 JS 引擎的实现有问题,这已经有点以讹传讹了。
二、展开简述
什么是词法环境?
在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
- 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如
this的值)的对象。 - 对 外部词法环境 的引用,与外部代码相关联。
简单来说,假如是一个函数,通过词法环境,就可以访问到函数内部的局部变量和外部函数的变量(包括最顶层的全局变量)。
但需要注意的是,词法环境不是函数的专属,一个简单的语句或者代码块都是有词法环境的。
上图右侧的矩形演示了全局词法环境的变化:
- 一开始,全局词法环境没有外部引用,所以箭头指向了
null。当脚本开始运行,词法环境预先填充了所有声明的变量。这里涉及到一个暂时性死区的概念,后面会提到。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
let声明前,不能引用它。几乎就像变量不存在一样。
- 然后
let phrase定义出现了。它尚未被赋值,因此它的值为undefined。从这一刻起,我们就可以使用变量了。 phrase被赋予了一个值。phrase的值被修改。
当然,如果是一个函数,还分为内部和外部的词法环境。
- 对于
name变量,当say中的alert试图访问name时,会立即在内部词法环境中找到它。 - 当它试图访问
phrase时,然而内部没有phrase,所以它顺着对外部词法环境的引用找到了它。
讲到这里,便引出了一个问题,一个函数,是如何调用词法环境的?
这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。我们可以简单将 [[Environment]] 理解为一个指向词法环境的地址。
理解 V8 引擎的一个特性
通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。
但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。
在下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达。因此,此词法环境仍然有效。
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g.[[Environment]] 存储了对相应 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! 理论上,它应该是可以访问的,但引擎把它优化掉了。
new Function 的特殊性
通常,闭包是指使用一个特殊的属性 [[Environment]] 来记录函数自身的创建时的环境的函数。它具体指向了函数创建时的词法环境。
但是如果我们使用 new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。
因此,此类函数无法访问外部(outer)变量,只能访问全局变量。
function getFunc() {
let value = "test";
let func = new Function('alert(value)');
return func;
}
getFunc()(); // error: value is not defined
使用闭包封装对象的私有属性和私有方法
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。
三、几个例子和关联知识点
if 内的函数 看看下面这个代码。最后一行代码的执行结果是什么?
let phrase = "Hello";
if (true) {
let user = "John";
function sayHi() { alert(`${phrase}, ${user}`);
}
}
sayHi();
答案:error。
函数 sayHi 是在 if 内声明的,所以它只存在于 if 中。外部是没有 sayHi 的。
闭包 sum
编写一个像 sum(a)(b) = a+b 这样工作的 sum 函数。
sum(1)(2) = 3
sum(5)(-1) = 4
为了使第二个括号有效,第一个(括号)必须返回一个函数。
function sum(a) {
return function(b) {
return a + b; // 从外部词法环境获得 "a"
};
}
alert( sum(1)(2) ); // 3 alert( sum(5)(-1) ); // 4
变量可见吗?
下面这段代码的结果会是什么?
let x = 1;
function func() {
console.log(x); // ?
let x = 2;
}
func();
以上代码输出的结果是:ReferenceError: Cannot access 'x' before initialization。
在这个例子中,我们可以观察到“不存在”的变量和“未初始化”的变量之间的特殊差异。从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let 语句。
函数中的变量 x 是存在变量提升的,这个不要理解错,但是使用 let、const 声明和使用 var 声明不一样,前者仅仅只是进行了变量提升,并没有对变量 x 进行变量赋值,而后者在进行变量提升的同时,也会进行变量赋值(undefined)。
立即调用的函数表达式(IIFE)
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
以上两种就是立即调用的函数表达式(IIFE)的语法,但是,为什么这样写?
在定义一个函数之后,如果想立即调用该函数,这时候,一般在函数后面直接加圆括号。但是,如果该函数是语句而不是表达式的形式时,在函数的定义之后加上圆括号就会产生语法错误。
// 语句
function f() {}
// 表达式
var f = function f() {}
JavaScript 规定,如果function关键字出现在行首,一律解释成语句,所以,有了 IIFE 的写法。
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。而以下的写法二优于写法一,因为完全避免了污染全局变量。
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
四、参考资料
- [MDN 闭包](闭包 - JavaScript | MDN (mozilla.org))
- [网道 闭包](函数 - JavaScript 教程 - 网道 (wangdoc.com))
- [现代 JavaScript 教程 闭包](变量作用域,闭包 (javascript.info))
- [方应航 JS 中的闭包是什么?](I miss you!(>﹏<) (fangyinghang.com))