???? javascript 語(yǔ)言中的作用域和上下文的實(shí)現(xiàn)比較獨(dú)特,在某種程度上是因?yàn)閖avascript是一種非常靈活的弱類型語(yǔ)言,函數(shù)可以用來(lái)封裝并保存不同類型的上下文以及作用域;這些概念是由權(quán)威的javascript設(shè)計(jì)者提供的;然而,這也成為開(kāi)發(fā)者們困惑的源頭,下面詳細(xì)介紹一下作用域和上下文之間的區(qū)別,以及如何利用各種設(shè)計(jì)模式.
上下文和作用域
???? 首先要明白上下文和作用域不是一回事,我注意到許多開(kāi)發(fā)人員多年來(lái)經(jīng)?;煜@兩個(gè)術(shù)語(yǔ)(包括我自己),錯(cuò)誤地,用一個(gè)術(shù)語(yǔ)去描述另一個(gè)術(shù)語(yǔ)。平心而論,這樣術(shù)語(yǔ)會(huì)變得很混亂。
??? 每個(gè)Function在調(diào)用的時(shí)候都會(huì)創(chuàng)建一個(gè)新的作用域以及上下文,從根本上說(shuō),作用域是基于函數(shù)的,上下文是基于對(duì)象的。換句話說(shuō),作用域與函數(shù)調(diào)用時(shí)變量的訪問(wèn)有關(guān),當(dāng)它被調(diào)用的時(shí)候,每個(gè)調(diào)用都是唯一的。上下文常常代表關(guān)鍵字"this"的值,指的是調(diào)用該方法的對(duì)象.
(補(bǔ)充知識(shí)點(diǎn):鏈?zhǔn)阶饔糜?chain scope):父對(duì)象的所有變量對(duì)子元素都是可見(jiàn)的,子元素的變量對(duì)父元素不可見(jiàn))
變量的作用域
???? 變量可以定義在局部環(huán)境中,也可以定義在全局環(huán)境中,在運(yùn)行時(shí)確定了不同作用域下的變量的可訪問(wèn)性.任何定義在全局中的變量,意味著一個(gè)在函數(shù)體之外聲明的變量能在整個(gè)函數(shù)運(yùn)行的過(guò)程中被調(diào)用,并且在任何地方都能被調(diào)用和修改,局部變量只存在于定義它們的函數(shù)體,每調(diào)用一次函數(shù)會(huì)產(chǎn)生不同的作用域;在一次調(diào)用的過(guò)程中進(jìn)行賦值,檢索和操作僅僅影響當(dāng)前的作用域下的值,其他作用域下的值不會(huì)被改變.
???? javascript不支持塊級(jí)作用域,類似于if語(yǔ)句,switch語(yǔ)句,for loop或者while loop語(yǔ)句.這意味著變量不能在這些語(yǔ)句之外被訪問(wèn),目前看來(lái),定義在語(yǔ)句塊中的變量可以在語(yǔ)句塊之外被訪問(wèn)到.但是這種現(xiàn)狀很快就會(huì)改變,因?yàn)镋S6規(guī)范中已經(jīng)出現(xiàn)了一個(gè)新的關(guān)鍵字let代替var,它可以用來(lái)聲明變量,并且產(chǎn)生塊級(jí)作用域(譯者增加:意思就是在語(yǔ)句塊之外訪問(wèn)不到,是undefined,let也不存在變量提升,這里不做多余解釋).
什么是this上下文
???? 上下文通常是取決于一個(gè)函數(shù)如何被調(diào)用.當(dāng)一個(gè)函數(shù)被當(dāng)成一個(gè)對(duì)象的方法調(diào)用的時(shí)候,this指向當(dāng)前調(diào)用該方法的對(duì)象;
var obj = {
???????? foo: function(){
? ? ? ?? ? ? ? ??? alert(this === obj);
????????? }
};
obj.foo(); // true
???? 同樣的原則適用于通過(guò)new操作符來(lái)創(chuàng)建對(duì)象的一個(gè)實(shí)例來(lái)調(diào)用一個(gè)函數(shù)。以創(chuàng)建實(shí)例的方式調(diào)用時(shí),這個(gè)函數(shù)的作用域內(nèi)的值將被設(shè)置為新創(chuàng)建的實(shí)例.
function foo(){
?????? alert(this);
}
foo() //window
new foo() // foo
????? 當(dāng)函數(shù)未綁定的時(shí)候,this指向全局上下文,或者瀏覽器中的window對(duì)象.但是如果函數(shù)是運(yùn)行在嚴(yán)格模式下的話,上下文默認(rèn)為undefined.
執(zhí)行上下文
?????? JavaScript是一個(gè)單線程的語(yǔ)言,這意味著一次只能執(zhí)行一個(gè)任務(wù).JavaScript解釋器初次執(zhí)行代碼時(shí),它首先進(jìn)入一個(gè)全局默認(rèn)的執(zhí)行上下文。此后,每次調(diào)用一個(gè)函數(shù)將導(dǎo)致創(chuàng)建一個(gè)新的執(zhí)行上下文。
??????? 這就是導(dǎo)致困惑的原因,術(shù)語(yǔ)"執(zhí)行上下文"在這里指的是作用域,并不是前面討論的上下文,更加不幸的是它已經(jīng)作為ECMAScript規(guī)范存在,所以我們只能接受.(譯者增加:術(shù)語(yǔ)"執(zhí)行上下文"只是名字和"上下文"相似,而且有時(shí)候也會(huì)把前者簡(jiǎn)稱為后者,但是他們并沒(méi)有關(guān)系).
???????? 每次當(dāng)創(chuàng)建一個(gè)新的執(zhí)行上下文的時(shí)候,它都會(huì)被添加到當(dāng)前執(zhí)行棧的頂部,瀏覽器總是執(zhí)行位于當(dāng)前執(zhí)行棧頂部的執(zhí)行上下文.一旦結(jié)束,就會(huì)從棧頂移除,并且依次繼續(xù)執(zhí)行下面的函數(shù).
????????? 一個(gè)執(zhí)行上下文可以分為創(chuàng)建和執(zhí)行階段。在創(chuàng)建階段,解釋器將首先創(chuàng)建一個(gè)變量對(duì)象(也稱為一個(gè)激活對(duì)象),是由所有的變量,函數(shù)聲明,定義的參數(shù)組成的。接下來(lái)是原型鏈的初始化,this的值被決定。然后在執(zhí)行階段,解釋和執(zhí)行代碼。
原型鏈
?????? 對(duì)于每個(gè)執(zhí)行上下文都有一個(gè)原型鏈耦合。該作用域鏈包含了在執(zhí)行堆棧中每個(gè)執(zhí)行上下文中的變量對(duì)象。它是用來(lái)確定變量訪問(wèn)和標(biāo)識(shí)符解析。例如:
function first(){
?????????? second();
?????????? function second(){
?????????????????? third();
?????????????????? function third(){
????????????????????????? fourth();
????????????????????????? function fourth(){
??????????????????????????????? // do something
?????????????????????????? }
?????????????????? }
????????? }
}
first();
???? 運(yùn)行前面的代碼會(huì)導(dǎo)致嵌套函數(shù)一直向下執(zhí)行到fourth()函數(shù)。此時(shí)原型鏈的范圍,從上到下:fourth,third,second,first,global。fourth函數(shù)能夠訪問(wèn)全局變量以及定義在first, second和third函數(shù)中的變量以及函數(shù)本身.
???? 可以搜索作用域鏈來(lái)解決不同的執(zhí)行上下文中的變量命名沖突的問(wèn)題,從局部變量一直向上到全局變量,這意味著局部變量和作用域鏈更高的變量中具有相同名稱時(shí),會(huì)優(yōu)先考慮局部變量,支持就近原則.
????? 簡(jiǎn)而言之,每當(dāng)你試圖在函數(shù)的執(zhí)行上下文中訪問(wèn)一個(gè)變量時(shí),查找過(guò)程總是從自身的變量對(duì)象開(kāi)始。如果變量的標(biāo)識(shí)符在自身的對(duì)象中沒(méi)有找到,搜索范圍會(huì)持續(xù)一直到作用域鏈。它會(huì)查詢整個(gè)作用域鏈檢查每個(gè)執(zhí)行上下文的變量對(duì)象范圍來(lái)尋找匹配的變量名。
閉包
?????? 當(dāng)在嵌套函數(shù)中訪問(wèn)函數(shù)之外的值的時(shí)候就會(huì)創(chuàng)建一個(gè)閉包。換句話說(shuō),一個(gè)嵌套函數(shù)內(nèi)部定義另一個(gè)函數(shù)時(shí)就形成一個(gè)閉包,在這個(gè)函數(shù)內(nèi)部允許訪問(wèn)外部函數(shù)的變量。它將在外部函數(shù)返回時(shí)被執(zhí)行,允許在內(nèi)部函數(shù)中訪問(wèn)外部函數(shù)的局部變量、參數(shù)和函數(shù)聲明.封裝允許我們從外部作用域中隱藏和保護(hù)執(zhí)行上下文,而暴露公共接口,通過(guò)接口進(jìn)一步操作,舉一個(gè)簡(jiǎn)單的例子:
function foo(){
????????? var localVariable = 'private variable';
????????? return function bar(){
???????? ? ? ?????? return localVariable;
???????? }
}
var getLocalVariable = foo();
getLocalVariable(); // private variable
最受歡迎的閉包模式是廣為人知的模塊模式,它允許你模擬共有成員,私有成員和特權(quán)成員
var Module = (function(){
???????? var privateProperty = 'foo';
??????? function privateMethod(args){
?????????????? // do something
??????? }
??????? return {
???????????? publicProperty: '',
???????????? publicMethod: function(args){
???????????????????? // do something
??????? },
???????? privilegedMethod: function(args){
?????????????? return privateMethod(args);
???????? }
};
})();
???? 這個(gè)模塊類似一個(gè)單例,因此,在函數(shù)末尾添加一對(duì)(),js解析器解析完成之后就會(huì)立即執(zhí)行函數(shù)。在閉包執(zhí)行的上下文之外唯一能獲取到的是返回對(duì)象中的屬性和公共方法(publicMethod)。然而,所有私有屬性和方法將在應(yīng)用執(zhí)行上下文中一直存在,意味著變量通過(guò)公共方法會(huì)進(jìn)一步發(fā)生改變。
另一種類型的閉包是立即調(diào)用函數(shù)(IIFE),也就是在window的執(zhí)行上下文中的一個(gè)自調(diào)用的匿名函數(shù)
(function(window){
???????? var foo, bar;
???????? function private(){
????????? ? ? ??? // do something
????????? }
????????? window.Module = {
?????????????????? public: function(){
?????????????????? // do something
??????? }
};
})(this);
????? 這個(gè)表達(dá)式對(duì)于保護(hù)全局變量非常有用,任何一個(gè)在函數(shù)體內(nèi)聲明的變量都是局部變量,通過(guò)閉包在整個(gè)函數(shù)運(yùn)行周期中都存在。這是一個(gè)受歡迎的封裝源代碼應(yīng)用程序和框架的方法,通常暴露單一的全局接口進(jìn)行交互。
Call 和 Apply
??????? 這兩種方法固有的功能是對(duì)于所有函數(shù)允許你在任何期望的上下文中執(zhí)行任何功能。這是令人難以置信的強(qiáng)大能力。call函數(shù)需要顯式地列出的參數(shù),apply函數(shù)允許您提供參數(shù)作為數(shù)組:
function user(firstName, lastName, age){
??????????? // do something
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);
這兩個(gè)調(diào)用的結(jié)果是完全相同的,在window的執(zhí)行上下文中調(diào)用user函數(shù),提供相同的三個(gè)參數(shù)。
?????? ECMAScript 5(ES5)介紹了Function.prototype.bind方法,用于操作上下文。它返回一個(gè)新函數(shù),該函數(shù)將被永久地綁定到bind方法下的第一個(gè)參數(shù),而不管它是如何被調(diào)用的.它是通過(guò)一個(gè)閉包調(diào)用適當(dāng)?shù)纳舷挛???吹揭韵聀olyfill不受支持的瀏覽器:
if(!('bind' in Function.prototype)){
???????????? Function.prototype.bind = function(){
? ? ? ? ????????????? var fn = this,
??????????? ? ? ? ? ? context = arguments[0],
????? ? ? ? ? ? ? ???? args = Array.prototype.slice.call(arguments, 1);
?????? ? ? ? ? ? ? ??? return function(){
???????????????????? ? ? ? ? ?? return fn.apply(context, args.concat([].slice.call(arguments)));
? ? ???????? ? ? ? ? }
????? ? ? ?? }
}
??????? 它經(jīng)常用在上下文丟失的情況下;面向?qū)ο蠛褪录幚?這很有必要,因?yàn)橐粋€(gè)節(jié)點(diǎn)的addEventListener方法總是在綁定了節(jié)點(diǎn)事件處理器的上下文中去執(zhí)行回調(diào)函數(shù),的確也應(yīng)該這么做,然而,如果你采用高級(jí)的面向?qū)ο蠹夹g(shù),需要回調(diào)一個(gè)實(shí)例的方法,您將需要手動(dòng)調(diào)整的上下文,此時(shí)bind派上了用場(chǎng):
function MyClass(){
????????????? this.element = document.createElement('div');
????????????? this.element.addEventListener('click', this.onClick.bind(this), false);
}
MyClass.prototype.onClick = function(e){
????????????? // do something
};
????? 當(dāng)回顧Function.prototype.bind的源碼的時(shí)候,你已經(jīng)注意到關(guān)于數(shù)組的slice方法(譯者增加:這是將類數(shù)組轉(zhuǎn)化成數(shù)組的方法之一,上下兩行代碼功能一樣)
Array.prototype.slice.call(arguments, 1);
[].slice.call(arguments);
??????? 比較有趣的一點(diǎn)是這里的arguments對(duì)象不再是一個(gè)真正意義上的數(shù)組,通常被稱為類數(shù)組就像節(jié)點(diǎn)列表一樣(可以是element.childNodes的任意返回值).它們具有l(wèi)ength屬性和index值,但依然不是數(shù)組,而且也不支持?jǐn)?shù)組原生的內(nèi)置方法,比如slice和push.然而,類數(shù)組和數(shù)組有著相似的表現(xiàn)形式,因此也可以執(zhí)行數(shù)組的方法,上面的代碼中,數(shù)組的方法就是在一個(gè)類數(shù)組的上下文中被執(zhí)行的;
這種使用其他對(duì)象方法的技巧同樣也適應(yīng)于在javascript中模擬類繼承時(shí)的面向?qū)ο?
MyClass.prototype.init = function(){
????????? // call the superclass init method in the context of the "MyClass" instance
?????????? MySuperClass.prototype.init.apply(this, arguments);
}
??????? 通過(guò)調(diào)用子類(MyClass)對(duì)象實(shí)例下的superclass的方法(MySuperClass),我們可以充分利用這個(gè)強(qiáng)大的設(shè)計(jì)模式的能力來(lái)模仿調(diào)用的方法.
結(jié)論
???? 在你開(kāi)始接觸設(shè)計(jì)模式之前理解這些概念非常重要,作用域和上下文在現(xiàn)代JavaScript中發(fā)揮著基礎(chǔ)性的作用。在談?wù)撻]包、面向?qū)ο蠛屠^承,或各種原生實(shí)現(xiàn)、上下文和作用域都發(fā)揮著重要的作用。如果你的目標(biāo)是掌握J(rèn)avaScript語(yǔ)言并深入理解它的組成,作用域和上下文應(yīng)該是你的一個(gè)起點(diǎn)。