簡介
JavaScript 有個特性稱為作用域报强。盡管對于很多開發(fā)新手來說,作用域的概念不容易理解拱燃,我會盡可能地從最簡單的角度向你解釋它們秉溉。理解作用域能讓你編寫更優(yōu)雅、錯誤更少的代碼碗誉,并能幫助你實現(xiàn)強大的設(shè)計模式召嘶。
什么是作用域?
作用域是你的代碼在運行時哮缺,各個變量弄跌、函數(shù)和對象的可訪問性。換句話說蝴蜓,作用域決定了你的代碼里的變量和其他資源在各個區(qū)域中的可見性碟绑。
為什么需要作用域俺猿?最小訪問原則
那么,限制變量的可見性格仲,不允許你代碼中所有的東西在任意地方都可用的好處是什么押袍?其中一個優(yōu)勢,是作用域為你的代碼提供了一個安全層級凯肋。計算機安全中谊惭,有個常規(guī)的原則是:用戶只能訪問他們當(dāng)前需要的東西。
想想計算機管理員吧侮东。他們在公司各個系統(tǒng)上擁有很多控制權(quán)圈盔,看起來甚至可以給予他們擁有全部權(quán)限的賬號。假設(shè)你有一家公司悄雅,擁有三個管理員驱敲,他們都有系統(tǒng)的全部訪問權(quán)限,并且一切運轉(zhuǎn)正常宽闲。但是突然發(fā)生了一點意外众眨,你的一個系統(tǒng)遭到惡意病毒攻擊。現(xiàn)在你不知道這誰出的問題了吧容诬?你這才意識到你應(yīng)該只給他們基本用戶的賬號娩梨,并且只在需要時賦予他們完全的訪問權(quán)。這能幫助你跟蹤變化并記錄每個人的操作览徒。這叫做最小訪問原則狈定。眼熟嗎?這個原則也應(yīng)用于編程語言設(shè)計习蓬,在大多數(shù)編程語言(包括 JavaScript)中稱為作用域纽什,接下來我們就要學(xué)習(xí)它。
在你的編程旅途中友雳,你會意識到作用域在你的代碼中可以提升性能稿湿,跟蹤 bug 并減少 bug。作用域還解決不同范圍的同名變量命名問題押赊。記住不要弄混作用域和上下文饺藤。它們是不同的特性。
JavaScript中的作用域
在 JavaScript 中有兩種作用域
全局作用域
局部作用域
當(dāng)變量定義在一個函數(shù)中時流礁,變量就在局部作用域中涕俗,而定義在函數(shù)之外的變量則從屬于全局作用域。每個函數(shù)在調(diào)用的時候會創(chuàng)建一個新的作用域神帅。
全局作用域
當(dāng)你在文檔中(document)編寫 JavaScript 時再姑,你就已經(jīng)在全局作用域中了。JavaScript 文檔中(document)只有一個全局作用域找御。定義在函數(shù)之外的變量會被保存在全局作用域中元镀。
// 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ù)中的變量就在局部作用域中。并且函數(shù)在每次調(diào)用時都有一個不同的作用域栖疑。這意味著同名變量可以用在不同的函數(shù)中讨永。因為這些變量綁定在不同的函數(shù)中,擁有不同作用域遇革,彼此之間不能訪問卿闹。
// Global Scope
function?someFunction()?{
????// Local Scope ##1
????function?someOtherFunction()?{
????????// Local Scope ##2
????}
}
// Global Scope
function?anotherFunction()?{
????// Local Scope ##3
}
// Global Scope
塊語句
塊級聲明包括if和switch,以及for和while循環(huán)萝快,和函數(shù)不同锻霎,它們不會創(chuàng)建新的作用域。在塊級聲明中定義的變量從屬于該塊所在的作用域揪漩。
if?(true)?{
????// this 'if' conditional block doesn't create a new scope
????var?name?=?'Hammad';?// name is still in the global scope
}
console.log(name);?// logs 'Hammad'
ECMAScript 6 引入了let和const關(guān)鍵字旋恼。這些關(guān)鍵字可以代替var。
var?name?=?'Hammad';
let likes?=?'Coding';
const?skills?=?'Javascript and PHP';
和var關(guān)鍵字不同氢拥,let和const關(guān)鍵字支持在塊級聲明中創(chuàng)建使用局部作用域蚌铜。
if?(true)?{
????// this 'if' conditional block doesn't create a scope
????// name is in the global scope because of the 'var' keyword
????var?name?=?'Hammad';
????// likes is in the local scope because of the 'let' keyword
????let likes?=?'Coding';
????// skills is in the local scope because of the 'const' keyword
????const?skills?=?'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
一個應(yīng)用中全局作用域的生存周期與該應(yīng)用相同。局部作用域只在該函數(shù)調(diào)用執(zhí)行期間存在嫩海。
上下文
很多開發(fā)者經(jīng)常弄混作用域和上下文,似乎兩者是一個概念囚痴。但并非如此叁怪。作用域是我們上面講到的那些,而上下文通常涉及到你代碼某些特殊部分中的this值深滚。作用域指的是變量的可見性奕谭,而上下文指的是在相同的作用域中的this的值。我們當(dāng)然也可以使用函數(shù)方法改變上下文痴荐,這個之后我們再討論血柳。在全局作用域中,上下文總是 Window 對象生兆。
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
console.log(this);
function?logFunction()?{
????console.log(this);
}
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// because logFunction() is not a property of an object
logFunction();
如果作用域定義在一個對象的方法中难捌,上下文就是這個方法所在的那個對象。
class?User?{
????logName()?{
????????console.log(this);
????}
}
(new?User).logName();?// logs User {}
(new User).logName()是創(chuàng)建對象關(guān)聯(lián)到變量并調(diào)用logName方法的一種簡便形式鸦难。通過這種方式你并不需要創(chuàng)建一個新的變量根吁。
你可能注意到一點,就是如果你使用new關(guān)鍵字調(diào)用函數(shù)時上下文的值會有差異合蔽。上下文會設(shè)置為被調(diào)用的函數(shù)的實例击敌。考慮一下上面的這個例子拴事,用new關(guān)鍵字調(diào)用的函數(shù)沃斤。
function?logFunction()?{
????console.log(this);
}
new?logFunction();?// logs logFunction {}
當(dāng)在嚴(yán)格模式(strict mode)中調(diào)用函數(shù)時圣蝎,上下文默認(rèn)是 undefined。
執(zhí)行環(huán)境
為了解決掉我們從上面學(xué)習(xí)中會出現(xiàn)的各種困惑衡瓶,“執(zhí)行環(huán)境(context)”這個詞中的“環(huán)境(context)”指的是作用域而并非上下文捅彻。這是一個怪異的命名約定,但由于 JavaScript 的文檔如此鞍陨,我們只好也這樣約定步淹。
JavaScript 是一種單線程語言,所以它同一時間只能執(zhí)行單個任務(wù)诚撵。其他任務(wù)排列在執(zhí)行環(huán)境中缭裆。當(dāng) JavaScript 解析器開始執(zhí)行你的代碼,環(huán)境(作用域)默認(rèn)設(shè)為全局寿烟。全局環(huán)境添加到你的執(zhí)行環(huán)境中澈驼,事實上這是執(zhí)行環(huán)境里的第一個環(huán)境。
之后筛武,每個函數(shù)調(diào)用都會添加它的環(huán)境到執(zhí)行環(huán)境中缝其。無論是函數(shù)內(nèi)部還是其他地方調(diào)用函數(shù),都會是相同的過程徘六。
每個函數(shù)都會創(chuàng)建它自己的執(zhí)行環(huán)境内边。
當(dāng)瀏覽器執(zhí)行完環(huán)境中的代碼,這個環(huán)境會從執(zhí)行環(huán)境中彈出待锈,執(zhí)行環(huán)境中當(dāng)前環(huán)境的狀態(tài)會轉(zhuǎn)移到父級環(huán)境漠其。瀏覽器總是先執(zhí)行在執(zhí)行棧頂?shù)膱?zhí)行環(huán)境(事實上就是你代碼最里層的作用域)。
全局環(huán)境只能有一個竿音,函數(shù)環(huán)境可以有任意多個和屎。
執(zhí)行環(huán)境有兩個階段:創(chuàng)建和執(zhí)行。
創(chuàng)建階段
第一階段是創(chuàng)建階段春瞬,是函數(shù)剛被調(diào)用但代碼并未執(zhí)行的時候柴信。創(chuàng)建階段主要發(fā)生了 3 件事。
創(chuàng)建變量對象
創(chuàng)建作用域鏈
設(shè)置上下文(this)的值
變量對象
變量對象(Variable Object)也稱為活動對象(activation object)宽气,包含所有變量随常、函數(shù)和其他在執(zhí)行環(huán)境中定義的聲明。當(dāng)函數(shù)調(diào)用時抹竹,解析器掃描所有資源线罕,包括函數(shù)參數(shù)、變量和其他聲明窃判。當(dāng)所有東西裝填進一個對象钞楼,這個對象就是變量對象。
'variableObject':?{
????// contains function arguments, inner variable and function declarations
}
作用域鏈
在執(zhí)行環(huán)境創(chuàng)建階段袄琳,作用域鏈在變量對象之后創(chuàng)建询件。作用域鏈包含變量對象燃乍。作用域鏈用于解析變量。當(dāng)解析一個變量時宛琅,JavaScript 開始從最內(nèi)層沿著父級尋找所需的變量或其他資源刻蟹。作用域鏈包含自己執(zhí)行環(huán)境以及所有父級環(huán)境中包含的變量對象。
'scopeChain':?{
????// contains its own variable object and other variable objects of the parent execution contexts
}
執(zhí)行環(huán)境對象
執(zhí)行環(huán)境可以用下面抽象對象表示:
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
}
代碼執(zhí)行階段
執(zhí)行環(huán)境的第二個階段就是代碼執(zhí)行階段嘿辟,進行其他賦值操作并且代碼最終被執(zhí)行舆瘪。
詞法作用域
詞法作用域的意思是在函數(shù)嵌套中,內(nèi)層函數(shù)可以訪問父級作用域的變量等資源红伦。這意味著子函數(shù)詞法綁定到了父級執(zhí)行環(huán)境英古。詞法作用域有時和靜態(tài)作用域有關(guān)。
function?grandfather()?{
????var?name?=?'Hammad';
????// likes is not accessible here
????function?parent()?{
????????// name is accessible here
????????// likes is not accessible here
????????function?child()?{
????????????// Innermost level of the scope chain
????????????// name is also accessible here
????????????var?likes?=?'Coding';
????????}
????}
}
你可能注意到了詞法作用域是向前的昙读,意思是子執(zhí)行環(huán)境可以訪問name召调。但不是由父級向后的,意味著父級不能訪問likes蛮浑。這也告訴了我們唠叛,在不同執(zhí)行環(huán)境中同名變量優(yōu)先級在執(zhí)行棧由上到下增加。一個變量和另一個變量同名沮稚,內(nèi)層函數(shù)(執(zhí)行棧頂?shù)沫h(huán)境)有更高的優(yōu)先級艺沼。
閉包
閉包的概念和我們剛學(xué)習(xí)的詞法作用域緊密相關(guān)。當(dāng)內(nèi)部函數(shù)試著訪問外部函數(shù)的作用域鏈(詞法作用域之外的變量)時產(chǎn)生閉包壮虫。閉包包括它們自己的作用域鏈澳厢、父級作用域鏈和全局作用域。
閉包不僅能訪問外部函數(shù)的變量囚似,也能訪問外部函數(shù)的參數(shù)。
即使函數(shù)已經(jīng)return线得,閉包仍然能訪問外部函數(shù)的變量饶唤。這意味著return的函數(shù)允許持續(xù)訪問外部函數(shù)的所有資源。
當(dāng)你的外部函數(shù)return一個內(nèi)部函數(shù)贯钩,調(diào)用外部函數(shù)時return的函數(shù)并不會被調(diào)用募狂。你必須先用一個單獨的變量保存外部函數(shù)的調(diào)用,然后將這個變量當(dāng)做函數(shù)來調(diào)用角雷』銮睿看下面這個例子:
function?greet()?{
????name?=?'Hammad';
????return?function?()?{
????????console.log('Hi '?+?name);
????}
}
greet();?// nothing happens, no errors
// the returned function from greet() gets saved in greetLetter
greetLetter?=?greet();
// calling greetLetter calls the returned function from the greet() function
greetLetter();?// logs 'Hi Hammad'
值得注意的是,即使在greet函數(shù)return后勺三,greetLetter函數(shù)仍可以訪問greet函數(shù)的name變量雷滚。如果不使用變量賦值來調(diào)用greet函數(shù)return的函數(shù),一種方法是使用()兩次()()吗坚,如下所示:
function?greet()?{
????name?=?'Hammad';
????return?function?()?{
????????console.log('Hi '?+?name);
????}
}
greet()();?// logs 'Hi Hammad'
共有作用域和私有作用域
在許多其他編程語言中祈远,你可以通過 public呆万、private 和 protected 作用域來設(shè)置類中變量和方法的可見性〕捣荩看下面這個 PHP 的例子
// Public Scope
public?$property;
public?function?method()?{
??// ...
}
// Private Sccpe
private?$property;
private?function?method()?{
??// ...
}
// Protected Scope
protected?$property;
protected?function?method()?{
??// ...
}
將函數(shù)從公有(全局)作用域中封裝谋减,使它們免受攻擊。但在 JavaScript 中扫沼,沒有 共有作用域和私有作用域出爹。然而我們可以用閉包實現(xiàn)這一特性。為了使每個函數(shù)從全局中分離出去缎除,我們要將它們封裝進如下所示的函數(shù)中:
(function?()?{
??// private scope
})();
函數(shù)結(jié)尾的括號告訴解析器立即執(zhí)行此函數(shù)严就。我們可以在其中加入變量和函數(shù),外部無法訪問伴找。但如果我們想在外部訪問它們盈蛮,也就是說我們希望它們一部分是公開的,一部分是私有的技矮。我們可以使用閉包的一種形式抖誉,稱為模塊模式(Module Pattern),它允許我們用一個對象中的公有作用域和私有作用域來劃分函數(shù)衰倦。
模塊模式
模塊模式如下所示:
var?Module?=?(function()?{
????function?privateMethod()?{
????????// do something
????}
????return?{
????????publicMethod:?function()?{
????????????// can call privateMethod();
????????}
????};
})();
Module 的return語句包含了我們的公共函數(shù)袒炉。私有函數(shù)并沒有被return。函數(shù)沒有被return確保了它們在 Module 命名空間無法訪問樊零。但我們的共有函數(shù)可以訪問我們的私有函數(shù)我磁,方便它們使用有用的函數(shù)、AJAX 調(diào)用或其他東西驻襟。
Module.publicMethod();?// works
Module.privateMethod();?// Uncaught ReferenceError: privateMethod is not defined
一種習(xí)慣是以下劃線作為開始命名私有函數(shù)夺艰,并返回包含共有函數(shù)的匿名對象。這使它們在很長的對象中很容易被管理沉衣。向下面這樣:
var?Module?=?(function?()?{
????function?_privateMethod()?{
????????// do something
????}
????function?publicMethod()?{
????????// do something
????}
????return?{
????????publicMethod:?publicMethod,
????}
})();
立即執(zhí)行函數(shù)表達式(IIFE)
另一種形式的閉包是立即執(zhí)行函數(shù)表達式(Immediately-Invoked Function Expression郁副,IIFE)。這是一種在 window 上下文中自調(diào)用的匿名函數(shù)豌习,也就是說this的值是window存谎。它暴露了一個單一全局接口用來交互。如下所示:
(function(window)?{
????// do anything
})(this);
使用 .call()肥隆, .apply() 和 .bind() 改變上下文
Call 和 Apply 函數(shù)來改變函數(shù)調(diào)用時的上下文既荚。這帶給你神奇的編程能力(和終極統(tǒng)治世界的能力)。你只需要使用 call 和 apply 函數(shù)并把上下文當(dāng)做第一個參數(shù)傳入栋艳,而不是使用括號來調(diào)用函數(shù)恰聘。函數(shù)自己的參數(shù)可以在上下文后面?zhèn)魅搿?/p>
function?hello()?{
????// do something...
}
hello();?// the way you usually call it
hello.call(context);?// here you can pass the context(value of this) as the first argument
hello.apply(context);?// here you can pass the context(value of this) as the first argument
.call()和.apply()的區(qū)別是 Call 中其他參數(shù)用逗號分隔傳入,而 Apply 允許你傳入一個參數(shù)數(shù)組。
function?introduce(name,?interest)?{
????console.log('Hi! I'm?'+ name +'?and?I?like?'+ interest +'.');
????console.log('The value of?this?is?'+ this +'.')
}
introduce('Hammad', 'Coding'); // the way you usually call it
introduce.call(window, 'Batman', 'to?save?Gotham'); // pass the arguments one by one after the contextt
introduce.apply('Hi', ['Bruce?Wayne', 'businesses']); // pass the arguments in an array after the context
// Output:
// 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 的效率高一點憨琳。
下面這個例子列舉文檔中所有項目诫钓,然后依次在控制臺打印出來。
????
????Things?to?learn
????
Things?to?Learn?to?Rule?the?World
????
????????
????????
????????
????????
????????
????????
????????
????
????????// Saves a NodeList of all list items on the page in listItems
????????var?listItems?=?document.querySelectorAll('ul li');
????????// Loops through each of the Node in the listItems NodeList and logs its content
????????for?(var?i?=?0;?i?<?listItems.length;?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 中選取它們菌湃。列表項會被從頭到尾循環(huán)一遍。在循環(huán)時遍略,我們把列表項的內(nèi)容輸出到控制臺惧所。
輸出語句包含在由括號包裹的函數(shù)中,然后調(diào)用call函數(shù)绪杏。相應(yīng)的列表項傳入 call 函數(shù)下愈,確保控制臺輸出正確對象的 innerHTML蕾久。
對象可以有方法势似,同樣函數(shù)對象也可以有方法。事實上僧著,JavaScript 函數(shù)有 4 個內(nèi)置方法:
Function.prototype.apply()
Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))
Function.prototype.call()
Function.prototype.toString()
Function.prototype.toString()返回函數(shù)代碼的字符串表示履因。
到現(xiàn)在為止,我們討論了.call()盹愚、.apply()和toString()栅迄。與 Call 和 Apply 不同,Bind 并不是自己調(diào)用函數(shù)皆怕,它只是在函數(shù)調(diào)用之前綁定上下文和其他參數(shù)毅舆。在上面提到的例子中使用 Bind:
(function?introduce(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函數(shù)一樣用逗號分隔其他傳入?yún)?shù),不像apply那樣用數(shù)組傳入?yún)?shù)愈腾。
結(jié)論
這些概念是 JavaScript 的基礎(chǔ)涤久,如果你想鉆研更深的話澈缺,理解這些很重要姻檀。我希望你對 JavaScript 作用域及相關(guān)概念有了更好地理解皂甘。如果有東西不清楚作谭,可以在評論區(qū)提問苍凛。
作用域常伴你的代碼左右箍铭,享受編碼螟炫!