前端基础知识:闭包就是指有权访问另一个函数作用域中的变量的函数
闭包就是指有权访问另一个函数作用域中的变量的函数。
官方解释:闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。(词法作用域)
通俗解释:闭包的关键在于:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象。
当某个函数被掉用的时候,会创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位...直至作为作用域链终点的全局执行环境。
作用域链本质上是一个指向变量对象的指针列表,他只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相同名字的变量,一般来讲,当函数执行完毕,局部活动对象就会被销毁,内存中仅保存全部作用域的活动对象。但是,闭包不同。
创建闭包: 在一个函数内部创建另一个函数
function add() {
let a = 1;
let b = 3;
function closure() {
b++;
return a + b;
}
return closure;
}
// 闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。
生命周期
通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。
当闭包中的函数closure
从add
中返回后,它的作用域链被初始化为包含add
函数的活动对象和全局变量对象。这样closure
就可以访问在add
中定义的所有变量。更重要的是,add
函数在执行完毕后,也不会销毁,因为closure
函数的作用域链仍然在引用这个活动对象。换句话说,当add
返回后,其执行环境的作用域链被销毁,但它的活动对象仍然在内存中,直至closure
被销毁。
function add(x) {
function closure(y) {
return x + y;
}
return closure;
}
let add2 = add(2);
let add5 = add(5);
// add2 和 add5 共享相同的函数定义,但是保存了不同的环境
// 在add2的环境中,x为5。而在add5中,x则为10
console.log(add2(3)); // 5
console.log(add5(10)); // 15
// 释放闭包的引用
add2 = null;
add5 = null;
闭包中的this对象
var name = 'window';
var obj = {
name: 'object',
getName: () => {
return () => {
return this.name;
}
}
}
console.log(obj.getName()()); // window
obj.getName()()是在全局作用域中调用了匿名函数,this指向了window。
函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this就指向哪里。
window才是匿名函数功能执行的环境。
使用注意点
1)由于闭包会让包含函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
使用
- 模仿块级作用域
- 私有变量
- 模块模式
在循环中创建闭包:一个常见错误
function show(i) {
console.log(i);
}
function showCallback(i) {
return () => {
show(i);
};
}
// 测试1【3,3,3】
const testFunc1 = () => {
// var i;
for (var i = 0; i < 3; i++) {
setTimeout(() => show(i), 300);
}
}
// 测试2 【0,1,2】
const testFunc2 = () => {
for (var i = 0; i < 3; i++) {
setTimeout(showCallback(i), 300);
}
}
// 测试3【0,1, 2】 闭包,立即执行函数
// 在闭包函数内部形成了局部作用域,每循环一次,形成一个自己的局部作用域
const testFunc3 = () => {
for (var i = 0; i < 3; i++) {
(() => {
setTimeout(() => show(i), 300);
})(i);
}
}
// 测试4【0,1, 2】let
const testFunc4 = () => {
for (let i = 0; i < 3; i++) {
setTimeout(() => show(i), 300);
}
}
setTimeout()函数回调属于异步任务,会出现在宏任务队列
中,被压到了任务队列的最后,在这段代码应该是for循环这个同步任务
执行完成后才会轮到它
测试1错误原因:赋值给 setTimeout
的是闭包。这些闭包是由他们的函数定义和在 testFunc1
作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量i
。这是因为变量i
使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus
的回调执行时,i
的值被决定。由于循环在事件触发之前早已执行完毕,变量对象i
(被三个闭包所共享)已经指向了i
的最后一个值。
测试2正确原因: 所有的回调不再共享同一个环境, showCallback
函数为每一个回调创建一个新的词法环境。在这些环境中,i
指向数组中对应的下标。
测试4正确原因:JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。