摘要:不同的是函數體并不會再被提升至函數作用域頭部,而僅會被提升到塊級作用域頭部避免全局變量在計算機編程中,全局變量指的是在所有作用域中都能訪問的變量。
變量作用域與提升ES6 變量作用域與提升:變量的生命周期詳解從屬于筆者的現代 JavaScript 開發:語法基礎與實踐技巧系列文章。本文詳細討論了 JavaScript 中作用域、執行上下文、不同作用域下變量提升與函數提升的表現、頂層對象以及如何避免創建全局對象等內容;建議閱讀前文 ES6 變量聲明與賦值。
在 ES6 之前,JavaScript 中只存在著函數作用域;而在 ES6 中,JavaScript 引入了 let、const 等變量聲明關鍵字與塊級作用域,在不同作用域下變量與函數的提升表現也是不一致的。在 JavaScript 中,所有綁定的聲明會在控制流到達它們出現的作用域時被初始化;這里的作用域其實就是所謂的執行上下文(Execution Context),每個執行上下文分為內存分配(Memory Creation Phase)與執行(Execution)這兩個階段。在執行上下文的內存分配階段會進行變量創建,即開始進入了變量的生命周期;變量的生命周期包含了聲明(Declaration phase)、初始化(Initialization phase)與賦值(Assignment phase)過程這三個過程。
傳統的 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";
定義在全局作用域中的變量能夠被任意的其他作用域中訪問:
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
函數作用域的缺陷在于粒度過大,在使用閉包或者其他特性時導致異常的變量傳遞:
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(); // 使用模塊 });
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87275.html
摘要:所以上面那段代碼鏈中最初應該是之后之后所以最后的輸出結果是作用域鏈概念看了前面一個完整的函數執行過程,讓我們來說下作用域鏈的概念吧。而這一條形成的鏈就是中的作用域鏈。 showImg(https://segmentfault.com/img/bVbvayE?w=1280&h=545); 1. 什么是作用域 作用域是你的代碼在運行時,某些特定部分中的變量,函數和對象的可訪問性。換句話說,...
摘要:作用域分類作用域共有兩種主要的工作模型。換句話說,作用域鏈是基于調用棧的,而不是代碼中的作用域嵌套。詞法作用域詞法作用域中,又可分為全局作用域,函數作用域和塊級作用域。 一篇鞏固基礎的文章,也可能是一系列的文章,梳理知識的遺漏點,同時也探究很多理所當然的事情背后的原理。 為什么探究基礎?因為你不去面試你就不知道基礎有多重要,或者是說當你的工作經歷沒有亮點的時候,基礎就是檢驗你好壞的一項...
摘要:前端日報精選新特性一覽動畫的種創建方式,每一種都不簡單精讀,和它們在之中的優先級變量作用域與提升變量的生命周期詳解讓完成背景圖加載完畢后顯示之解析的原理中文深入理解筆記改進數組的功能百度外賣前端周刊第期知乎專欄基礎繼承基礎作用域和 2017-08-14 前端日報 精選 ES8 新特性一覽React Web 動畫的 5 種創建方式,每一種都不簡單精讀 React functional s...
摘要:外層作用域不報錯正常輸出塊級作用域與函數聲明規定,函數只能在頂層作用域和函數作用域之中聲明,不能在塊級作用域聲明。規定,塊級作用域之中,函數聲明語句的行為類似于,在塊級作用域之外不可引用。同時,函數聲明還會提升到所在的塊級作用域的頭部。 前言:最近開始看阮一峰老師的《ECMAScript 6 入門》(以下簡稱原...
閱讀 1554·2021-11-19 09:55
閱讀 2778·2021-09-06 15:02
閱讀 3534·2019-08-30 15:53
閱讀 1071·2019-08-29 16:36
閱讀 1230·2019-08-29 16:29
閱讀 2286·2019-08-29 15:21
閱讀 621·2019-08-29 13:45
閱讀 2679·2019-08-26 17:15