Javascript作用域链和闭包

By
写代码

闭包指有权访问另一个函数作用域中变量的函数,通常是一个函数和以静态方式存储的所有父级作用域的一个集合体。

创建闭包常见方式,就是在一个函数内部创建另一个函数。因为内部函数的作用域链包含了外部函数的作用域,所以可以访问外部函数内定义的变量。注意,外部函数的AO和VO中,不会出现闭包的任何内容。

多个函数可能拥有相同的父级作用域,常见的例子就是拥有多个内部函数(比如return了一个包含多个函数的对象)。此时,一个闭包对变量的修改会体现在另一个闭包对变量的读取上。

先看一个例子:

大部分人对这个问题了如指掌,输出结果也能立即说出:4。

修改方法也简单,用闭包:

因为立即执行函数本身和闭包没有什么关系,可以用命名函数来取代,然后循环展开,所以第一段代码等同于下面这个:

闭包修改之后等同于下面:

下面开始看闭包是什么,又做了什么操作。

闭包的相关概念

想从作用域链看懂闭包,首先要知道几个概念:

1、执行环境(Execution Context):执行环境对应了变量或函数有权访问的数据,决定了它们各自的行为。执行环境可以看作一个对象,其中包括了三个属性:变量对象(variable object),this指针(this value),作用域链(scope chain)。

EC

2、执行环境栈(Execution Context Stack):由于函数的每次调用,都会生成上述具有全新状态的函数执行环境,而在函数内部调用另一个函数,又会触发进入另一个执行环境,控制权会转给新的执行环境。从逻辑上来说,这个情况以栈的形式实现,因此叫做执行环境栈。换句话说,所有程序运行时,都可以用执行环境栈来表示:栈顶是当前活跃的执行环境,栈底是全局执行环境。当栈中某个执行环境运行完毕后,会出栈,直到只剩下全局执行环境。

EC Stack

3、变量对象(Variable Object — VO):每个执行环境都有一个与之关联的变量对象,环境里定义的所有变量函数(及其形参)都保存在这个对象中(函数表达式不包含在变量对象中)。分为全局环境VO和函数环境VO。

4、活动对象(Activation Object):当一个函数被调用,活动对象将被创建,其中包含了变量和函数(及其形参)以及arguments对象。活动对象之后会作为函数执行环境的变量对象来使用,即多了一个arguments(与VO相比)。

5、this对象:this对象是在运行时,基于函数执行环境绑定的,任何对象都可以作为this的值,它是执行环境的一个属性。所以,代码中访问this时,直接从执行环境中获取,而不是通过作用域链查找。

全局函数时,等于window;被作为某个对象的方法调用时,等于那个对象。

函数在被调用时,默认会取得两个特殊变量thisarguments。匿名函数的执行环境具有全局性(未绑定到任何对象),所以this对象通常指向window。例子:

以上代码执行时,内部函数未绑定到任何对象(以object.getName()表达式来调用,相当于在全局环境中执行),所以this指向window,因此永远不可能直接访问外部函数中的name变量。

6、作用域链(Scope Chain):内容是执行环境中的变量对象的有序链表。用途是保证对执行环境有权访问的所有变量和函数的有序访问,即标识符解析。

注意,[[Scope]]属性和作用域链(Scope Chain)是不同的,前者是函数的属性,是静态的,保存了所有上层变量对象的分层链,在函数创建的时候保存在函数中。后者是执行环境的属性,有了函数调用,作用域链(和this)才会确定。

作用域链的前端始终都是当前执行的代码所在环境的变量对象。作用域链下一个变量来自包含(外部)环境,再下一个来自下一个包含环境,直至全局执行环境。而全局执行环境始终都是作用域链里最后一个对象。

标识符解析是沿着作用域链一层一层解析的,从最前端开始,直至全局环境的变量对象,搜到为止,搜不到则说明变量未声明。这个规则和原型链相似,均为向上查找。

scope-chain

上面代码执行时,foo函数给fn赋值为引用innerFoo。当bar函数执行时,实际调用了innerFoo函数。由于innerFoo是foo函数的内部函数,在执行时,其作用域链包含AO-闭包的AO(fooAO)-globalVO(函数调用时,活动对象作为执行环境的变量对象来使用),而变量也会按这个顺序在执行环境中取得。所以搜索c会报未声明的错误,a正常输出2。

scope-chain-example

回到最初的问题,第一个例子arr[0]()为什么输出了4?

首先,arr添加了4个匿名函数作为值。当作为函数调用时,需要输出变量t,匿名函数开始在作用域链中寻找t,首先自身作用域中没有,再往上就是全局作用域,找到了t,此时全局作用域里的t已经通过循环累加到4,所以直接输出4。

scope-chain-example

闭包

闭包指有权访问另一个函数作用域中变量的函数,通常是一个函数和以静态方式存储的所有父级作用域的一个集合体。

创建闭包常见方式,就是在一个函数内部创建另一个函数。因为内部函数的作用域链包含了外部函数的作用域,所以可以访问外部函数内定义的变量。注意,外部函数的AO和VO中,不会出现闭包的任何内容。

多个函数可能拥有相同的父级作用域,常见的例子就是拥有多个内部函数(比如return了一个包含多个函数的对象)。此时,一个闭包对变量的修改会体现在另一个闭包对变量的读取上。

更重要的是,外部函数执行完毕后,其活动对象也不会被销毁(但是其执行环境的作用域链会被销毁),因为内部函数作用域链仍在引用这个活动对象。直到内部函数被销毁后,外部函数的活动对象才会被销毁。

闭包的使用

模仿块级作用域:js没有块级作用域的概念。所有变量,都是在包含函数中创建的。可以使用立即执行函数声明并立即调用一个函数(因为function开始的为函数声明,后面不能跟括号,需要用括号将整个function声明转为函数表达式),来模仿块级作用域。
用于限制向全局作用域中添加过多变量或者函数,避免多人维护时命名冲突。

私有变量:任何在函数中定义的变量,都可以认为是私有变量。包括函数的参数、局部变量和内部定义的函数。可以在对象的构造函数内部定义私有变量,并定义特权方法(接口)以访问这些私有变量。

利用私有变量,隐藏了不应该被直接修改的数据。但是,由于在构造函数中定义的特权方法,所以针对每个实例都会创建同样的新方法,代码复用性较差。

静态私有变量:在私有作用域(立即执行函数)中定义私有变量或者方法。将对象的构造函数(使用函数表达式而不是函数声明)升为全局变量(不使用var),以便在外部访问对象。可以通过prototype在原型上定义共享方法。

私有变量name成为一个静态的、由所有实例共享的属性。每个实例对name的修改,都会影响到其他实例。但是由于使用了原型方法,增进了代码复用。

模块模式:为单例对象(只有一个实例的对象,js里用字面量创建即可)添加私有变量和接口。具体方式为,函数内部定义私有变量和函数,返回一个包含公共接口的对象字面量。在需要对单例进行初始化,同时又需要维护其私有变量时使用。示例:

增强的模块模式:与模块模式类似,不直接返回对象,而是创建特定类型的对象,然后为该对象添加变量或者接口,最后返回。

最后,如果把最开始的例子改一下:

那么输出的答案是{a:1, b:1}吗?

4

Comments: 3

  1. 学习了,却细看不下,最后那里感觉绕

    02月10日

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

:razz: