理解JavaScript 闭包

本文深入讲解JavaScript中的闭包概念,包括执行环境、词法作用域、作用域链等基础知识,并通过具体实例帮助读者理解闭包的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几个概念

执行环境

JavaScript的解释器每次开始执行一个函数时,都会为那个函数创建一个执行环境。该环境的作用就是定义调用对象(call object)。

词法作用域和作用域链

JavaScript中,变量的作用域(scope)是程序中定义这个变量的区域。全局变量,作用域是全局性的,函数体中的变量,包括参数,是局部变量,作用域是局部性的。

重点:JavaScript使用词法作用域(lexical scoping),也就是说作用域在函数定义的时候已经确定了。

作用域链是为了更好的描述和帮你理解作用域而提供的一种方法。每个执行环境都有一个和它关联在一起的作用域链,它是一个对象列表或对象链。

函数定义和执行的过程

我们先给出一段代码:

function sayHi(name){
     var greeting = "新年快乐";
     alert(name + ", " + greeting);
}
sayHi("Hu");

1) 定义函数sayHi的时候,该函数的作用域是全局的,所以作用域链上只有一个对象window,我们为了便于理解,使用 scopeChain = [window]。

2) 运行sayHi的时候,进入解释器创建的执行环境,此时,执行环境创建一个调用对象(call object),并把这个对象添加到作用域顶端,scopeChain = [sayHi的调用对象, window]

3) 给sayHi调用对象添加一个arguments属性,保存传递的参数,并把内部greeting和参数name添加到调用对象上。

4) alert();之后,函数被GC回收

再来一段代码:

function sayGreeting(greeting){

    function _say(){
         alert("Hu," + greeting);
     }
     
      return _say;
  }
var newYearGreeting = sayGreeting("新年快乐");
var birthdayGreeting = sayGreeting("生日快乐");

newYearGreeting();
birthdayGreeting();

这段代码sayGreeting函数中又定义了一个_say函数,sayGreeting定义和执行的过程是这样的:

1) 定义sayGreeting函数的时候,该函数的作用域已知是全局的,作用域链scopeChain = [window] 

2) 运行sayGreeting的时候,执行环境依然创建了一个调用对象,并添加其到作用域链的顶端:scopeChain = [sayGreeting的调用对象, window]

3) 给sayGreeting调用对象添加一个arguments属性,保存传递的参数,一起添加到调用对象上。

4) 这时定义了_say函数,此时它的作用域设置为sayGreeting的的作用域链。并返回_say函数的引用给newYearGreeting.

5) birthdayGreeting 这一句,又重新执行了1-4的过程,并返回_say函数的引用给birthdayGreeting.

6) 运行newYearGreeting和birthdayGreeting的时候:

    6.1) 执行环境创建了newYearGreeting的调用对象,并添加其到作用域链的顶端,我们已知_say的作用域链为[sayGreeting的调用对象, window],

             现在为:[_say调用对象, sayGreeting的调用对象, window],添加arguments属性和参数到调用对象上。

    6.1) 这时就运行到了alert()这一句,有个greeting的这个变量,我们需要得到它的值(变量名解析),JavaScript开始在作用域链上查找,

             首先查找_say调用对象,发现没有greeting;然后查找sayGreeting的调用对象,greeting="新年快乐",结束查找,alert出 "Hu,新年快乐"。

这里大家应该会发现一个问题,在运行newYearGreeting()的时候,sayGreeting已经运行结束了,按照回收机制,应该已经销毁了整个对象了,但是实际情况是"新年快乐",还存在。

这是为什么呢?因为运行sayGreeting("新年快乐")的时候产生了一个闭包,newYearGreeing和birthdayGreeting都是闭包,你会发现同时它们也是函数对象。

闭包的定义:A "closure" is an expression (typically a function) that can have free variables together with an environment that binds those variables (that "closes" the expression).

但是我觉得这个更好理解:“A closure is nothing more than a function object with a related scope in which the function’s variables are resolved.”

闭包:无非就是一个函数对象+相关的作用域,函数中的变量都是处在这个作用域中的。

从这里可以看出,广义上是个函数它都可以是闭包,只是我们常说的闭包,一般指的是嵌套函数中,把里面的函数返回给外部变量,该函数能捕捉到外函数中的自己需要用到的变量值。

举例:

 function makeAdder(x) {
    function adder(y) {
      return x + y;
    };
   return adder;
 }
  
var add5 = makeAdder(5);
var add10 = makeAdder(10);

print(add5(2));  // 7
print(add10(2)); // 12


如这个例子:add5是一个闭包。makeAdder(5)这句就是产生闭包的过程,adder(y)这个内部函数在此时捕捉了makeAdder函数的参数x,由于返回adder, add5引用了它,所以adder不会被回收,同时被adder捕捉的变量x也不会被回收。所以,print(add5(2))的时候,返回5+2=7,这里的5就是运行了makeAdder(5)时,x存储的值。同理add10(2)就是10+2。

提醒一点:makeAdder(5) 和 makeAdder(10)  是分别创建了各自单独的执行环境,所以add5 和 add10的作用域链也就是各自单独一条作用域链了。

那么我们理解闭包,最关键的点就是:JavaScript使用词法作用域,也就是说函数的作用域在定义函数的时候就已经确定,运行函数的时候只是在作用域链顶端添加了调用对象。

 

闭包使用案例

闭包的使用网上有很多案例,大家可以去搜搜看,我推荐几个个人认为不错的:

https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures     这个通俗易懂。

http://msdn.microsoft.com/en-us/magazine/ff696765.aspx  MSDN上的,案例比较全,讲的比较深一点。

 

仔细理解案例,从中再加深对闭包的理解。

欢迎交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值