前言
闭包在JavaScript一直是一个比较难以理解的概念,在这篇文章中将通过执行上下文和作用域来理解闭包。
如果对上下文和作用域不太了解,建议先看前两章。
https://blog.youkuaiyun.com/seapackk/article/details/131843987
https://blog.youkuaiyun.com/seapackk/article/details/131847749
什么是闭包?
MDN文档中是这样描述闭包的:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用组合。换而言之,闭包可以让开发者从内部函数访问外部函数的作用域。在JavaScript中,闭包会随着函数的创建而被同时创建。
红宝书中是这样描述闭包的:
闭包指的是那些引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现。
总结一下:闭包本质是父级函数中的变量在子级函数中被引用,导致父级函数生命周期完成后函数内部变量不能被销毁,必须要子级函数生命周期结束销毁后才能被销毁。
闭包,执行上下文与作用域
代码执行过程分析
通过一个实例来进行分析:
function createComparisonFunction(propertyName){
return function(object1,object2){
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
}
//创建比较函数,比较名称-name
let compareName = createComparisonFunction('name');
//调用函数
let result = compareName({name:"Nicholas"},{name:'Matt'});
console.log(result); //1
代码第5、6行位于内部函数(匿名函数)中,其中引用了外部函数的变量propertyName
。在这个内部函数被返回并在其他地方使用后,它仍然引用这那个变量。
现在我们结合执行上下文和作用域来分析。
1,在代码被定义好后作用域就已经确定了。
其对应的作用域链的关系如图。箭头表示包含与被包含的关系,全局作用域包含createComparisonFunction函数作用域,createComparisonFunction函数作用域包含匿名函数作用域。
2,开始解析执行代码,在执行代码前会生成对应的全局执行上下文,并将其放到执行上下文栈中。全局上下文对象的作用范围是全局作用域。
3,代码依次执行,执行到18行,会生成对应的createComparisonFunction函数上下文,并放入上下文栈中。此时createComparisonFunction函数上下文的作用范围是createComparisonFunction函数作用域。
4,第18行代码执行完毕。在正常情况下,createComparisonFunction函数执行完毕之后,执行上下文栈会弹出createComparisonFunction函数上下文并销毁。
但createComparisonFunction函数的返回值是函数,并将这个函数赋值给变量compareName,这个函数里面有对createComparisonFunction函数上下文中变量propertyName的引用,所以createComparisonFunction函数上下文暂时不会出栈被销毁掉。如果它被销毁掉,propertyName的值就找不到了。
5,代码执行到20行,调用compareName函数(匿名函数),会生成对应的compareName函数上下文。compareName函数上下文的作用范围是匿名函数作用域。
6,在执行compareName函数的过程中,变量的值是从上下文栈中处于活跃状态的compareName函数上下文中获取的。
当获取变量propertyName时,没有在compareName函数上下文中找到改变量对应的值。
这时就会顺着作用域链向上,在当前作用域的父级作用域链中寻找。作用域链如下:
当前作用域是匿名函数作用域,匿名函数对应的上下文是compareName函数上下文,compareName函数上下文中没有propertyName变量。
匿名函数作用域的父级作用域是createComparisonFunction函数作用域,会在createComparisonFunction函数作用域对应的createComparisonFunction函数上下文中寻找propertyName变量,这个上下文中有propertyName变量,找到了直接返回。如果找不到会继续向上一级作用域寻找。
7,第20行代码执行完毕后,compareName函数上下文会被弹出上下文栈被销毁掉,之后createComparisonFunction函数上下文也会被弹出栈销毁掉。
8,全部代码执行完毕,全局上下文出栈并销毁。
小结
通过上述的代码执行过程分析,使用闭包过程中代码的执行过程和对应上下文的销毁顺序。由于使用闭包会导致父级函数执行完毕后对应的上下文不能被释放掉,会占用更多的内存,所以使用闭包内存开销会更大。如果是非必要情况还是少用闭包。
闭包中使用this
在闭包中使用this分为以下几种情况:
- 如果内部函数中没有使用箭头定义,则this对象会在运行时绑定到执行函数的上下文。
- 如果在全局函数中调用,this在非严格模式下等于window,严格模式下等于undefined。
- 如果作为某个对象的方法调用,则this等于这个对象。
- 匿名函数在这种情况下不会绑定到某个对象,这就意味着this会指向window,在严格模式下this是undefined。
看一段代码
window.identity = 'The Window';
let object = {
identity:'My Object',
getIdentityFunc(){
return function(){
return this.identity;
}
}
}
console.log(object.getIdentityFunc()()); //The Window
根据代码可以判断其作用域链:匿名函数作用域—》getIdentityFunc函数作用域—》全局作用域
按照正常思路如果在匿名函数的作用域中没有找到变量,那应该到其父级getIdentityFunc函数作用域中寻找,结果应该为 My Object
,而不应该是The Window
。
上述查找思路只适用于普通变量,也就是我们在代码中定义的变量。但有两个特殊的变量this和arguments,这种查找方式是不适用的。
之前执行上下文中讲到过,函数每次被调用就会生成对应的函数上下文,函数上下文是一个对象,里面存储了函数体中定义的一些变量,还有函数中自带的变量this
,arguments
。内部函数永远不可能直接访问外部函数的这两个变量。但是可以将其引用到闭包中访问。
window.identity = 'The Window';
let object = {
identity:'My Object',
getIdentityFunc(){
let that = this;
return function(){
return that.identity;
}
}
}
console.log(object.getIdentityFunc()()); //My Object
经典面试题
第一题:下面代码执行后,控制台输出的结果是多少?
var a = 10
function foo(){
console.log(a)
}
function bar() {
var a = 20
foo()
}
bar()
结果:10
分析:JavaScript使用的是静态作用域,又称词法作用域。即作用域是代码定义时确定的,与在哪里调用无关。
根据定义函数定义位置,可以得到对应的作用域链如下图:
foo函数作用域中找不到变量a的值,就会向上到全局作用域中寻找变量a,最终结果为10。
第二题:下面代码执行后,控制台输出的结果是多少?
var a = 10
function bar() {
var a = 20
function foo(){
console.log(a)
}
foo()
}
bar()
结果:20
分析:和第一题类似,根据函数定义位置确定其作用域链如下图:
foo函数会先到foo函数作用域中寻找,再到bar函数作用域中寻找。
第三题:控制台打印结果
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
f()
return f
}
var x = fn()
x()
x()
console.log(n)
结果:21 22 23 10
分析:同第二题,根据函数定义位置确定作用域链
,根据作用域链查找作用域。最后的console.log(n)
输出在是全局作用域中调用,输出的全局作用域中的n。
第四题:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
结果:3 3 3
分析:var 变量提升
第五题:改善下面代码,使其达到预期效果:0 1 2
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
结果:
方式一:使用立即调用的函数表达式(IIFE),创建一个块作用域,将变量保存起来。
var data = [];
for (var i = 0; i < 3; i++) {
(function(j){
data[j] = function(){
console.log(j)
}
})(i)
}
data[0](); //1
data[1](); //2
data[2](); //3
方式二:使用let声明,使变量的作用范围在for块内。
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();