JavaScript:闭包
本文和大家聊聊闭包,闭包与变量对象和作用域链有着比较多的联系,在阅读本文前,大家需要理解执行上下文、变量对象以及作用域链等内容,这些内容对理解闭包的本质有很大的帮助,前面的两篇文章已经梳理过了,不清楚的同学可以先阅读之前的文章。
自由变量
上篇文章没有提到自由变量这个概念,现在需要理解这个概念。
在一个作用域中使用了一个变量,但是这个变量没有在这个作用域中声明(在其他作用域中声明),对于该作用域而言,这个变量就是一个自由变量。
let a = 10;
function foo() {
let b = 20;
console.log(a + b); // 10 在foo函数作用域中,a就是一个自由变量
}
foo();
从上面的实例来看,调用foo
函数时,a
的取值是来自全局作用域,所以变量a
相对foo函数作用域而言变量a
是一个自由变量,而b的取值是来自foo
作用域,所以变量b
对于foo
作用域变量b
不是自由变量。
定义
闭包是函数和声明该函数的词法环境的组合。
其实闭包的概念不好解释,似乎解释不清楚,目前业界对闭包的概念解释有两种,但是不管是哪种解释,思想是一致的,只是包含的范围不同而已,我们看下面的实例,再来说说闭包这个东西。
function foo() {
let a = 10;
function bar() {
console.log(a); // 10
}
return bar;
}
let baz = foo();
baz();
上面是一个很简单的实例,这就产生了闭包,为啥产生了闭包???
函数foo
中创建了函数bar
,并返回了函数bar
,并在函数foo
作用域外执行了函数bar
,当函数bar
执行时,访问了foo
作用域中的变量a
,这就产生了闭包。
也就是说当一个函数有权访问另一个函数作用域中的变量,并且该函数在另一个函数的词法作用域外执行就会产生闭包。
从上面的实例来看,也就有人会理解函数foo
是闭包,也有人理解函数bar
是闭包,Chrome开发者工具中会以函数foo
代指闭包。其实不用管闭包是指哪个,我们需要理解什么情况下会产生闭包,闭包产生是在一个什么样的场景。下面从底层原理上分析闭包产生的原因。
原理
我们先看一个实例:
function foo() {
let a = 10;
function bar() {
console.log(a); // 10
}
return bar;
}
let baz = foo();
baz();
这个实例和上面的举例是同一个,产生了闭包,我们分析下这个实例在代码执行过程中,执行上下文栈的情况:
// 创建执行上下文栈
ECStack = [];
// 最先进入全局环境,全局执行上下文被创建被压入栈
ECStack.push(globalContext);
// foo() 创建该函数执行上下文并压入栈中
ECStack.push(<foo> functionContext);
// foo()执行完毕弹出
ECStack.pop();
// baz被调用,创建baz执行上下文并压入栈中
ECStack.push(<baz> functionContext);
// baz执行完毕弹出
ECStack.pop();
// 代码全局执行完毕,全局执行上下文弹出
ECStack.pop();
在来看看bar
函数执行上下文的内容:
bar.[[scope]] = [fooContext.VO, globalContext.VO];
barContext = {
VO: {xxx}, // 变量对象
this: xxx,
scopeChain: [barContext.VO].concat(bar.[[scope]]) // [barContext.VO, fooContext.VO, globalContext.VO]
}
从上面的执行上下文栈的执行情况来看,baz
函数执行的时候,foo
函数的执行上下文已经出栈了,按照JavaScript
垃圾回收机制,foo
函数执行上下文的变量对象失去引用后会被垃圾回收机制回收。
但是上面的实例特殊,bar
函数在foo
函数中创建,foo
函数最终是返回了bar
函数,并通过变量baz
,在foo
函数作用域外执行了,以及访问了foo
函数作用域中的a
变量。
函数bar
执行上下文中的作用域链包含了函数foo
执行上下文中的变量对象fooContext.VO
,所以函数foo
执行上下文的变量对象不会被垃圾回收机制回收,函数bar
访问了函数foo
中的变量,阻止了函数foo
执行上下文的变量对象被垃圾回收机制回收,正因此函数bar
在函数foo
的词法作用域外执行,同时也可以访问foo
作用域中的变量a
,这也就是产生闭包的原因。
我们来归纳下闭包本质是什么:
闭包是一个函数,上面的实例来看,不管是foo
函数还是bar
函数,归根结底还是一个函数,但是和普通函数不一样,其拥有特殊能力。
概括的讲,我们可以把闭包看作是一个场景,如果一个函数B
在函数A
中创建,当函数A的执行上下文已经出栈了,但是函数B
在函数A
的词法作用域外执行并仍然能访问函数A
中的变量对象,我们就可以说这产生了闭包。我们可以不用在意函数A
是闭包还是函数B
是闭包,但我们要清楚什么场景下会产生闭包。
归纳下闭包的特点:
- 函数
A
的执行上下文已经出栈 - 函数
B
能访问函数A
执行上下文的变量对象 - 函数
B
在函数A
的词法作用域外执行
最后总结性的说,函数A
调用完成后,函数A
的执行上下文已经出栈,其变量对象会失去引用等待被垃圾回收机制回收,然而闭包,阻止这一过程,因为函数B
的作用域链包含了函数A
的执行上下文的变量对象。
下面我们看一个实例,熟悉下闭包,增强对闭包的理解。
function foo() {
let a = 'Hello world';
function bar() {
a += ' 6';
console.log(a);
}
return bar;
}
let baz = foo();
baz(); // Hello world 6
baz(); // Hello world 6 6
函数foo
调用完成后,此时函数foo
执行上下文的变量对象内容如下:
fooContext.VO = {
bar: <reference to FunctionDeclaration 'bar'>,
a: 'Hello world'
}
当函数foo
调用完成后,其执行上下文出栈后,它的变量对象没有被垃圾回收机制回收,因为baz
函数调用,函数bar
的作用域链保存了函数foo
执行上下文的变量对象,其变量对象一直在内存中,没有被销毁。
在函数baz
第一次调用后,访问了函数foo
作用域中的变量a
,并对变量a
做相关的操作,使得变量a
的值发生了变化,值为Hello world 6
,此时函数foo
执行上下文的变量对象内容如下:
fooContext.VO = {
bar: <reference to FunctionDeclaration 'bar'>,
a: 'Hello world 6'
}
第一次调用baz
后,函数foo
中的变量a
值为Hello world 6
,没有被销毁,所以第二次调用baz
时,函数foo
中的变量a
值为Hello world 6 6
。
也正因为闭包会阻止垃圾回收机制对变量进行回收,变量会永久存在内存中,相当于全局变量一样会占用着内存,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。如上面实例,我们可以将变量设置为null
:
function foo() {
let a = 'Hello world';
function bar() {
a += ' 6';
console.log(a);
}
return bar;
}
let baz = foo();
baz(); // Hello world 6
baz(); // Hello world 6 6
baz = null; //如果baz不再使用,将其指向的对象释放
闭包应用
在JavaScript
中,因为闭包独有的特性,其应用场景很多。
- 用于保存私有属性,将不需要对外暴露的属性、函数保存在闭包函数的父函数里,避免外部操作对值的干扰
- 避免局部属性污染全局变量空间导致的命名空间混乱
- 模块化封装,将对立的功能模块通过闭包进去封装,只暴露较少的
API
供外部应用使用 - 柯里化,在函数式编程中,利用闭包能够实现很多炫酷的功能,柯里化便是其中很重要的一种
关于闭包的应用,在这里先不做展开,因为里面也有很多自己不太清楚的东西,例如函数式编程,目前自己也不太熟悉,里面还涉及很多其他的知识,关于闭包的应用这块内容暂时不做详细的输出,避免不懂装懂,在这里先梳理闭包有哪些应用,后期对柯里化、模块化封装等内容另外做文字输出。
转载自:https://juejin.cn/post/6844903919764652046