函数 | 闭包

闭包

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComparisonFunction(propertyName) {
return function(obj1, obj2) {
let val1 = obj1[propertyName];
let val2 = obj2[propertyName];

if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
};
};

内部的匿名函数引用了外部函数的变量propertyName,在这个内部函数被返回并在其他地方被使用后,它仍然引用着那个变量,这是因为内部函数的作用域链包含createComparisonFunction()函数的作用域

变量对象(variable object):在执行上下文创建阶段,都会有一个变量对象,变量对象中存储着形参,函数声明,变量声明等)

活动对象(activation object):在执行上下文执行阶段,会把活动对象当作变量对象,用arguments对象和其他命名参数来初始化这个对象

回顾一下,在调用一个函数是会发生什么?

  1. 创建一个执行上下文并创建一个作用域链(这个作用域链一直向外串起了所有包含函数的活动对象)
  2. 用arguments和其他命名参数来初始化这个函数的活动对象(外部函数的活动对象是内部函数作用域链上的第二个对象)
1
2
3
4
5
6
7
8
9
function compare(val1, val2) {
if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
}

这里定义的compare()函数是在全局上下文中调用的。第一次调用compare()时,会为它创建一个包含argumentsvalue1value2 的活动对象,这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是compare()作用域链上的第二个对象,其中包含thisresult compare

从上图也可以直到,作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象

闭包底层原理

闭包只是定义嵌套函数时的外在表现,闭包的本质是包含函数的活动对象加长了其作用域链,通俗来说,在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中

看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createComparisonFunction(propertyName) {
return function(obj1, obj2) {
let val1 = obj1[propertyName];
let val2 = obj2[propertyName];

if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
};
};

let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });

createComparisonFunction()返回匿名函数后,它的作用域链被初始化为包含createComparisonFunction()的活动对象和全局变量对象。这样,匿名函数就可以访问到createComparisonFunction()可以访问的所有变量

闭包的副作用

内存泄漏

上述闭包存在副作用,即createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用

createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁

这就是内存泄漏

消除内存泄漏的方法

compareNames设置为等于null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉

作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁

1
2
3
4
5
6
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;

知识补充

作用域 | 执行上下文与作用域