Scope & Hoisting & Closure

Scope

  • Scope 以及 Hoisting、Closure 這三段是搞懂 JavaScript 該如何 coding 的主力內容,建議將 Scope 這段提供的電子書連結看過一遍,我覺得網路上很難找到完整的資訊,還是看書最快

  • 一句話說清楚:Scope 就是一個程式語言的規則,這些規則和作法會決定變數(variables)如何被查找:變數是怎麼宣告的?怎麼被賦值?怎麼被取值?

  • 有兩種主流 Scope:lexical scope(JavaScript 用這款) 和 dynamic scope

  • 一大串一定會忘記的內容:JavaScript 的編譯可粗分成三步驟

    • 一、Tokenzing and Lexing,從一堆字串取出各種基本單元(tokens)

    • 二、Parsing,做出一顆樹,AST(Abstract syntax tree)

    • 三、Code-Generation,把 AST 轉成 executable code/a set of machine instructions

    • Engine、Compiler、Scope

      • 如果 RHS 在 Scopes 中找不到東西,Engine 會丟出 ReferenceError

      • 如果 LHS 在 Scopes 中找不到東西,則 Engine 會收到「因為找不到所以被創建出來的新變數」, 若是在 Strict Mode 下,則和 RHS 的情形一樣,Engine 會丟出 ReferenceError

      • 如果 RHS 有找到東西,但你的操作不合理,Engine 會丟出 TypeError

  • 有兩個東西可以在 runtime 時改變 scope,一、eval(不要用),二、with(已被棄用)

  • ES6-let & const

    • let 會被附接到所在的 { .. } 上,並且不會被 hoisting。(總之使用起來會比較符合其他語言 block scope 的習慣)

    • const,不可改變的變數

  • 以上詳細內容推薦必看(看完還是會忘記):《你所不知道的 JS-範疇與Closures,this與物件原型》 https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20&%20closures/README.md

Hoisting

  • 承 Scope

  • 一句話說清楚:Hoisting 拉升指的是「宣告這個動作被提高到所屬範疇(scope)的頂端」

  • 若有同樣名稱的宣告,函式會比變數優先。但是 variable assignment 又可以將 function declaration OVERRIDE

  • /******************************************/
    // 例子1
    foo();
    
    var foo;
    
    function foo() {
      console.log(1);
    }
    
    foo = function() {
      console.log(2);
    };
    
    // 結果會是 foo(); 被執行並跑出 1
    // 可以把上段 code 理解為以下順序
    
    function foo() {
      console.log(1);
    }
    
    foo(); // 所以這裡就跑出 1
    
    foo = function() {
      console.log(2);
    };
    
    /******************************************/
    // 比較有用的例子2
    
    foo(); // TypeError
    bar(); // ReferenceError
    
    var foo = function bar() {
    };
    
    // foo(); 會 TypeError,foo is not a function
    // bar(); 會 ReferenceError,bar is not defined
    // 可以把上段 code 理解為以下順序
    
    var foo;
    
    foo(); // TypeError
    bar(); // ReferenceError
    
    foo = function() {
      var bar = ...self...
      // ...
    }

Closure

  • 承 Scope

  • 一句話說清楚:到處都是 closures。closure 指的是會記得其 Scope,即使傳送調用到原本的範疇外,仍有存取其變數的能力

  • 直接看錯誤例子最快,而且避開地雷、避開無意義的 bug,正是這份文件的用途

  • 下面這些程式碼來自 MDN 和《你所不知道的 JS-範疇與Closures,this與物件原型》電子書,而 MDN 網站上有其他更進一步的解法程式

  • /******************************************/
    // 經典錯誤例子1
    
    var result = [];
    for (var i = 0; i < 5; i++) {
      result[i] = function() {
        console.log(i);
      };
    }
    
    result[0]; // 5, expected 0
    result[1]; // 5, expected 1
    result[2]; // 5, expected 2
    result[3]; // 5, expected 3
    result[4]; // 5, expected 4
    
    //pseudocode
    //environment: {
    // EnvironmentRecord: {
    // result: [...],
    // i: 5
    // },
    // outer: null,
    
    // 可以把上段 code 修改如下,以符合預期
    
    var result = [];
    for (var i = 0; i < 5; i++) {
      result[i] = (function inner(x) { // additional enclosing context
        return function() {
          console.log(x);
        }
      })(i);
    }
    result[0]; // 0, expected 0
    result[1]; // 1, expected 1
    result[2]; // 2, expected 2
    result[3]; // 3, expected 3
    result[4]; // 4, expected 4
    
    /******************************************/
    // 經典錯誤例子2
    for (var i = 1; i <= 5; i++) {
      setTimeout( function timer() {
        console.log(i);
      }, i * 1000);
    }
    // 結果會是印出五個 6
    // 可以用 IIFE(Immediately Invoked Function Expression) 模式修改如下,以符合預期
    
    // 版本1-1
    for (var i = 1; i <= 5; i++) {
      (function() {
        var j = i;
        setTimeout( function timer() {
          console.log(j);
        }, j * 1000);
      })();
    }
    
    // 版本1-2,看起來更精美
    for (var i = 1; i <= 5; i++) {
      (function(j) {
        setTimeout( function timer() {
          console.log(j);
        }, j * 1000);
      })(i);
    }
    
    // 也可以用 ES6 的 let 修改如下,以符合預期
    for (let i = 1; i <= 5; i++) {
      setTimeout( function timer() {
        console.log(i);
      }, i * 1000);
    }
    
    /******************************************/
    // 其他例子
    function showHelp(help) {
      document.getElementById('help').innerHTML = help;
    }
    
    function setupHelp() {
      var helpText = [
        {'id': 'email', 'help': 'Your e-mail address'},
        {'id': 'name', 'help': 'Your full name'},
        {'id': 'age', 'help': 'Your age (you must be over 16)'}
      ];
    
      for (var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function() {
          showHelp(item.help);
        } // 不符合預期的結果:永遠都綁到 age 欄位!
      }
    }  
    
    setupHelp();
    
    // 可以把上段 code 修改如下,以符合預期
    
    function showHelp(help) {
      document.getElementById('help').innerHTML = help;
    }
    
    // 1.把環境拉出來  
    function makeHelpCallback(help) {  
      return function() {  
        showHelp(help);  
      };  
    }
    
    function setupHelp() {
      var helpText = [
        {'id': 'email', 'help': 'Your e-mail address'},  
        {'id': 'name', 'help': 'Your full name'},  
        {'id': 'age', 'help': 'Your age (you must be over 16)'}  
      ];
    
      for (var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = makeHelpCallback(item.help); //2.已經將各次呼叫的環境分別創立
      }
    }
    
    setupHelp();
  • 補充閱讀資訊

  • 那麼 Closure 可以拿來做什麼?可以做出一種 code pattern:模組(module)

  • /******************************************/
    // 例子1
    var makeCounter = function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }  
    };
    
    var counter1 = makeCounter();
    var counter2 = makeCounter();
    alert(counter1.value()); // Alerts 0
    counter1.increment();
    counter1.increment();
    alert(counter1.value()); // Alerts 2
    counter1.decrement();
    alert(counter1.value()); // Alerts 1
    alert(counter2.value()); // Alerts 0
    • Module

Last updated