ES6 變量做用域與提高:變量的生命週期詳解

ES6 變量做用域與提高:變量的生命週期詳解從屬於筆者的現代 JavaScript 開發:語法基礎與實踐技巧系列文章。本文詳細討論了 JavaScript 中做用域、執行上下文、不一樣做用域下變量提高與函數提高的表現、頂層對象以及如何避免建立全局對象等內容;建議閱讀前文 ES6 變量聲明與賦值javascript

變量做用域與提高

在 ES6 以前,JavaScript 中只存在着函數做用域;而在 ES6 中,JavaScript 引入了 let、const 等變量聲明關鍵字與塊級做用域,在不一樣做用域下變量與函數的提高表現也是不一致的。在 JavaScript 中,全部綁定的聲明會在控制流到達它們出現的做用域時被初始化;這裏的做用域其實就是所謂的執行上下文(Execution Context),每一個執行上下文分爲內存分配(Memory Creation Phase)與執行(Execution)這兩個階段。在執行上下文的內存分配階段會進行變量建立,即開始進入了變量的生命週期;變量的生命週期包含了聲明(Declaration phase)、初始化(Initialization phase)與賦值(Assignment phase)過程這三個過程。java

傳統的 var 關鍵字聲明的變量容許在聲明以前使用,此時該變量被賦值爲 undefined;而函數做用域中聲明的函數一樣能夠在聲明前使用,其函數體也被提高到了頭部。這種特性表現也就是所謂的提高(Hoisting);雖然在 ES6 中以 let 與 const 關鍵字聲明的變量一樣會在做用域頭部被初始化,不過這些變量僅容許在實際聲明以後使用。在做用域頭部與變量實際聲明處之間的區域就稱爲所謂的暫時死域(Temporal Dead Zone),TDZ 可以避免傳統的提高引起的潛在問題。另外一方面,因爲 ES6 引入了塊級做用域,在塊級做用域中聲明的函數會被提高到該做用域頭部,即容許在實際聲明前使用;而在部分實現中該函數同時被提高到了所處函數做用域的頭部,不過此時被賦值爲 undefined。編程

做用域

做用域(Scope)即代碼執行過程當中的變量、函數或者對象的可訪問區域,做用域決定了變量或者其餘資源的可見性;計算機安全中一條基本原則便是用戶只應該訪問他們須要的資源,而做用域就是在編程中遵循該原則來保證代碼的安全性。除此以外,做用域還可以幫助咱們提高代碼性能、追蹤錯誤而且修復它們。JavaScript 中的做用域主要分爲全局做用域(Global Scope)與局部做用域(Local Scope)兩大類,在 ES5 中定義在函數內的變量便是屬於某個局部做用域,而定義在函數外的變量便是屬於全局做用域。瀏覽器

全局做用域

當咱們在瀏覽器控制檯或者 Node.js 交互終端中開始編寫 JavaScript 時,即進入了所謂的全局做用域:安全

// the scope is by default global
var name = 'Hammad';複製代碼

定義在全局做用域中的變量可以被任意的其餘做用域中訪問:markdown

var name = 'Hammad';

console.log(name); // logs 'Hammad'

function logName() {
    console.log(name); // 'name' is accessible here and everywhere else
}

logName(); // logs 'Hammad'複製代碼

函數做用域

定義在某個函數內的變量即從屬於當前函數做用域,在每次函數調用中都會建立出新的上下文;換言之,咱們能夠在不一樣的函數中定義同名變量,這些變量會被綁定到各自的函數做用域中:閉包

// Global Scope
function someFunction() {
    // Local Scope #1
    function someOtherFunction() {
        // Local Scope #2
    }
}

// Global Scope
function anotherFunction() {
    // Local Scope #3
}
// Global Scope複製代碼

函數做用域的缺陷在於粒度過大,在使用閉包或者其餘特性時致使異常的變量傳遞:dom

var callbacks = [];

// 這裏的 i 被提高到了當前函數做用域頭部
for (var i = 0; i <= 2; i++) {
    callbacks[i] = function () {
            return i * 2;
        };
}

console.log(callbacks[0]()); //6
console.log(callbacks[1]()); //6
console.log(callbacks[2]()); //6複製代碼

塊級做用域

相似於 if、switch 條件選擇或者 for、while 這樣的循環體便是所謂的塊級做用域;在 ES5 中,要實現塊級做用域,即須要在原來的函數做用域上包裹一層,即在須要限制變量提高的地方手動設置一個變量來替代原來的全局變量,譬如:異步

var callbacks = [];
for (var i = 0; i <= 2; i++) {
    (function (i) {
        // 這裏的 i 僅歸屬於該函數做用域
        callbacks[i] = function () {
            return i * 2;
        };
    })(i);
}
callbacks[0]() === 0;
callbacks[1]() === 2;
callbacks[2]() === 4;複製代碼

而在 ES6 中,能夠直接利用 let 關鍵字達成這一點:編程語言

let callbacks = []
for (let i = 0; i <= 2; i++) {
    // 這裏的 i 屬於當前塊做用域
    callbacks[i] = function () {
        return i * 2
    }
}
callbacks[0]() === 0
callbacks[1]() === 2
callbacks[2]() === 4複製代碼

詞法做用域

詞法做用域是 JavaScript 閉包特性的重要保證,筆者在基於 JSX 的動態數據綁定一文中也介紹瞭如何利用詞法做用域的特性來實現動態數據綁定。通常來講,在編程語言裏咱們常見的變量做用域就是詞法做用域與動態做用域(Dynamic Scope),絕大部分的編程語言都是使用的詞法做用域。詞法做用域注重的是所謂的 Write-Time,即編程時的上下文,而動態做用域以及常見的 this 的用法,都是 Run-Time,即運行時上下文。詞法做用域關注的是函數在何處被定義,而動態做用域關注的是函數在何處被調用。JavaScript 是典型的詞法做用域的語言,即一個符號參照到語境中符號名字出現的地方,局部變量缺省有着詞法做用域。此兩者的對比能夠參考以下這個例子:

function foo() {
    console.log( a ); // 2 in Lexical Scope ,But 3 in Dynamic Scope
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;

bar();複製代碼

執行上下文與提高

做用域(Scope)與上下文(Context)經常被用來描述相同的概念,不過上下文更多的關注於代碼中 this 的使用,而做用域則與變量的可見性相關;而 JavaScript 規範中的執行上下文(Execution Context)其實描述的是變量的做用域。衆所周知,JavaScript 是單線程語言,同時刻僅有單任務在執行,而其餘任務則會被壓入執行上下文隊列中(更多知識能夠閱讀 Event Loop 機制詳解與實踐應用);每次函數調用時都會建立出新的上下文,並將其添加到執行上下文隊列中。

執行上下文

每一個執行上下文又會分爲內存建立(Creation Phase)與代碼執行(Code Execution Phase)兩個步驟,在建立步驟中會進行變量對象的建立(Variable Object)、做用域鏈的建立以及設置當前上下文中的 this 對象。所謂的 Variable Object ,又稱爲 Activation Object,包含了當前執行上下文中的全部變量、函數以及具體分支中的定義。當某個函數被執行時,解釋器會先掃描全部的函數參數、變量以及其餘聲明:

'variableObject': {
    // contains function arguments, inner variable and function declarations
}複製代碼

在 Variable Object 建立以後,解釋器會繼續建立做用域鏈(Scope Chain);做用域鏈每每指向其反作用域,每每被用於解析變量。當須要解析某個具體的變量時,JavaScript 解釋器會在做用域鏈上遞歸查找,直到找到合適的變量或者任何其餘須要的資源。做用域鏈能夠被認爲是包含了其自身 Variable Object 引用以及全部的父 Variable Object 引用的對象:

'scopeChain': {
    // contains its own variable object and other variable objects of the parent execution contexts
}複製代碼

而執行上下文則能夠表述爲以下抽象對象:

executionContextObject = {
    'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
    'variableObject': {}, // contains function arguments, inner variable and function declarations
    'this': valueOfThis
}複製代碼

變量的生命週期與提高

變量的生命週期包含着變量聲明(Declaration Phase)、變量初始化(Initialization Phase)以及變量賦值(Assignment Phase)三個步驟;其中聲明步驟會在做用域中註冊變量,初始化步驟負責爲變量分配內存而且建立做用域綁定,此時變量會被初始化爲 undefined,最後的分配步驟則會將開發者指定的值分配給該變量。傳統的使用 var 關鍵字聲明的變量的生命週期以下:

而 let 關鍵字聲明的變量生命週期以下:

如上文所說,咱們能夠在某個變量或者函數定義以前訪問這些變量,這便是所謂的變量提高(Hoisting)。傳統的 var 關鍵字聲明的變量會被提高到做用域頭部,並被賦值爲 undefined:

// var hoisting
num;     // => undefined  
var num;  
num = 10;  
num;     // => 10  
// function hoisting
getPi;   // => function getPi() {...}  
getPi(); // => 3.14  
function getPi() {  
  return 3.14;
}複製代碼

變量提高只對 var 命令聲明的變量有效,若是一個變量不是用 var 命令聲明的,就不會發生變量提高。

console.log(b);
b = 1;複製代碼

上面的語句將會報錯,提示 ReferenceError: b is not defined,即變量 b 未聲明,這是由於 b 不是用 var 命令聲明的,JavaScript 引擎不會將其提高,而只是視爲對頂層對象的 b 屬性的賦值。ES6 引入了塊級做用域,塊級做用域中使用 let 聲明的變量一樣會被提高,只不過不容許在實際聲明語句前使用:

> let x = x;
ReferenceError: x is not defined
    at repl:1:9
    at ContextifyScript.Script.runInThisContext (vm.js:44:33)
    at REPLServer.defaultEval (repl.js:239:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:433:10)
    at emitOne (events.js:120:20)
    at REPLServer.emit (events.js:210:7)
    at REPLServer.Interface._onLine (readline.js:278:10)
    at REPLServer.Interface._line (readline.js:625:8)
> let x = 1;
SyntaxError: Identifier 'x' has already been declared複製代碼

函數的生命週期與提高

基礎的函數提高一樣會將聲明提高至做用域頭部,不過不一樣於變量提高,函數一樣會將其函數體定義提高至頭部;譬如:

function b() {  
   a = 10;  
   return;  
   function a() {} 
}複製代碼

會被編譯器修改成以下模式:

function b() {
  function a() {}
  a = 10;
  return;
}複製代碼

在內存建立步驟中,JavaScript 解釋器會經過 function 關鍵字識別出函數聲明而且將其提高至頭部;函數的生命週期則比較簡單,聲明、初始化與賦值三個步驟都被提高到了做用域頭部:

若是咱們在做用域中重複地聲明同名函數,則會由後者覆蓋前者:

sayHello();

function sayHello () {
    function hello () {
        console.log('Hello!');
    }

    hello();

    function hello () {
        console.log('Hey!');
    }
}

// Hey!複製代碼

而 JavaScript 中提供了兩種函數的建立方式,函數聲明(Function Declaration)與函數表達式(Function Expression);函數聲明便是以 function 關鍵字開始,跟隨者函數名與函數體。而函數表達式則是先聲明函數名,而後賦值匿名函數給它;典型的函數表達式以下所示:

var sayHello = function() {
  console.log('Hello!');
};

sayHello();

// Hello!複製代碼

函數表達式遵循變量提高的規則,函數體並不會被提高至做用域頭部:

sayHello();

function sayHello () {
    function hello () {
        console.log('Hello!');
    }

    hello();

    var hello = function () {
        console.log('Hey!');
    }
}

// Hello!複製代碼

在 ES5 中,是不容許在塊級做用域中建立函數的;而 ES6 中容許在塊級做用域中建立函數,塊級做用域中建立的函數一樣會被提高至當前塊級做用域頭部與函數做用域頭部。不一樣的是函數體並不會再被提高至函數做用域頭部,而僅會被提高到塊級做用域頭部:

f; // Uncaught ReferenceError: f is not defined
(function () {
  f; // undefined
  x; // Uncaught ReferenceError: x is not defined
  if (true) {
    f();
    let x;
    function f() { console.log('I am function!'); }
  }

}());複製代碼

避免全局變量

在計算機編程中,全局變量指的是在全部做用域中都能訪問的變量。全局變量是一種很差的實踐,由於它會致使一些問題,好比一個已經存在的方法和全局變量的覆蓋,當咱們不知道變量在哪裏被定義的時候,代碼就變得很難理解和維護了。在 ES6 中能夠利用 let 關鍵字來聲明本地變量,好的 JavaScript 代碼就是沒有定義全局變量的。在 JavaScript 中,咱們有時候會無心間建立出全局變量,即若是咱們在使用某個變量以前忘了進行聲明操做,那麼該變量會被自動認爲是全局變量,譬如:

function sayHello(){
  hello = "Hello World";
  return hello;
}
sayHello();
console.log(hello);複製代碼

在上述代碼中由於咱們在使用 sayHello 函數的時候並無聲明 hello 變量,所以其會建立做爲某個全局變量。若是咱們想要避免這種偶然建立全局變量的錯誤,能夠經過強制使用 strict mode 來禁止建立全局變量。

函數包裹

爲了不全局變量,第一件事情就是要確保全部的代碼都被包在函數中。最簡單的辦法就是把全部的代碼都直接放到一個函數中去:

(function(win) {
    "use strict"; // 進一步避免建立全局變量
    var doc = window.document;
    // 在這裏聲明你的變量
    // 一些其餘的代碼
}(window));複製代碼

聲明命名空間

var MyApp = {
    namespace: function(ns) {
        var parts = ns.split("."),
            object = this, i, len;
        for(i = 0, len = parts.lenght; i < len; i ++) {
            if(!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
    return object;
    }
};

// 定義命名空間
MyApp.namespace("Helpers.Parsing");

// 你如今可使用該命名空間了
MyApp.Helpers.Parsing.DateParser = function() {
    //作一些事情
};複製代碼

模塊化

另外一項開發者用來避免全局變量的技術就是封裝到模塊 Module 中。一個模塊就是不須要建立新的全局變量或者命名空間的通用的功能。不要將全部的代碼都放一個負責執行任務或者發佈接口的函數中。這裏以異步模塊定義 Asynchronous Module Definition (AMD) 爲例,更詳細的 JavaScript 模塊化相關知識參考 JavaScript 模塊演化簡史

//定義
define( "parsing", //模塊名字
        [ "dependency1", "dependency2" ], // 模塊依賴
        function( dependency1, dependency2) { //工廠方法

            // Instead of creating a namespace AMD modules
            // are expected to return their public interface
            var Parsing = {};
            Parsing.DateParser = function() {
              //do something
            };
            return Parsing;
        }
);

// 經過 Require.js 加載模塊
require(["parsing"], function(Parsing) {
    Parsing.DateParser(); // 使用模塊
});複製代碼
相關文章
相關標籤/搜索