摘要:不同的是函數(shù)體并不會(huì)再被提升至函數(shù)作用域頭部,而僅會(huì)被提升到塊級(jí)作用域頭部避免全局變量在計(jì)算機(jī)編程中,全局變量指的是在所有作用域中都能訪問的變量。
變量作用域與提升ES6 變量作用域與提升:變量的生命周期詳解從屬于筆者的現(xiàn)代 JavaScript 開發(fā):語(yǔ)法基礎(chǔ)與實(shí)踐技巧系列文章。本文詳細(xì)討論了 JavaScript 中作用域、執(zhí)行上下文、不同作用域下變量提升與函數(shù)提升的表現(xiàn)、頂層對(duì)象以及如何避免創(chuàng)建全局對(duì)象等內(nèi)容;建議閱讀前文 ES6 變量聲明與賦值。
在 ES6 之前,JavaScript 中只存在著函數(shù)作用域;而在 ES6 中,JavaScript 引入了 let、const 等變量聲明關(guān)鍵字與塊級(jí)作用域,在不同作用域下變量與函數(shù)的提升表現(xiàn)也是不一致的。在 JavaScript 中,所有綁定的聲明會(huì)在控制流到達(dá)它們出現(xiàn)的作用域時(shí)被初始化;這里的作用域其實(shí)就是所謂的執(zhí)行上下文(Execution Context),每個(gè)執(zhí)行上下文分為內(nèi)存分配(Memory Creation Phase)與執(zhí)行(Execution)這兩個(gè)階段。在執(zhí)行上下文的內(nèi)存分配階段會(huì)進(jìn)行變量創(chuàng)建,即開始進(jìn)入了變量的生命周期;變量的生命周期包含了聲明(Declaration phase)、初始化(Initialization phase)與賦值(Assignment phase)過程這三個(gè)過程。
傳統(tǒng)的 var 關(guān)鍵字聲明的變量允許在聲明之前使用,此時(shí)該變量被賦值為 undefined;而函數(shù)作用域中聲明的函數(shù)同樣可以在聲明前使用,其函數(shù)體也被提升到了頭部。這種特性表現(xiàn)也就是所謂的提升(Hoisting);雖然在 ES6 中以 let 與 const 關(guān)鍵字聲明的變量同樣會(huì)在作用域頭部被初始化,不過這些變量?jī)H允許在實(shí)際聲明之后使用。在作用域頭部與變量實(shí)際聲明處之間的區(qū)域就稱為所謂的暫時(shí)死域(Temporal Dead Zone),TDZ 能夠避免傳統(tǒng)的提升引發(fā)的潛在問題。另一方面,由于 ES6 引入了塊級(jí)作用域,在塊級(jí)作用域中聲明的函數(shù)會(huì)被提升到該作用域頭部,即允許在實(shí)際聲明前使用;而在部分實(shí)現(xiàn)中該函數(shù)同時(shí)被提升到了所處函數(shù)作用域的頭部,不過此時(shí)被賦值為 undefined。
作用域作用域(Scope)即代碼執(zhí)行過程中的變量、函數(shù)或者對(duì)象的可訪問區(qū)域,作用域決定了變量或者其他資源的可見性;計(jì)算機(jī)安全中一條基本原則即是用戶只應(yīng)該訪問他們需要的資源,而作用域就是在編程中遵循該原則來保證代碼的安全性。除此之外,作用域還能夠幫助我們提升代碼性能、追蹤錯(cuò)誤并且修復(fù)它們。JavaScript 中的作用域主要分為全局作用域(Global Scope)與局部作用域(Local Scope)兩大類,在 ES5 中定義在函數(shù)內(nèi)的變量即是屬于某個(gè)局部作用域,而定義在函數(shù)外的變量即是屬于全局作用域。
全局作用域當(dāng)我們?cè)跒g覽器控制臺(tái)或者 Node.js 交互終端中開始編寫 JavaScript 時(shí),即進(jìn)入了所謂的全局作用域:
// 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"函數(shù)作用域
定義在某個(gè)函數(shù)內(nèi)的變量即從屬于當(dāng)前函數(shù)作用域,在每次函數(shù)調(diào)用中都會(huì)創(chuàng)建出新的上下文;換言之,我們可以在不同的函數(shù)中定義同名變量,這些變量會(huì)被綁定到各自的函數(shù)作用域中:
// Global Scope function someFunction() { // Local Scope #1 function someOtherFunction() { // Local Scope #2 } } // Global Scope function anotherFunction() { // Local Scope #3 } // Global Scope
函數(shù)作用域的缺陷在于粒度過大,在使用閉包或者其他特性時(shí)導(dǎo)致異常的變量傳遞:
var callbacks = []; // 這里的 i 被提升到了當(dāng)前函數(shù)作用域頭部 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塊級(jí)作用域
類似于 if、switch 條件選擇或者 for、while 這樣的循環(huán)體即是所謂的塊級(jí)作用域;在 ES5 中,要實(shí)現(xiàn)塊級(jí)作用域,即需要在原來的函數(shù)作用域上包裹一層,即在需要限制變量提升的地方手動(dòng)設(shè)置一個(gè)變量來替代原來的全局變量,譬如:
var callbacks = []; for (var i = 0; i <= 2; i++) { (function (i) { // 這里的 i 僅歸屬于該函數(shù)作用域 callbacks[i] = function () { return i * 2; }; })(i); } callbacks[0]() === 0; callbacks[1]() === 2; callbacks[2]() === 4;
而在 ES6 中,可以直接利用 let 關(guān)鍵字達(dá)成這一點(diǎn):
let callbacks = [] for (let i = 0; i <= 2; i++) { // 這里的 i 屬于當(dāng)前塊作用域 callbacks[i] = function () { return i * 2 } } callbacks[0]() === 0 callbacks[1]() === 2 callbacks[2]() === 4詞法作用域
詞法作用域是 JavaScript 閉包特性的重要保證,筆者在基于 JSX 的動(dòng)態(tài)數(shù)據(jù)綁定一文中也介紹了如何利用詞法作用域的特性來實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)綁定。一般來說,在編程語(yǔ)言里我們常見的變量作用域就是詞法作用域與動(dòng)態(tài)作用域(Dynamic Scope),絕大部分的編程語(yǔ)言都是使用的詞法作用域。詞法作用域注重的是所謂的 Write-Time,即編程時(shí)的上下文,而動(dòng)態(tài)作用域以及常見的 this 的用法,都是 Run-Time,即運(yùn)行時(shí)上下文。詞法作用域關(guān)注的是函數(shù)在何處被定義,而動(dòng)態(tài)作用域關(guān)注的是函數(shù)在何處被調(diào)用。JavaScript 是典型的詞法作用域的語(yǔ)言,即一個(gè)符號(hào)參照到語(yǔ)境中符號(hào)名字出現(xiàn)的地方,局部變量缺省有著詞法作用域。此二者的對(duì)比可以參考如下這個(gè)例子:
function foo() { console.log( a ); // 2 in Lexical Scope ,But 3 in Dynamic Scope } function bar() { var a = 3; foo(); } var a = 2; bar();執(zhí)行上下文與提升
作用域(Scope)與上下文(Context)常常被用來描述相同的概念,不過上下文更多的關(guān)注于代碼中 this 的使用,而作用域則與變量的可見性相關(guān);而 JavaScript 規(guī)范中的執(zhí)行上下文(Execution Context)其實(shí)描述的是變量的作用域。眾所周知,JavaScript 是單線程語(yǔ)言,同時(shí)刻僅有單任務(wù)在執(zhí)行,而其他任務(wù)則會(huì)被壓入執(zhí)行上下文隊(duì)列中(更多知識(shí)可以閱讀 Event Loop 機(jī)制詳解與實(shí)踐應(yīng)用);每次函數(shù)調(diào)用時(shí)都會(huì)創(chuàng)建出新的上下文,并將其添加到執(zhí)行上下文隊(duì)列中。
執(zhí)行上下文每個(gè)執(zhí)行上下文又會(huì)分為內(nèi)存創(chuàng)建(Creation Phase)與代碼執(zhí)行(Code Execution Phase)兩個(gè)步驟,在創(chuàng)建步驟中會(huì)進(jìn)行變量對(duì)象的創(chuàng)建(Variable Object)、作用域鏈的創(chuàng)建以及設(shè)置當(dāng)前上下文中的 this 對(duì)象。所謂的 Variable Object ,又稱為 Activation Object,包含了當(dāng)前執(zhí)行上下文中的所有變量、函數(shù)以及具體分支中的定義。當(dāng)某個(gè)函數(shù)被執(zhí)行時(shí),解釋器會(huì)先掃描所有的函數(shù)參數(shù)、變量以及其他聲明:
"variableObject": { // contains function arguments, inner variable and function declarations }
在 Variable Object 創(chuàng)建之后,解釋器會(huì)繼續(xù)創(chuàng)建作用域鏈(Scope Chain);作用域鏈往往指向其副作用域,往往被用于解析變量。當(dāng)需要解析某個(gè)具體的變量時(shí),JavaScript 解釋器會(huì)在作用域鏈上遞歸查找,直到找到合適的變量或者任何其他需要的資源。作用域鏈可以被認(rèn)為是包含了其自身 Variable Object 引用以及所有的父 Variable Object 引用的對(duì)象:
"scopeChain": { // contains its own variable object and other variable objects of the parent execution contexts }
而執(zhí)行上下文則可以表述為如下抽象對(duì)象:
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)三個(gè)步驟;其中聲明步驟會(huì)在作用域中注冊(cè)變量,初始化步驟負(fù)責(zé)為變量分配內(nèi)存并且創(chuàng)建作用域綁定,此時(shí)變量會(huì)被初始化為 undefined,最后的分配步驟則會(huì)將開發(fā)者指定的值分配給該變量。傳統(tǒng)的使用 var 關(guān)鍵字聲明的變量的生命周期如下:
而 let 關(guān)鍵字聲明的變量生命周期如下:
如上文所說,我們可以在某個(gè)變量或者函數(shù)定義之前訪問這些變量,這即是所謂的變量提升(Hoisting)。傳統(tǒng)的 var 關(guān)鍵字聲明的變量會(huì)被提升到作用域頭部,并被賦值為 undefined:
// var hoisting num; // => undefined var num; num = 10; num; // => 10 // function hoisting getPi; // => function getPi() {...} getPi(); // => 3.14 function getPi() { return 3.14; }
變量提升只對(duì) var 命令聲明的變量有效,如果一個(gè)變量不是用 var 命令聲明的,就不會(huì)發(fā)生變量提升。
console.log(b); b = 1;
上面的語(yǔ)句將會(huì)報(bào)錯(cuò),提示 ReferenceError: b is not defined,即變量 b 未聲明,這是因?yàn)?b 不是用 var 命令聲明的,JavaScript 引擎不會(huì)將其提升,而只是視為對(duì)頂層對(duì)象的 b 屬性的賦值。ES6 引入了塊級(jí)作用域,塊級(jí)作用域中使用 let 聲明的變量同樣會(huì)被提升,只不過不允許在實(shí)際聲明語(yǔ)句前使用:
> 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函數(shù)的生命周期與提升
基礎(chǔ)的函數(shù)提升同樣會(huì)將聲明提升至作用域頭部,不過不同于變量提升,函數(shù)同樣會(huì)將其函數(shù)體定義提升至頭部;譬如:
function b() { a = 10; return; function a() {} }
會(huì)被編譯器修改為如下模式:
function b() { function a() {} a = 10; return; }
在內(nèi)存創(chuàng)建步驟中,JavaScript 解釋器會(huì)通過 function 關(guān)鍵字識(shí)別出函數(shù)聲明并且將其提升至頭部;函數(shù)的生命周期則比較簡(jiǎn)單,聲明、初始化與賦值三個(gè)步驟都被提升到了作用域頭部:
如果我們?cè)谧饔糜蛑兄貜?fù)地聲明同名函數(shù),則會(huì)由后者覆蓋前者:
sayHello(); function sayHello () { function hello () { console.log("Hello!"); } hello(); function hello () { console.log("Hey!"); } } // Hey!
而 JavaScript 中提供了兩種函數(shù)的創(chuàng)建方式,函數(shù)聲明(Function Declaration)與函數(shù)表達(dá)式(Function Expression);函數(shù)聲明即是以 function 關(guān)鍵字開始,跟隨者函數(shù)名與函數(shù)體。而函數(shù)表達(dá)式則是先聲明函數(shù)名,然后賦值匿名函數(shù)給它;典型的函數(shù)表達(dá)式如下所示:
var sayHello = function() { console.log("Hello!"); }; sayHello(); // Hello!
函數(shù)表達(dá)式遵循變量提升的規(guī)則,函數(shù)體并不會(huì)被提升至作用域頭部:
sayHello(); function sayHello () { function hello () { console.log("Hello!"); } hello(); var hello = function () { console.log("Hey!"); } } // Hello!
在 ES5 中,是不允許在塊級(jí)作用域中創(chuàng)建函數(shù)的;而 ES6 中允許在塊級(jí)作用域中創(chuàng)建函數(shù),塊級(jí)作用域中創(chuàng)建的函數(shù)同樣會(huì)被提升至當(dāng)前塊級(jí)作用域頭部與函數(shù)作用域頭部。不同的是函數(shù)體并不會(huì)再被提升至函數(shù)作用域頭部,而僅會(huì)被提升到塊級(jí)作用域頭部:
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!"); } } }());避免全局變量
在計(jì)算機(jī)編程中,全局變量指的是在所有作用域中都能訪問的變量。全局變量是一種不好的實(shí)踐,因?yàn)樗鼤?huì)導(dǎo)致一些問題,比如一個(gè)已經(jīng)存在的方法和全局變量的覆蓋,當(dāng)我們不知道變量在哪里被定義的時(shí)候,代碼就變得很難理解和維護(hù)了。在 ES6 中可以利用 let 關(guān)鍵字來聲明本地變量,好的 JavaScript 代碼就是沒有定義全局變量的。在 JavaScript 中,我們有時(shí)候會(huì)無意間創(chuàng)建出全局變量,即如果我們?cè)谑褂媚硞€(gè)變量之前忘了進(jìn)行聲明操作,那么該變量會(huì)被自動(dòng)認(rèn)為是全局變量,譬如:
function sayHello(){ hello = "Hello World"; return hello; } sayHello(); console.log(hello);
在上述代碼中因?yàn)槲覀冊(cè)谑褂?sayHello 函數(shù)的時(shí)候并沒有聲明 hello 變量,因此其會(huì)創(chuàng)建作為某個(gè)全局變量。如果我們想要避免這種偶然創(chuàng)建全局變量的錯(cuò)誤,可以通過強(qiáng)制使用 strict mode 來禁止創(chuàng)建全局變量。
函數(shù)包裹為了避免全局變量,第一件事情就是要確保所有的代碼都被包在函數(shù)中。最簡(jiǎn)單的辦法就是把所有的代碼都直接放到一個(gè)函數(shù)中去:
(function(win) { "use strict"; // 進(jìn)一步避免創(chuàng)建全局變量 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"); // 你現(xiàn)在可以使用該命名空間了 MyApp.Helpers.Parsing.DateParser = function() { //做一些事情 };模塊化
另一項(xiàng)開發(fā)者用來避免全局變量的技術(shù)就是封裝到模塊 Module 中。一個(gè)模塊就是不需要?jiǎng)?chuàng)建新的全局變量或者命名空間的通用的功能。不要將所有的代碼都放一個(gè)負(fù)責(zé)執(zhí)行任務(wù)或者發(fā)布接口的函數(shù)中。這里以異步模塊定義 Asynchronous Module Definition (AMD) 為例,更詳細(xì)的 JavaScript 模塊化相關(guān)知識(shí)參考 JavaScript 模塊演化簡(jiǎn)史
//定義 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(); // 使用模塊 });
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/87275.html
摘要:所以上面那段代碼鏈中最初應(yīng)該是之后之后所以最后的輸出結(jié)果是作用域鏈概念看了前面一個(gè)完整的函數(shù)執(zhí)行過程,讓我們來說下作用域鏈的概念吧。而這一條形成的鏈就是中的作用域鏈。 showImg(https://segmentfault.com/img/bVbvayE?w=1280&h=545); 1. 什么是作用域 作用域是你的代碼在運(yùn)行時(shí),某些特定部分中的變量,函數(shù)和對(duì)象的可訪問性。換句話說,...
摘要:作用域分類作用域共有兩種主要的工作模型。換句話說,作用域鏈?zhǔn)腔谡{(diào)用棧的,而不是代碼中的作用域嵌套。詞法作用域詞法作用域中,又可分為全局作用域,函數(shù)作用域和塊級(jí)作用域。 一篇鞏固基礎(chǔ)的文章,也可能是一系列的文章,梳理知識(shí)的遺漏點(diǎn),同時(shí)也探究很多理所當(dāng)然的事情背后的原理。 為什么探究基礎(chǔ)?因?yàn)槟悴蝗ッ嬖嚹憔筒恢阑A(chǔ)有多重要,或者是說當(dāng)你的工作經(jīng)歷沒有亮點(diǎn)的時(shí)候,基礎(chǔ)就是檢驗(yàn)?zāi)愫脡牡囊豁?xiàng)...
摘要:前端日?qǐng)?bào)精選新特性一覽動(dòng)畫的種創(chuàng)建方式,每一種都不簡(jiǎn)單精讀,和它們?cè)谥械膬?yōu)先級(jí)變量作用域與提升變量的生命周期詳解讓完成背景圖加載完畢后顯示之解析的原理中文深入理解筆記改進(jìn)數(shù)組的功能百度外賣前端周刊第期知乎專欄基礎(chǔ)繼承基礎(chǔ)作用域和 2017-08-14 前端日?qǐng)?bào) 精選 ES8 新特性一覽React Web 動(dòng)畫的 5 種創(chuàng)建方式,每一種都不簡(jiǎn)單精讀 React functional s...
摘要:外層作用域不報(bào)錯(cuò)正常輸出塊級(jí)作用域與函數(shù)聲明規(guī)定,函數(shù)只能在頂層作用域和函數(shù)作用域之中聲明,不能在塊級(jí)作用域聲明。規(guī)定,塊級(jí)作用域之中,函數(shù)聲明語(yǔ)句的行為類似于,在塊級(jí)作用域之外不可引用。同時(shí),函數(shù)聲明還會(huì)提升到所在的塊級(jí)作用域的頭部。 前言:最近開始看阮一峰老師的《ECMAScript 6 入門》(以下簡(jiǎn)稱原...
閱讀 1609·2021-11-19 09:55
閱讀 2838·2021-09-06 15:02
閱讀 3629·2019-08-30 15:53
閱讀 1234·2019-08-29 16:36
閱讀 1302·2019-08-29 16:29
閱讀 2356·2019-08-29 15:21
閱讀 679·2019-08-29 13:45
閱讀 2735·2019-08-26 17:15