闭包
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
关键
- 是一个函数
- 可以引用外部函数的变量
特点
- 闭包可以访问当前函数以外的变量,闭包可以更新外部函数的值
- 即使外部函数已经返回,闭包仍能访问外部函数定义的变量
- 被闭包函数访问的父级及以上的函数的局部变量(如范例中的局部变量 i )会一直存在于内存中,不会被JS的垃圾回收机制回收。
作用
- 隐藏变量,避免全局污染
- 可以读取函数内部的变量
例:
闭包模拟私有方法
var Counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
闭包循环
使用闭包
for (var i = 0; i < 3; i++) {
(function(num) {
setTimeout(function() {
console.log(num);
}, 1000);
})(i);
}
// 0
// 1
// 2
未使用闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
问题
使用闭包时变量不会被垃圾回收机制收销毁,需要手动销毁
JS规定在一个函数作用域内,程序执行完以后变量就会被销毁,这样可节省内存;
使用闭包时,按照作用域链的特点,闭包(函数)外面的变量不会被销毁,因为函数会一直被调用,所以一直存在,如果闭包使用过多会造成内存销毁
例:
function getData() {
var buf = new Array(1000).join('*');
var index = 0;
return function () {
index++;
if (index < buf.length) {
return buf[index - 1];
} else {
buf = null; // 不再使用buf, 手动清除引用。
return '';
}
};
}
var data = getData();
var next = data();
while (next !== '') {
// process data()
next = data();
console.log(next);
}
getData()返回一个函数,就是说返回了一个闭包。
闭包引用了getData()函数中的局部变量buf。var data = getData()执行完后,getData()的局部变量buf不会被释放。这是因为data变量引用了getData()返回的闭包,而该闭包又引用了变量buf,所以javascript引擎不会回收buf内存。
从作用域链理解闭包
var scope = '全局作用域';
function localScope() {
var scope = '本地作用域';
function f() {
return scope;
}
return f;
}
var foo = localScope(); // foo指向函数f
foo(); // 调用函数f()
作用域链是一个数组,结构如下:
fContext = {
Scope: [AO, localScopeContext.AO, globalContext.VO],
}
执行上下文
执行上下文是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。
三个类型
- 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,
this
指向这个全局对象。 - 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
- eval函数执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用。
8步过程
- 进入全局代码,创建全局执行上下文, 全局执行上下文压入执行上下文栈
- 全局执行上下文初始化
- 执行 localScope函数,创建 localScope函数执行上下文,localScope 执行上下文被压入执行上下文栈
- localScope执行上下文初始化,创建变量对象、作用域链、this等
- localScope函数执行完毕,localScope执行上下文从执行上下文栈中弹出
- 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
- f 执行上下文初始化,创建变量对象、作用域链、this等
- f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
Q:函数f 执行的时候,localScope函数上下文已经被销毁了,那函数f是如何获取到scope变量的呢?
A:函数f 执行上下文维护了一个作用域链,会指向指向localScope
作用域,作用域链是一个数组,结构如下
fContext = {
Scope: [AO, localScopeContext.AO, globalContext.VO],
}
函数上下文中,两个对象
- 变量对象(VO):是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
- 活动对象(AO) :当进入到一个执行上下文后,这个变量对象才会被激活,这时候活动对象上的各种属性才能被访问。