《JavaScript权威指南》读书笔记: 闭包

闭包,简单说是指: 函数对象本身和这个函数关联的作用域链的结合

上面的定义可能听不懂,为了详细说明这个概念,需要先解释清楚JavaScript中作用域相关的概念。

变量作用域

变量作用域是一个变量在定义后在源码中可见的范围。

JavaScript中存在两种变量作用域:

全局作用域:全局变量具有全局作用域,在整个JavaScript代码中都可见

本地作用域:

  • 在函数中定义的变量就是本地变量,这些变量具有本地作用域,他们在其被定义的函数体内部完全可见,函数的参数也算作该函数的本地变量。
  • 本地变量可以覆盖同名的全局变量
  • 函数可以嵌套,且每一个函数都具有自己的本地作用域环境。

例子:

/**
 * 全局变量,全局可见
 */
var globalVar = 'global Scope';
var anotherGlobalVar = 'another global var'

function fn(){
     var localVar = 'local Scope';
     var anotherGlobalVar = 'global override';
     console.log( localVar );
     console.log( globalVar );
     console.log( anotherGlobalVar );
}

fn(); // 将输出:   'local Scope'  'globalScope' 'global override'

console.log( localVar ); // 报错, localVar 不存在

函数作用域

JavaScript使用函数作用域规则来决定变量的作用域范围。这意味着,一个在函数中被定义的变量,在这个函数体中,以及该函数体中嵌套定义的所有函数内部都可见。另外和同名本地变量覆盖全局变量同理,嵌套函数中的同名本地变量也会覆盖其上层函数中的本地变量。

例子:

function fn(){
     var localVar = 'fn var';

     function nestedFn(){
          var localVar = 'nested fn var';
          console.log( localVar );          // ==> 'nested fn var'
     }

     nestedFn();
}

fn();

作为对象属性的变量

变量的本质是通过对象来组织到一起的属性集合。

当你定义了一个全局变量,你是在全局对象上定义了一个属性(这个全局对象根据宿主环境不一样而不一样,比如在浏览器里面就是window)

var a = {};
console.log( window.a === a ); // true

对于本地变量,你可以同样的将他们视为某个对象的属性,只是在JavaScript中,你并没有方法获取到这个对象。在ES3中,这个对象被称为“调用对象”(Call Object),在ES5中被称为“声明上下文”(declarative enviroment record),以下面这个函数定义为例:

function fn( parameter ){
     var localVar = 'local var';
}

若我们能找到办法获取到这个“调用对象”的对象引用,即为A,则A将具有如下的属性:

A.parameter
A.localVar

作用域链

结合上面的所有信息,在JS的实际执行中,为了准确的定位到一个标示符具体指向的是哪个变量,我们需要了解作用域链的概念。为什么需要这个听起来很复杂的东西?先看看下面的代码:

var neekey = { name: 'Neekey' };

function run( name ){

     var originName = myName;
     var myName = name;

     function sayMyCurrentName(){
          console.log( myName );
     }

     function changeMyName( name ){
          originName = name;
          neekey.name = name;
     }

     function sayMyOriginName(){
          console.log( originName );
     }

     sayMyCurrentName();     // Neekey
     changeMyName( 'Nick' );
     sayMyOriginName();      // Neekey
}

run( neekey.name );

console.log( neekey.name ); // Nick

这个例子其实不算复杂,但是你在看的时候还是要仔细注意这些个namemyNameoriginName 都实际指向的是哪个变量。

首先作用域链是什么呢?JavaScript中的每一块代码都会与一个作用域链相关联,这个链是一个对象列表,对于每一个标示符,都依次从这个链的对象中查找具有同样标示符的属性。而作用域链就是由全局变量和不定数量的由函数调用产生的调用对象构成的列表。

看下面的例子:

var x = 1;
var y = 2;

// 代码执行到此,为了确定x和y,查询其作用域链: [ window ],从 window.x 和 window.y 中取出了值
console.log( x + y );

function fnA(){
     // 函数被调用,产生了fnA的调用对象(即为a),用于查询变量的作用域链为:[ a, window ]
     var x = 2;
     // a.x 定位到x,a.y不存在,继续从 window.y 中定位到y
     console.log( x + y );
}

fn();

需要注意,在一个函数刚刚被定义时,还没有调用对象存在,只有当这个函数开始被调用执行时,才会有一个调用对象被创建用于储存其中的本地变量。也就是说,相同的一个函数,在每一次调用都会产生一个独立的调用对象。

另外,在函数刚刚被定义时,除了新建了一个函数对象(Function Object)外,还将当前的作用域链和这个函数对象关联在了一起:

var x = 1;
var y = 2;

// 此时的作用域链: [ window ]
// fnA 函数定义后,当前作用域链和fnA 关联了起来,我们可以假设存在类似 fnA.__scope_chain = [ window ] 这样的东西存在
function fnA(){
     // 函数调用开始,新建了一个调用对象a,和之前关联起来的作用域链结合,生成了属于该函数调用过程的作用域链: [ a, window ]
     var x = 2;
     console.log( x + y );
}

fn();

所以当函数嵌套很多的时候,作用域链会很长:

var x = 1;
var y = 2;

// [ window ]
// fnA.__scope_chain = [ window ]
function fnA(){

     // [ a, window ]
     // fB.__scope_chain = [ a, window ]
     function fnB(){

          // [ b, a, window ]
          // fnC.__scope.chain = [ b, a, window ]
          function fnC(){

               … // N次嵌套后

               // [ n … b, a, window ]
               // fnN.__scope.chain = [ n …. b, a, window ]
               function fnN() {

                    // [ n+1, n, … b, a, window ]
               }
          }
     }
}

fn();

词法作用域

词法作用域(lexical scoping)是指,函数在执行时,使用的是它被定义时的作用域,而不是这个函数被调用时的作用域。

有点抽象,我们看下面的例子:

var scope = 'global scope';
function checkScope(){
     var scope = 'local scope';

     return function(){
         console.log( scope );
     }
}

var f = checkScope();
f();      // local scope

上面的例子中,核心是嵌套定义的这个函数:

return function(){
     console.log( scope );
}

虽然实际该函数在执行时fn()时作用域中的scopeglobal scope,但是实际执行使用的是定义函数时的那个 ‘local scope’,集合上面作用域链的知识,所谓的词法作用域规定了:

函数在执行时使用的作用域链是它在定义时绑定的那个作用域链(准确地说是使用当时绑定的那个作用域链加上新建的调用对象组成的新的作用域链),而不是函数调用时所处上下文的作用域链。

回到闭包上来

上面讲了这么多概念后,回到一开始的答案:

函数对象本身和这个函数关联的作用域链的结合。

也即是:fn + fn.__scope_chain (并不存在__scope_chain,只是为了让这个关联更加形象)

其实,由于作用域链本身的特性,以及函数在定义时就能和作用域链关联起来的自然特性,可以简单说:在JavaScript中,每个函数都是闭包。

理解了闭包本身后,你会发现,闭包的强大之处其实在于:JavaScript中的函数,通过作用域链和词法作用域两者的特性,将该函数定义时的所处的作用域中的相关函数进行了捕获和保存,从而可以在完全不同的上下文中进行引用。

Tools & Services I Use

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.