闭包和作用域

5.1 理解闭包

  • 闭包允许函数访问并操作函数外部的变量。只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。

  • 声明的函数可以在声明之后的任何时间被调用,甚至当该函数声明的作用域消失之后仍然可以调用。

    //一个简单的闭包
    var outerValue = "ninja";
    function outerFunction() {
    	assert(outerValue === "ninja","I can see the ninja.");
    }
    outerFunction();
    
    • 在同一作用域中声明了变量 outerValue 及外部函数 outerFunction,在这个例子中是全局作用域。
    • 外部函数outerFunction和外部变量outerValue都是在全局作用域中声明的。所以执行外部函数outerFunction,该函数可以看见并访问变量outerValue。
    • 该作用域其实就是一个闭包,从未消失(只要应用处于运行状态)。该函数可以访问到外部变量,因为它仍然在作用域内并且是可见的。虽然闭包存在,但是闭包的优势不明显。
    //另一个闭包的例子
    var outerValue = "samurai";
    var later;
    
    function outerFunction() {
    var innerValue = "ninja";
    
    	function innerFunction() {
        assert(outerValue === "samurai", "I can see the samurai.");
        assert(innerValue === "ninja", "I can see the ninja.");
          }
    
        later = innerFunction;
      }
    
    outerFunction();
    later();
    
    • 当在外部函数中声明内部函数时,不仅定义了函数的声明,而且还创建了一个闭包。该闭包不仅包含了函数的声明,还包含了在函数声明时该作用域中的所有变量。当最终执行内部函数时,尽管声明时的作用域已经消失了,但是通过闭包,仍然能够访问到原始作用域。

    • 闭包创建了被定义时的作用域内的变量和函数的安全气泡,因此函数获得了执行时所需的内容。该气泡包含了函数和变量,只要函数存在,它就会存在。

    • 谨记每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息。
    • 虽然闭包非常有用,但是不能过度使用。

    • 使用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些信息不再使用(可以安全的进行垃圾回收)或页面卸载时,才会清理这些信息。

5.2 使用闭包

5.2.1 封装私有变量

  • 许多编程语言使用私有变量,这些私有变量是对外部隐藏的对象属性。当通过其他代码访问这些变量时,不希望对象的实现细节对用户造成过度负荷。原生JavaScript不支持私有变量,通过闭包,可以实现很接近的、可接收的私有变量。

    //使用闭包模拟私有变量
    function Ninja() {
          var feints = 0;
          this.getFeints = function(){
            return feints;
          };
          this.feint = function(){
            feints++;
          };
         }
    
         var ninja1 = new Ninja();
         ninja1.feint();
    
         assert(ninja1.feints === undefined,
                "And the private data is inaccessible to us.");
         assert(ninja1.getFeints() === 1,
               "We're able to access the internal feint count.");
    
    
         var ninja2 = new Ninja();
         assert(ninja2.getFeints() === 0,
                "The second ninja object gets it’s own feints variable.");
    
    
    • 创建了一个Ninja构造器。通过在函数上使用new时,就会创建一个新的对象实例,此时调用构造函数,将新的对象作为它的上下文,所以,函数内的this指向新的实例化对象。
    • 在构造器内部,定义了一个变量feints用于保护状态。由于JavaScript的作用域规则的限制,只能在构造器内部访问该变量。为了让作用域外部的代码能够访问到该变量,定义了访问该变量的方法getFeints。该方法可以读取私有变量,但不能改变私有变量。(只读访问的方法通常称为“getter”)。
    • 接下来创建增量方法feint,用于控制私有变量的值。
    • 在构造器完成了它的使命之后,创建了一个 ninja1 实例,并调用 ninja1 的实例方法feint。
    • 测试显示,可以通过闭包内部方法获取私有变量的值,但是不能直接访问私有变量,直接访问的值显示undefined。有效地阻止了变量的不可控,与真实的面向对象语言中的私有变量一样。
      • 通过变量ninja1、ninja2,对象实例是可见的。
      • 因为 feint 方法在闭包内部,因此可以访问变量 feints
      • 在闭包外部,无法访问变量 feints。
  • 通过使用闭包,可以通过方法对ninja的状态进行维护,而不允许用户直接访问——这是因为闭包内部的变量可以通过闭包内的方法访问,构造器外部的代码则不能访问闭包内部的变量。

5.2.2 回调函数

  • 处理回调函数是另一种常见的使用闭包的场景。回调函数指的是需要在将来不确定的某一时刻异步调用的函数。通常,在这种回调函数中,我们经常需要频繁地访问外部数据。

    //在 interval 的回调函数中使用闭包
    	<div id="box1">First Box</div> 
      <script>
        function animateIt(elementId) {
          var elem = document.getElementById(elementId);
          var tick = 0;
          var timer = setInterval(function(){
            if (tick < 100) {
              elem.style.left = elem.style.top = tick + "px";
              tick++;
            }
            else {
              clearInterval(timer);
              assert(tick === 100,
                     "Tick accessed via a closure.");
              assert(elem,
                     "Element also accessed via a closure.");
              assert(timer,
                     "Timer reference also obtained via a closure." );
            }
          }, 10);
        }
        animateIt("box1");
      </script>
    
    • 使用了一个独立的匿名函数来完成目标元素的动画效果,该匿名函数作为计时器的一个参数传入计时器。通过闭包,该匿名函数通过3个变量控制动画过程:elem, tick, timer。这三个变量用于维持整个动画的过程,且必须可以在全局作用域内访问到。
    • 通过在函数内部定义变量,并基于闭包,使得在计时器的回调函数中可以访问这些变量,每个动画都能获得属于自己的“气泡”中的私有变量。
    • 如果没有闭包,一次性同时做许多事情,例如事件绑定、动画、甚至服务端请求等,都将变得非常困难。
    • 闭包内的函数不仅可以在闭包创建的时刻访问这些变量,而且当闭包内部的函数执行时,还可以更新这些变量的值。闭包不是在创建的那一时刻的状态的快照,而是一个真实的状态封装,只要闭包存在,就可以对变量进行修改。
    • 闭包与作用域是强相关的。

5.3 通过执行上下文来跟踪代码

  • JavaScript代码有两种类型:

    • 全局代码:在所有函数外部定义
    • 函数代码:位于函数内部
  • 两种执行上下文:

    • 全局执行上下文:
      • 只有一个,当JavaScript程序开始执行时就已经创建了全局执行上下文。
    • 函数执行上下文:
      • 在每次调用时,就会创建一个新的函数执行上下文。
  • JavaScript是基于单线程的执行模型:

    • 在某个特定的时刻只能执行特定的代码。一旦发生函数调用,当前的执行上下文必须停止执行,并创建新的函数执行上下文来执行函数。当函数执行完毕后,将函数执行上下文销毁,并重新回到发生调用时的执行上下文。所以需要跟踪执行上下文——正在执行的上下文以及正在等待的上下文。最简单的跟踪方法是使用执行上下文栈(调用栈)。
      • 栈是一种基本的数据结构,只能在栈的顶端对数据项进行插入和读取。
    //创建执行上下文
    function skulk(ninja) {
       report(ninja + " skulking");
    }
    
    function report(message) {
       console.log(message);
    }
    
    skulk("Kuma");
    skulk("Yoshi");		
    
    • 上面代码执行上下文的行为:
      1. 每个JavaScript程序只创建一个全局执行上下文,并从全局执行上下文开始执行(在单页应用中每个页面只有一个全局执行的上下文)。当执行全局代码时,全局执行上下文处于活跃状态。
      2. 首先在全局代码中定义两个函数:skulk 和 report,然后调用skulk(“Kuma”)。在同一个特定时刻只能执行特定代码,所以JavaScript引擎停止执行全局代码,开始执行带有Kuma参数的skulk函数。函数创建新的函数执行上下文,并置入执行上下文栈的顶部。
      3. skulk函数进而调用report函数。又一次因为在同一个特定时刻只能执行特定代码,所以,暂停 skulk函数执行上下文,创建新的Kuma作为参数的report函数的执行上下文,并置入执行上下文栈的顶部。
      4. report函数通过内置函数console.log打印出消息后,report函数执行完毕,代码又回到了skulk函数。report函数执行上下文从执行上下文栈顶部弹出,skulk函数执行上下文重新激活,skulk函数继续执行。
      5. skulk函数执行完毕后,skulk函数执行上下文从执行上下文栈顶部弹出,重新激活全局执行上下文并恢复执行。JavaScript全局代码恢复执行。
  • 执行上下文除了可以跟踪应用程序的执行位置之外,对于标识符也是至关重要,在静态环境中通过执行上下文可以准确定位标识符实际指向的变量。

5.4 使用词法环境跟踪变量的作用域

  • 词法环境是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系。

    var ninja = 'Hattori';
    console.log(ninja);
    
    • 当console.log语句访问ninja变量时,会进行词法环境的查询。
      • 词法环境是JavaScript作用域的内部实现机制,通常称之为作用域。
  • 词法环境与特定的JavaScript代码结构关联,既可以是一个函数、一段代码片段,也可以是try-catch语句。这些代码结构(函数、代码片段、try-catch)可以具有独立的标识符映射表。

5.4.1 代码嵌套

  • 词法环境主要基于代码嵌套,通过代码嵌套可以实现代码结构包含另一代码结构。
  • 在作用域范围内,每次执行代码时,代码结构都获得与之关联的词法环境。例如每次调用skulk函数,都将创建新的函数词法环境。
  • 内部代码结构可以访问外部代码结构中定义的变量。

5.4.2 代码嵌套与词法环境

  • 除了跟踪局部变量、函数声明、函数的参数和词法环境外,还有必要跟踪外部(父级)词法环境。需要访问外部代码结构中的变量,如果当前环境中无法找到唯一标识符,就会对外部环境进行查找。一旦找到匹配的变量,或是全局环境中仍无法查找到对应的标识符而返回错误,就会停止查找。每个执行上下文都有一个与之关联的词法环境,词法环境中包含了在上下文中定义的标识符的映射表。
  • 在特定的执行上下文中,程序不仅直接访问词法环境中定义的局部变量,而且还会访问外部环境中定义的变量。为了实现这一点,需要跟踪这些外部环境。JavaScript实现这一点得益于函数是第一类对象的特性。
  • 无论何时创建函数,都会创建一个与之关联的词法环境,并存储在名为[[Environment]]的内部属性上(无法直接访问或操作)。两个中括号用于标志内部属性。
    • 注意:JavaScript函数可以作为任意对象进行传递,定义函数时的环境与调用函数的环境往往是不同的。
  • 无论何时调用函数都会创建一个新的执行环境,被推入执行上下文栈。此外,还会创建一个与之关联的词法环境。
  • 外部环境与新建的词法环境,JavaScript引擎将调用函数的内置[[Environment]]属性与创建函数时的环境进行关联。

5.5 理解JavaScript的变量类型

  • 在JavaScript中通过3个关键字定义变量:var、let、const
    • 三个关键字有两点不同:可变性和词法环境
    • 注意:var 关键词一开始就是JavaScript的一部分,而let和const是在es6中加进来的。

5.5.1 变量可变性

  • 通过变量的可变性来进行分类,const放在一组,var和let放在一组。
    • 通过const定义的变量都不可变。通过const声明的变量的值只能设置一次。
    • 通过var 或 let 声明的变量的值可以变更任意次数。
const变量
  • 通过const声明的变量与普通变量类似,但在声明时需要写初始值,一旦声明完成之后,其值就无法更改。

    • const常量常用于两种目的:

      • 不需要重新赋值的特殊变量
      • 指定一个固定的值,例如球队人数的最大值,可通过const变量MAX_RONIN_COUNT来表示,而不仅仅通过数字234来表示。这使得代码更加易于理解和维护。虽然在代码里没有直接使用数字234,但是通过语义化的变量名MAX_RONIN_COUNT来表示,MAX_RONIN_COUNT的值只能指定一次。
    • 在其他情况下,由于程序在执行过程中不允许对const变量重新复制,这可以避免的代码发生不必要的变更,同时也为JavaScript引擎性能优化提供便利。

          "use strict"
      
      	  const firstConst = "samurai";
          assert(firstConst === "samurai", "firstConst is a samurai");
      
          try{
            firstConst = "ninja";
            fail("Shouldn't be here");
          } catch(e){
            pass("An exception has occured");
          }
          
          assert(firstConst === "samurai", "firstConst is still a samurai!");
      
          const secondConst = {};
          secondConst.weapon = "wakizashi";
          assert(secondConst.weapon === "wakizashi", "We can add new properties");
      
          const thirdConst = [];
          assert(thirdConst.length === 0, "No items in our array");  
          
          thirdConst.push("Yoshi");
      
          assert(thirdConst.length === 1, "The array has changed");
      
      • 由于firstConst变量是静态变量,不允许重新赋值,因此JavaScript引擎抛出异常。
      • fail和pass两个方法与assert方法类似,fail表示失败,pass表示成功。通常用来验证是否发生异常,如果发生异常,将执行catch中的pass方法。如果没有异常,将执行fail方法,表示发生了不该发生的事。
  • const重要特性:

    • 不能将一个全新的值赋值给const变量,但是,可以修改const变量已有的对象。例如可以给已有对象添加属性。可以增加数组长度。
  • const变量只能在声明时被初始化一次,之后再不允许将全新的值赋值给const变量。但是,可以修改const变量已经存在的值,只是不重写const变量。

5.5.2 定义变量的关键字与词法环境

  • 通过与词法环境的关系来进行分类(按作用域分类),var放在一组,const和let放在一组。
使用关键字var
  • 使用var关键字时,该变量在距离最近的函数内部或是在全局词法环境中定义的。(忽略块级作用域)这是JavaScript由来已久的特性。

    	  var globalNinja = "Yoshi";
    
        function reportActivity(){
          var functionActivity = "jumping";
    
          for(var i = 1; i < 3; i++) {
              var forMessage = globalNinja + " " + functionActivity;
              assert(forMessage === "Yoshi jumping",
                     "Yoshi is jumping within the for block");
              assert(i, "Current loop counter:" + i);
          }
    
          assert(i === 3 && forMessage === "Yoshi jumping",
                "Loop variables accessible outside of the loop");
          }
    
        reportActivity();
        assert(typeof functionActivity === "undefined"
            && typeof i === "undefined" && typeof forMessage === "undefined",
            "We cannot see function variables outside of a function");
    
    • 首先定义全局变量 globalNinja ,接着定义函数 reportActivity,在函数中使用循环并验证变量globalNinja的行为。在循环体内可以正常访问块级作用域中的变量(变量i与forMessage)、函数体内的变量(functionActivity)以及全局变量(globalNinja)。
    • 即使在块级作用域的内定义的变量,在块级作用域外仍然能够被访问。
    • 这源于通过var声明的变量实际上总是在距离最近的函数内或全局词法环境中注册的,不关注块级作用域。
    • 这里有3种词法环境:
      • 变量globalNinja是在全局环境中定义的(距离最近的函数内或全局词法环境)。
      • reportActivity函数创建的函数环境,包含变量 functionActivity、i与forMessage,这3个变量均通过关键字var定义,与他们距离最近的是reportActivity函数。
      • for循环的块级作用域,关键字var定义的变量忽略块级作用域。
使用let与const定义具有块级作用域的变量
  • var是在距离最近的函数或全局词法环境中定义变量,与var不同的是,let和const更加直接。let和const直接在最近的词法环境中定义变量(可以是块级作用域内、循环内、函数内或全局环境内)。可以使用let和const定义块级别、函数级别、全局级别的变量。

    //使用const和let关键字
    "use strict";
        const globalNinja = "Yoshi";
    
        function reportActivity(){
          const functionActivity = "jumping";
    
          for(let i = 1; i < 3; i++) {
              let forMessage = globalNinja + " " + functionActivity;
              assert(forMessage === "Yoshi jumping",
                     "Yoshi is jumping within the for block");
              assert(i, "Current loop counter:" + i);
          }
    
          assert(typeof i === "undefined" && typeof forMessage === "undefined",
                "Loop variables not accessible outside the loop");
          }
    
        reportActivity();
        assert(typeof functionActivity === "undefined"
            && typeof i === "undefined" && typeof forMessage === "undefined",
            "We cannot see function variables outside of a function");
    
    

    此时有3个词法环境:

    • 全局环境(函数和块级作用域之外的全局代码),reportActivity函数环境和for循环体。由于使用了关键字let和const,变量在距离最近的词法环境中定义:变量globalNinja是在全局环境中定义的,变量functionActivity是在函数reportActivity中定义的,变量i和变量forMessage是在for循环的块级作用域中定义的。

5.5.3 在词法环境中注册标识符

  • JavaScript作为一门编程语言,其设计的基本原理是易用性。这也是不需要指定函数返回值类型、函数参数类型、变量类型等的主要原因。

  • JavaScript对于在哪儿定义函数不挑剔,在调用函数之前或者之后声明函数均可。

注册标识符的过程
  • JavaScript代码的执行事实上是分两个阶段进行的。一旦创建了新的词法环境,就会执行第一阶段。
    • 在第一阶段没有执行代码,但是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。
    • 第一阶段完成之后执行第二阶段,具体如何执行取决于变量的类型(let、var、const和函数声明)以及环境类型(全局环境、函数环境或块级作用域)。
  • 具体处理过程如下:
    1. 如果创建一个函数环境,那么创建形参及函数参数的默认值。如果非函数,跳过此步骤。
    2. 如果是创建全局或函数环境,就扫描当前代码进行函数声明(不会扫描其他函数的函数体),但是不会扫描函数表达式或箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若标识符已存在,那么该标识符将被重写。如果是块级作用域,将跳过此步骤。
    3. 扫描当前代码进行变量声明。在函数或全局环境中,找到所有当前函数以及他函数之外通过var声明的变量,并找到所有在其他函数或代码块之外通过let或const定义的变量。在块级环境中,仅查找当前块中通过let或const定义的变量。对于所查找的变量,若该标识符不存在,进行注册并将其初始化为undefined。若该标识符已存在,将保留其值。
在函数声明之前调用函数
  • JavaScript易用性的一个典型特征是函数的声明顺序无关紧要。
//在函数调用之前访问函数
assert(typeof fun === "function",
          "fun is a function even though its definition isn’t reached yet!");

    assert(typeof myFunExp === "undefined",
          "But we cannot access function expressions");

    assert(typeof myLamda === "undefined",
           "Nor lambda functions");

    function fun(){}

    var myFunExpr = function(){};
    var myLambda = (x) => x;

  • 能在函数定义之前访问函数,可以这么做的原因是fun是通过函数声明定义的,表明函数已通过函数声明进行定义,在当前词法环境创建时已在其他代码执行之前注册了函数标识符。所以在执行函数调用之前,fun函数已经存在。

  • JavaScript引擎通过这种方式,允许我们直接使用函数的引用,而不需要强制指定函数的定义顺序。在代码执行之前,函数已经存在了。

  • 需要注意,这种情况仅针对函数声明有效。函数表达式与箭头函数都不在此过程中,而是在程序执行过程中执行定义的。这就是不能访问myFunExpr函数和myLambda函数的原因。

函数重载
  • 第二个难题就是处理重载函数标识符的问题。
//函数重载
assert(typeof fun === "function", "We access the function");

    var fun = 3;

    assert(typeof fun === "number", "Now we access the number");

    function fun(){}

    assert(typeof fun == "number", "Still a number");

  • 声明的变量与函数使用相同的名字fun。两个断言都通过了。第一个断言,标识符fun指向一个函数;在第二个断言中,标识符fun指向一个数字。

  • JavaScript的这种行为是由标识符注册的结果直接导致的。在处理过程的第2步中,通过函数声明进行定义的函数在代码执行之前对函数进行创建,并赋值给对应的标识符;在第3步,处理变量的声明,那些在当前环境中未声明的变量,将将被赋值为undefined。在第2步中——注册函数声明时,由于标识符fun已经存在,并未被赋值为undefined。这就是第一个测试fun是否是函数的断言执行通过的原因。之后,执行赋值语句var fun = 3,将数字3赋值给标识符fun。执行完这个赋值语句后,fun就不再指向函数了,而是指向数字3.

  • 在程序的实际执行过程中,跳过了函数声明部分,所以函数的声明不会影响标识符fun的值。

  • 变量提升(variable hoisting)
    • 变量的声明提升至函数顶部,函数的声明提升至全局代码顶部。
    • 变量和函数的声明并没有发生实际上的移动,只是在代码执行之前,先在词法环境中进行注册。虽然描述为提升了,并且进行了定义,但是我们可以通过词法环境对整个处理过程进行更深入的理解,了解真正的原理。

5.6 研究闭包的工作原理

  • 闭包可以访问创建函数时所在作用域内的全部变量,还可以通过闭包模拟私有变量,通过回调函数使得代码更加优雅。
  • 闭包与作用域密切相关。闭包对JavaScript的作用域规则产生了直接影响。

5.6.1 回顾使用闭包模拟私有变量的代码

  • 通过闭包可以模拟私有变量。

  • 这次关注执行上下文与词法环境。

    function Ninja() {
        var feints = 0;
        this.getFeints = function(){
          return feints;
        };
        this.feint = function(){
          feints++;
        };
      }
      var ninja1 = new Ninja();
      assert(ninja1.feints === undefined,
    "And the private data is inaccessible to us.");
      ninja1.feint();
      assert(ninja1.getFeints() === 1,
    "We're able to access the internal feint count.");
      var ninja2 = new Ninja();
      assert(ninja2.getFeints() === 0,
    "The second ninja object gets it’s own feints variable.");
    
    
    • 分析第一个Ninja对象创建完成之后程序的状态。

      • JavaScript构造函数是通过关键字new调用的函数。因此,每次调用构造函数时,都会创建一个新的词法环境,该词法环境保持构造函数内部的局部变量。
    • 此外,无论何时创建函数,都会保持词法环境的引用(通过内置[[Environment]]属性)。在本例中,Ninja构造函数内部,创建了两个函数:getFeints和feint,他们都有Ninja环境的引用,因为Ninja环境是这两个函数创建时所处的环境。

    • getFeints和feint函数是新创建的ninja1的对象方法(可通过this关键字访问)。因此,可以在Ninja构造函数外部访问getFeints和feint函数,这样实际上就是创建了包含feints变量的闭包。

    • 当再创建一个Ninja的实例,即ninja2对象时,将重复整个过程。

    • 每一个通过Ninja构造函数创建的实例对象均获得了各自的方法(ninja1.getFeints与ninja2.getFeints是不同的),当调用构造函数时,各自的实例方法包含各自的变量。这些“私有变量”只能通过构造函数内定义的对象方法进行访问,不允许直接访问。

    • 在调用ninja2.getFeints方法之前,JavaScript引擎正在执行全局代码。我们的程序处于全局执行上下文状态,是执行栈里的唯一上下文。同时,唯一活跃的词法环境是全局环境,与全局执行上下文关联。

    • 当调用ninja2.getFeints()时,我们调用的是ninja2对象的getFeints方法。由于每次调用函数时均会创建新的执行上下文,因此创建了新的getFeints执行环境并推入执行栈。这样同时引起创建新的词法环境,词法环境通常用于保持跟踪函数中定义的变量。另外,getFeints词法环境包含了getFeints函数被创建时所处的环境,当ninja2对象构建时,Ninja环境是活跃的。

    • 了解试图获取feints变量时是如何工作的。首先,访问活跃的getFeints词法环境。因为在getFeints函数内部未定义任何变量,该词法环境是空的,找不到feints变量。接下来,在当前词法环境的外部环境进行查找——本例中,当创建ninja2对象时,Ninja环境处于活跃状态。Ninja环境中具有feints变量的引用,返回该引用,完成搜索过程。

5.6.2 私有变量的警告

  • JavaScript从未阻止我们将一个对象中创建的属性复制给另一个对象。

    //通过函数访问私有变量,而不是通过对象访问
    function Ninja() {
          var feints = 0;
          this.getFeints = function(){
            return feints;
          };
          this.feint = function(){
            feints++;
          };
        }
        var ninja1 = new Ninja();
        ninja1.feint();
    
        var imposter = {};
        imposter.getFeints = ninja1.getFeints;
    
        assert(imposter.getFeints () === 1,
              "The imposter has access to the feints variable!");
    
    
    • 将ninja1对象的方法getFeints赋值给一个新的对象imposter,然后通过对象imposter的getFeints方法,可以访问ninja1对象的私有变量。
    • 尽管该函数是对象的方法,但是仍可以通过该函数访问私有变量。
    • 在JavaScript中没有真正的私有对象属性,但是可以通过闭包实现一种可以接受的“私有”变量的方案。虽然不是真正的私有变量,但是这是一种隐藏信息的有用方式。

5.6.3 回顾闭包和回调函数的例子

<style>
  	#box1, #box2 {
  		width: 200px;
  		height: 200px;
  		position: relative;
  		margin:5;
  		color: white;
  		font-weight: bolder;
  	}

  	#box1{
  		background-color:blue;
  	}
  	#box2{
  		background-color: red;
      margin-bottom: 100px;
  	}
  </style>

<body>
	<div id="box1">First Box</div>
  <div id="box2">Second Box</div>
  <script>
    function animateIt(elementId) {
      var elem = document.getElementById(elementId);
      var tick = 0;
      var timer = setInterval(function(){
        if (tick < 100) {
          elem.style.left = elem.style.top = tick + "px";
          tick++;
        }
        else {
          clearInterval(timer);
          assert(tick === 100,
                 "Tick accessed via a closure.");
          assert(elem,
                 "Element also accessed via a closure.");
          assert(timer,
                 "Timer reference also obtained via a closure." );
        }
      }, 10);
    }
    animateIt("box1");
    animateIt("box2");
  </script>
</body>

  • 通过创建多个闭包,我们可以同时做许多事。每当定时器执行时,回调函数重新激活创建该回调函数时所处的环境。每个回调函数闭包自动保存各自的变量组。
  • 每次调用animateIt函数时,均会创建新的词法环境,该词法环境保存了动画所需的重要变量(elementId、elem、动画元素、tick、计数次数、timer、动画计数器的ID)。只要至少有一个通过闭包访问这些变量的函数存在,这个环境就会一直保持。
  • 在本例中,浏览器会一直保持setInterval的回调函数,直到调用clearInterval方法。随后,当一个计时器到期,浏览器会调用对应的回调函数,通过回调函数的闭包访问创建闭包时的变量。这样避免了手动匹配回调函数的麻烦,并激活变量,极大地简化代码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值