也可在我的个人博客查看:个人博客——闭包
闭包
闭包就是能够读取另一个(外部)作用域变量的函数
使用场景
用于实现私有变量,回调函数,还有一些设计模式等等。
引入
在全局下声明一个变量 name 和函数 sayHello,并执行函数,那么实质该作用域就是一个闭包,而特殊的是其外部是全局作用域(唯一且持久)
var name = "dilomen";
function sayHello() {
console.log("Hello " + name);
}
sayHello();
全局可能不是那么直观,那么就来看看局部作用域下,sayHello 方法依旧能访问到 person 内的变量
function person() {
var name = "dilomen";
function inner() {
console.log("Hello " + name);
}
return inner;
}
var sayHello = person();
sayHello();
为什么变量在作用域消失之后还能被访问到呢?
在外部函数中声明内部函数时,在定义了函数的同时,还会创建一个闭包。闭包会包含函数的声明和该作用域下的所有变量,如上述例子中的 inner 和 name,所以当调用内部函数时,即便声明时的作用域已经消失了,但是还是可以通过闭包来获取声明作用域下的所有变量。由于会存储和引用额外的信息到内存,所以会导致一些性能的降低,所以不能过多使用闭包。
闭包会保存声明内部函数(inner)时作用域下的所有变量和函数声明:
构造函数也是同理,声明一个变量,这种方式往往用于私有变量的使用,然后通过声明内部函数,同时产生了闭包,在之后的实例中就可以通过调用内部函数来调取闭包内保存的变量
function Person() {
var name = "dilomen";
this.sayHello = function() {
console.log("Hello " + name);
};
}
var person = new Person();
person.sayHello();
!!!趁热打铁
当我们需要对一个函数内的变量进行每次累加更改时,我们就会使用到闭包,那么根据之前的知识能够解释这个原理吗?
function compute() {
var num = 1;
function add() {
num++;
}
function get() {
console.log(num);
}
return {
add,
get,
};
}
var a = compute();
a.add();
a.add();
a.get(); // 4
var b = compute();
b.get(); // 1
首先闭包是在内部函数声明时产生的,所以当我们调用 compute 方法时,第一次声明了内部函数,也就是产生了一个闭包,而之后的 add 和 get 都是对这个闭包内的变量进行操作。当我们再次调用一个新的 compute 方法时,会产生一个新的闭包,因此变量也是作用域下的初始值。
执行上下文
要深入理解闭包,就必须先了解执行上下文和作用域(词法环境)的概念
执行上下文
- 全局执行上下文:程序开始执行时,就创建了全局执行上下文,有且只有一个
- 函数执行上下文:每次调用函数时就会创建一个函数执行上下文
js 执行机制
由于 JavaScript 是单线程的,所以只能在某一刻执行某个代码,而不能同时执行。
一旦函数发生调用当前的执行上下文就必须停止,并创建新的函数执行上下文,直到函数执行完成,将函数执行上下文销毁,返回到调用的执行上下文。所以被称为调用栈,是一种先进后出的数据结构。
function outer() {
console.log("outer");
inner();
}
function inner() {
console.log("inner");
}
outer();
- 程序开始,就会创建一个全局执行上下文
- 首先全局调用了 outer 函数,所以创建了 outer 函数执行上下文
- 在 outer 里又调用了 inner 函数,所以有创建了 inner 函数上下文
- inner 函数执行完成,回到 outer 函数执行上下文
- outer 函数执行完成,回到全局执行上下文
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaUhAGD8-1589005836060)(/JavaScript/js调用栈.png)]
同样也可以在 chrome 浏览器的调试中观察到
作用域
又称为词法环境,和执行上下文一样,作用域也分全局作用域和局部作用域。
- ES6 之前,在 JS 中是没有块状作用域,不像别的语言{}包裹就是一个块状作用域,但是函数是有作用域的。try-catch 中的 catch 也是具有作用域的
- ES 推出了新的变量声明,let,const 会创建一个块状作用域
执行上下文和作用域的区别
-
执行上下文是指调用函数时触发的,主要是和当前对象的指向相关,所以会存在谁调用,this 就指向谁的问题
-
作用域是函数声明或者变量声明(let,const)时就已经确定了的,主要是与变量的访问相关
箭头函数就是指向声明时的作用域下的,所以不管谁调用都不会改变,说明作用域是声明时就确定的,而不是执行时
function outer() {
console.log("outer");
inner();
}
function inner() {
console.log("inner");
}
outer();
所以以上代码,在函数声明时,outer 和 inner 就已经创建了函数作用域,这样,在每次调用时,都是相互独立的
那么由于函数有作用域,正常情况下,就只能内部获取外部的变量(通过执行上下文生成的作用域链来获取),那么如何才能让外部访问到内部的变量呢?这个时候我们就能通过闭包来实现。
作用域链
作用域链就是指程序在执行是未在当前作用域内找到对应变量,那么它就会去父级作用域上找,直到全局作用域,这种向上的作用域关系就是作用域链。
每一个通过闭包访问变量的函数都具有一个作用域链,作用域包含闭包的全部信息。
!!!趁热打铁
经过了执行上下文和作用域知识的完善,就可以解释以下这道经典闭包问题了
function test() {
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
});
}
}
test(); // 5 5 5 5 5
由于 for 循环没有作用域,所以 setTimeout 中的 i 根据作用域链得到的是 test 作用域下的 i,而这个 i 通过 for 循环已经变成了 5(同步代码在异步代码前)
或者之后调用也是一样,arr 各子项被赋值了,但是函数并未执行,所以此时程序并不关心函数中 i 的值,只有当该函数被调用时,那么程序会去找函数被定义时的作用域链,也就是 test,而此时 i 已经变成 5 了
function test() {
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function() {
console.log(i);
};
}
return arr;
}
var myArr = test();
for (var i = 0; i < 5; i++) {
myArr[i](); // 5 5 5 5 5
}
解决方式:就是添加一层作用域,将 test 的 i 变量保存在闭包中
包裹一层作用域,这样每个 for 循环都会产生一个闭包,这个闭包会记录每一个 i 值,异步代码就可以通过闭包访问到正确的 i 值
为解决上述问题,我们就可以通过闭包将每次的 i 保存下来
function test() {
for (var i = 0; i < 5; i++) {
(function(t) {
setTimeout(() => {
console.log(t);
});
})(i);
}
}
test(); // 0 1 2 3 4
使用块状变量声明 let
function test() {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(t);
});
}
}
test(); // 0 1 2 3 4
补充
为什么闭包会造成内存泄露
- 首先闭包的信息是存储在内存中的。
- js 在函数结束时,会进行垃圾回收机制,但是由于闭包被引用了,那么根据标记清除法,它就不会在函数结束后回收。