闭包的定义
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
如何理解闭包
我们可以先来看看代码:
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName]; //注意这两行代码
let value2 = object2[propertyName]; //注意这两行代码
//我们分别称上面两行代码为A和B
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
代码A和B位于内部函数(匿名函数)中,其中引用了外部函数的变量 propertyName。在这个内部函数被返回并在其他地方被使用后,它仍然引用着那个变量。这是因为内部函数的作用域链包含 createComparisonFunction() 函数的作用域。要理解为什么会这样,可以想想第一次调用这个函数时会发生什么。
要想知道第一次调用这个函数时会发生什么,我们就必须理解作用域链创建和使用
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。
在函数执行时,要从作用域链中查找变量,以便读、写值。来看下面的代码:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);
这里定义的 compare() 函数是在全局上下文中调用的。第一次调用 compare() 时,会为它创建一个包含arguments、value1 和 value2 的活动对象,这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是 compare() 作用域链上的第二个对象,其中包含this、result 和 compare。关系如下图所示。
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫全局变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。在定义 compare() 函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的 [[Scope]] 中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的 [[Scope]] 来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。
在这个例子中,这意味着 compare() 函数执行上下文的作用域链中有两个变量对象:局部变量对象和全局变量对象。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。
不过,闭包就不一样了。
回到最开始的代码。
function createComparisonFunction(propertyName) {
return function(object1, object2)
let value1 = object1[propertyName]; //注意这两行代码
let value2 = object2[propertyName]; //注意这两行代码
//我们分别称上面两行代码为A和B
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。因此,在 createComparisonFunction() 函数中,匿名函数的作用域链中实际上包含 createComparisonFunction() 的活动对象。
//创建比较函数
let compareNames = createComparisonFunction('name');
//调用比较函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
下图展示了以上代码执行后的结果。
在 createComparisonFunction() 返回匿名函数后,它的作用域链被初始化为包含 createComparisonFunction() 的活动对象和全局变量对象。
这样,匿名函数就可以访问到 createComparisonFunction() 可以访问的所有变量。另一个有意思的副作用就是,createComparisonFunction() 的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction() 执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。这,就是闭包。
想要对解除对函数的引用,从而让垃圾回收程序可以将内存释放掉也很简单,把 compareNames 设置为 null 就可以了。这样一来,作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁。
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;