介紹
JavaScript中有一個被稱為作用域(Scope)的特性。雖然對于許多新手開發者來說,作用域的概念并不是很容易理解,我會盡我所能用最簡單的方式來解釋作用域。理解作用域將使你的代碼脫穎而出,減少錯誤,并幫助您使用它強大的設計模式。
什么是作用域(Scope)?
作用域是在運行時代碼中的某些特定部分中變量,函數和對象的可訪問性。換句話說,作用域決定了代碼區塊中變量和其他資源的可見性。
為什么說作用域是最小訪問原則?
那么,為什么要限制變量的可見性呢,為什么你的變量不是在代碼的任何地方都可用呢?一個優點是作用域為您的代碼提供了一定程度的安全性。計算機安全的一個常見原則是用戶應該一次只能訪問他們需要的東西。
想象一下計算機管理員。由于他們對公司的系統有很多控制權限,因此向他們授予超級管理員權限就好了。他們都可以完全訪問系統,一切工作順利。但突然發生了一些壞事,你的系統感染了惡意病毒。現在你不知道誰犯的錯誤?你意識到應該授予普通用戶權限,并且只在需要時授予超級訪問權限。這將幫助您跟蹤更改,并記錄誰擁有什么帳戶。這被稱為最小訪問原則。看起來很直觀?這個原則也適用于編程語言設計,在大多數編程語言中被稱為作用域,包括我們接下來要研究的 JavaScript 。
當你繼續在你的編程旅程,您將意識到,您的代碼的作用域有助于提高效率,幫助跟蹤錯誤并修復它們。作用域還解決了命名問題,在不同作用域中變量名稱可以相同。記住不要將作用域與上下文混淆。它們的特性不同。
JavaScript中的作用域
在JavaScript中有兩種類型的作用域:
全局作用域
局部作用域(也叫本地作用域)
定義在函數內部的變量具有局部作用域,而定義在函數外部的變量具有全局范圍內。每個函數在被調用時都會創建一個新的作用域。
全局作用域
當您開始在文檔中編寫JavaScript時,您已經在全局作用域中了。全局作用域貫穿整個javascript文檔。如果變量在函數之外定義,則變量處于全局作用域內。
JavaScript代碼:
// 默認全局作用域
varname='Hammad';
在全局作用域內的變量可以在任何其他作用域內訪問和修改。
JavaScript代碼:
varname='Hammad';
console.log(name);// logs 'Hammad'
functionlogName(){
console.log(name);// 'name' 可以在這里和其他任何地方被訪問
}
logName();// logs 'Hammad'
局部作用域
函數內定義的變量在局部(本地)作用域中。而且個函數被調用時都具有不同的作用域。這意味著具有相同名稱的變量可以在不同的函數中使用。這是因為這些變量被綁定到它們各自具有不同作用域的相應函數,并且在其他函數中不可訪問。
JavaScript代碼:
// Global Scope
functionsomeFunction(){
// Local Scope #1
functionsomeOtherFunction(){
// Local Scope #2
}
}
// Global Scope
functionanotherFunction(){
// Local Scope #3
}
// Global Scope
塊語句
塊語句,如if和switch條件語句或for和while循環語句,不像函數,它們不會創建一個新的作用域。在塊語句中定義的變量將保留在它們已經存在的作用域中。
JavaScript代碼:
if(true){
// 'if' 條件語句塊不會創建一個新的作用域
varname='Hammad';// name 依然在全局作用域中
}
console.log(name);// logs 'Hammad'
ECMAScript 6 引入了let和const關鍵字。可以使用這些關鍵字來代替var關鍵字。
JavaScript代碼:
varname='Hammad';
let likes='Coding';
constskills='Javascript and PHP';
與var關鍵字相反,let和const關鍵字支持在局部(本地)作用域的塊語句中聲明。
JavaScript代碼:
if(true){
// 'if' 條件語句塊不會創建一個新的作用域
// name 在全局作用域中,因為通過 'var' 關鍵字定義
varname='Hammad';
// likes 在局部(本地)作用域中,因為通過 'let' 關鍵字定義
let likes='Coding';
// skills 在局部(本地)作用域中,因為通過 'const' 關鍵字定義
constskills='JavaScript and PHP';
}
console.log(name);// logs 'Hammad'
console.log(likes);// Uncaught ReferenceError: likes is not defined
console.log(skills);// Uncaught ReferenceError: skills is not defined
只要您的應用程序生活,全球作用域就會生存。 只要您的函數被調用并執行,局部(本地)作用域就會存在。
上下文
許多開發人員經常混淆 作用域(scope) 和 上下文(context),很多誤解為它們是相同的概念。但事實并非如此。作用域(scope)我們上面已經討論過了,而上下文(context)是用來指定代碼某些特定部分中this的值。作用域(scope) 是指變量的可訪問性,上下文(context)是指this在同一作用域內的值。我們也可以使用函數方法來改變上下文,將在稍后討論。 在全局作用域(scope)中上下文中始終是Window對象。(愚人碼頭注:取決于JavaScript 的宿主換環境,在瀏覽器中在全局作用域(scope)中上下文中始終是Window對象。在Node.js中在全局作用域(scope)中上下文中始終是Global對象)
JavaScript代碼:
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
console.log(this);
functionlogFunction(){
console.log(this);
}
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// 因為 logFunction() 不是一個對象的屬性
logFunction();
如果作用域在對象的方法中,則上下文將是該方法所屬的對象。
ES6代碼:
classUser{
logName(){
console.log(this);
}
}
(newUser).logName();// logs User {}
(new User).logName() 是一種將對象存儲在變量中然后調用logName函數的簡單方法。在這里,您不需要創建一個新的變量。
您會注意到,如果您使用new關鍵字調用函數,則上下文的值會有所不同。然后將上下文設置為被調用函數的實例。考慮上面的示例,通過new關鍵字調用的函數。
JavaScript代碼:
functionlogFunction(){
console.log(this);
}
newlogFunction();// logs logFunction {}
當在嚴格模式(Strict Mode)中調用函數時,上下文將默認為undefined。
執行期上下文(Execution Context)
愚人碼頭注:這部分解釋建議先查看這篇文章,更加通俗易懂,http://www.css88.com/archives/7262
上面我們了解了作用域和上下文,為了消除混亂,特別需要注意的是,執行期上下文中的上下文這個詞語是指作用域而不是上下文。這是一個奇怪的命名約定,但由于JavaScipt規范,我們必須鏈接他們這間的聯系。
JavaScript是一種單線程語言,因此它一次只能執行一個任務。其余的任務在執行期上下文中排隊。正如我剛才所說,當 JavaScript 解釋器開始執行代碼時,上下文(作用域)默認設置為全局。這個全局上下文附加到執行期上下文中,實際上是啟動執行期上下文的第一個上下文。
之后,每個函數調用(啟用)將其上下文附加到執行期上下文中。當另一個函數在該函數或其他地方被調用時,會發生同樣的事情。
每個函數都會創建自己的執行期上下文。
一旦瀏覽器完成了該上下文中的代碼,那么該上下文將從執行期上下文中銷毀,并且執行期上下文中的當前上下文的狀態將被傳送到父級上下文中。 瀏覽器總是執行堆棧頂部的執行期上下文(這實際上是代碼中最深層次的作用域)。
無論有多少個函數上下文,但是全局上下文只有一個。
執行期上下文有創建和代碼執行的兩個階段。
創建階段
第一階段是創建階段,當一個函數被調用但是其代碼還沒有被執行的時。 在創建階段主要做的三件事情是:
創建變量(激活)對象
創建作用域鏈
設置上下文(context)的值( `this` )
變量對象
變量對象,也稱為激活對象,包含在執行期上下文中定義的所有變量,函數和其他聲明。當調用函數時,解析器掃描它所有的資源,包括函數參數,變量和其他聲明。包裝成一個單一的對象,即變量對象。
JavaScript代碼:
'variableObject':{
// 包含函數參數,內部變量和函數聲明
}
作用域鏈
在執行期上下文的創建階段,作用域鏈是在變量對象之后創建的。作用域鏈本身包含變量對象。作用域鏈用于解析變量。當被要求解析變量時,JavaScript 始終從代碼嵌套的最內層開始,如果最內層沒有找到變量,就會跳轉到上一層父作用域中查找,直到找到該變量或其他任何資源為止。作用域鏈可以簡單地定義為包含其自身執行上下文的變量對象的對象,以及其父級對象的所有其他執行期上下文,一個具有很多其他對象的對象。
JavaScript代碼:
'scopeChain':{
// 包含自己的變量對象和父級執行上下文的其他變量對象
}
執行期上下文對象
執行期上下文可以表示為一個抽象對象,如下所示:
JavaScript代碼:
executionContextObject={
'scopeChain':{},// 包含自己的變量對象和父級執行上下文的其他變量對象
'variableObject':{},// 包含函數參數,內部變量和函數聲明
'this':valueOfThis
}
代碼執行階段
在執行期上下文的第二階段,即代碼執行階段,分配其他值并最終執行代碼。
詞法作用域
詞法作用域意味著在一組嵌套的函數中,內部函數可以訪問其父級作用域中的變量和其他資源。這意味著子函數在詞法作用域上綁定到他們父級的執行期上下文。詞法作用域有時也被稱為靜態作用域。
JavaScript代碼:
functiongrandfather(){
varname='Hammad';
// likes 在這里不可以被訪問
functionparent(){
// name 在這里可以被訪問
// likes 在這里不可以被訪問
functionchild(){
// 作用域鏈最深層
// name 在這里也可以被訪問
varlikes='Coding';
}
}
}
你會注意到詞法作用域向內傳遞的,意味著name可以通過它的子級期執行期上下文訪問。但是,但是它不能向其父對象反向傳遞,意味著變量likes不能被其父對象訪問。這也告訴我們,在不同執行上下文中具有相同名稱的變量從執行堆棧的頂部到底部獲得優先級。在最內層函數(執行堆棧的最上層上下文)中,具有類似于另一變量的名稱的變量將具有較高優先級。
閉包(?Closures)
愚人碼頭注:這部分解釋建議先查看這篇文章,更加通俗易懂,http://www.css88.com/archives/7262
閉包的概念與我們在上面講的詞法作用域密切相關。 當內部函數嘗試訪問其外部函數的作用域鏈,即在直接詞法作用域之外的變量時,會創建一個閉包。 閉包包含自己的作用域鏈,父級的作用域鏈和全局作用域。
閉包不僅可以訪問其外部函數中定義的變量,還可以訪問外部函數的參數。
即使函數返回后,閉包也可以訪問其外部函數的變量。這允許返回的函數保持對外部函數所有資源的訪問。
當從函數返回內部函數時,當您嘗試調用外部函數時,不會調用返回的函數。您必須首先將外部函數的調用保存在單獨的變量中,然后將該變量調用為函數。考慮這個例子:
JavaScript代碼:
functiongreet(){
name='Hammad';
returnfunction(){
console.log('Hi '+name);
}
}
greet();// 什么都沒發生,沒有錯誤
// 從 greet() 中返回的函數保存到 greetLetter 變量中
greetLetter=greet();
// 調用? greetLetter 相當于調用從 greet() 函數中返回的函數
greetLetter();// logs 'Hi Hammad'
這里要注意的是,greetLetter函數即使在返回后也可以訪問greet函數的name變量。 有一種方法不需要分配一個變量來訪問greet函數返回的函數,即通過使用兩次括號(),即()()來調用,就是這樣:
JavaScript代碼:
functiongreet(){
name='Hammad';
returnfunction(){
console.log('Hi '+name);
}
}
greet()();// logs 'Hi Hammad'
公共作用域和私有作用域
在許多其他編程語言中,您可以使用公共,私有和受保護的作用域來設置類的屬性和方法的可見性。考慮使用PHP語言的這個例子:
PHP代碼:
// Public Scope
public$property;
publicfunctionmethod(){
// ...
}
// Private Sccpe
private$property;
privatefunctionmethod(){
// ...
}
// Protected Scope
protected$property;
protectedfunctionmethod(){
// ...
}
來自公共(全局)作用域的封裝函數使他們免受脆弱的攻擊。但是在JavaScript中,沒有公共或私有作用域。幸好,我們可以使用閉包來模擬此功能。為了保持一切與全局分離,我們必須首先將我們的函數封裝在如下所示的函數中:
JavaScript代碼:
(function(){
// 私有作用域 private scope
})();
函數末尾的括號會告知解析器在沒有調用的情況下一旦讀取完成就立即執行它。(愚人碼頭注:這其實叫立即執行函數表達式)我們可以在其中添加函數和變量,它們將不能在外部訪問。但是,如果我們想在外部訪問它們,也就是說我們希望其中一些公開的,另一些是私有的?我們可以使用一種稱為 模塊模式 的閉包類型,它允許我們使用對象中公共和私有的作用域來對我們的函數進行調整。
模塊模式
模塊模式類似這樣:
JavaScript代碼:
varModule=(function(){
functionprivateMethod(){
// do something
}
return{
publicMethod:function(){
// can call privateMethod();
}
};
})();
Module中的return語句包含了我們公開的函數。私有函數只是那些沒有返回的函數。沒有返回的函數不可以在Module命名空間之外訪問。但是公開函數可以訪問私有函數,這使它們對于助手函數,AJAX調用和其他事情很方便。
JavaScript代碼:
Module.publicMethod();// 可以正常工作
Module.privateMethod();// Uncaught ReferenceError: privateMethod is not defined
私有函數一個慣例是用下劃線開始,并返回一個包含我們公共函數的匿名對象。這使得它們很容易在長對象中管理。它看起來是這樣子的:
JavaScript代碼:
varModule=(function(){
function_privateMethod(){
// do something
}
functionpublicMethod(){
// do something
}
return{
publicMethod:publicMethod,
}
})();
立即執行函數表達式(IIFE)
另一種類型的閉包是立即執行函數表達式(IIFE)。這是一個在window上下文中調用的自動調用的匿名函數,這意味著this的值為window。暴露一個單一的全局接口來進行交互。他是這樣的:
JavaScript代碼:
(function(window){
// do anything
})(this);
使用 .call(), .apply() 和 .bind() 改變上下文
.call()和.apply()函數用于在調用函數時改變上下文。這給了你令人難以置信的編程能力(和一些終極權限來駕馭代碼)。
要使用call或apply函數,您只需要在函數上調用它,而不是使用一對括號調用函數,并將新的上下文作為第一個參數傳遞。
函數自己的參數可以在上下文之后傳遞。(愚人碼頭注:call或apply用另一個對象來調用一個方法,將一個函數上下文從初始的上下文改變為指定的新對象。簡單的說就是改變函數執行的上下文。)
JavaScript代碼:
functionhello(){
// do something...
}
hello();// 通常的調用方式
hello.call(context);// 在這里你可以傳遞上下文(this 值)作為第一個參數
hello.apply(context);// 在這里你可以傳遞上下文(this 值)作為第一個參數
.call()和.apply()之間的區別在于,在.call()中,其余參數作為以逗號分隔的列表,而.apply()則允許您在數組中傳遞參數。
JavaScript代碼:
functionintroduce(name,interest){
console.log('Hi! I\'m '+name+' and I like '+interest+'.');
console.log('The value of this is '+this+'.')
}
introduce('Hammad','Coding');// 通常的調用方式
introduce.call(window,'Batman','to save Gotham');// 在上下文之后逐個傳遞參數
introduce.apply('Hi',['Bruce Wayne','businesses']);// 在上下文之后傳遞數組中的參數
// 輸出:
// Hi! I'm Hammad and I like Coding.
// The value of this is [object Window].
// Hi! I'm Batman and I like to save Gotham.
// The value of this is [object Window].
// Hi! I'm Bruce Wayne and I like businesses.
// The value of this is Hi.
.call()的性能要比.apply()稍快。
以下示例將文檔中的項目列表逐個記錄到控制臺。
HTML 代碼:
Things to learn
Things to Learn to Rule the World
Learn PHP
Learn Laravel
Learn JavaScript
Learn VueJS
Learn CLI
Learn Git
Learn Astral Projection
// 在listItems中保存頁面上所有列表項的NodeList
varlistItems=document.querySelectorAll('ul li');
// 循環遍歷listItems NodeList中的每個節點,并記錄其內容
for(vari=0;i
(function(){
console.log(this.innerHTML);
}).call(listItems[i]);
}
// Output logs:
// Learn PHP
// Learn Laravel
// Learn JavaScript
// Learn VueJS
// Learn CLI
// Learn Git
// Learn Astral Projection
HTML僅包含無序的項目列表。然后 JavaScript 從DOM中選擇所有這些項目。列表循環,直到列表中的項目結束。在循環中,我們將列表項的內容記錄到控制臺。
該日志語句包裹在一個函數中,該call函數包含在調用函數中的括號中。將相應的列表項傳遞給調用函數,以便控制臺語句中的this關鍵字記錄正確對象的 innerHTML 。
對象可以有方法,同樣的函數對象也可以有方法。 事實上,JavaScript函數附帶了四種內置方法:
Function.prototype.apply()
Function.prototype.bind() ( ECMAScript 5 (ES5) 中引進)
Function.prototype.call()
Function.prototype.toString()
Function.prototype.toString() 返回函數源代碼的字符串表示形式。
到目前為止,我們討論過.call(),.apply()和toString()。與.call()和.apply()不同,.bind()本身不調用該函數,它只能用于在調用函數之前綁定上下文和其他參數的值。在上面的一個例子中使用.bind():
JavaScript代碼:
(functionintroduce(name,interest){
console.log('Hi! I\'m '+name+' and I like '+interest+'.');
console.log('The value of this is '+this+'.')
}).bind(window,'Hammad','Cosmology')();
// logs:
// Hi! I'm Hammad and I like Cosmology.
// The value of this is [object Window].
.bind()就像.call()函數一樣,它允許你傳遞其余的參數,用逗號分隔,而不是像apply,在數組中傳遞參數。
結論
這些概念是 JavaScript 的根本,對于了解高級語法很重要。我希望你能更好地了解JavaScript作用域和他相關的事情。如果沒用弄明白這些問題,歡迎在下面的評論中提問。
原文地址:https://scotch.io/tutorials/understanding-scope-in-javascript