javascript中的作用域(scope)和上下文(context)是這門語言的獨到之處,這部分歸功于他們帶來的靈活性。每個函數(shù)有不同的變量上下文和作用域。這些概念是javascript中一些強大的設計模式的后盾。然而這也給開發(fā)人員帶來很大困惑。下面全面揭示了javascript中的上下文和作用域的不同,以及各種設計模式如何使用他們。
上下文 vs 作用域
首先需要澄清的問題是上下文和作用域是不同的概念。多年來我注意到許多開發(fā)者經(jīng)常將這兩個術語混淆,錯誤的將一個描述為另一個。平心而論,這些術語變得非?;靵y不堪。
每個函數(shù)調(diào)用都有與之相關的作用域和上下文。從根本上說,范圍是基于函數(shù)(function-based)而上下文是基于對象(object-based)。換句話說,作用域是和每次函數(shù)調(diào)用時變量的訪問有關,并且每次調(diào)用都是獨立的。上下文總是關鍵字 this 的值,是調(diào)用當前可執(zhí)行代碼的對象的引用。
變量作用域
變量能夠被定義在局部或者全局作用域,這導致運行時變量的訪問來自不同的作用域。全局變量需被聲明在函數(shù)體外,在整個運行過程中都存在,能在任何作用域中訪問和修改。局部變量僅在函數(shù)體內(nèi)定義,并且每次函數(shù)調(diào)用都有不同的作用域。這主題是僅在調(diào)用中的賦值,求值和對值的操作,不能訪問作用域之外的值。
目前javascript不支持塊級作用域,塊級作用域指在if語句,switch語句,循環(huán)語句等語句塊中定義變量,這意味著變量不能在語句塊之外被訪問。當前任何在語句塊中定義的變量都能在語句塊之外訪問。然而,這種情況很快會得到改變,let 關鍵字已經(jīng)正式添加到ES6規(guī)范。用它來代替var關鍵字可以將局部變量聲明為塊級作用域。
"this" 上下文
上下文通常是取決于一個函數(shù)如何被調(diào)用。當函數(shù)作為對象的方法被調(diào)用時,this 被設置為調(diào)用方法的對象:
代碼如下:
var object = {
foo: function(){
alert(this === object);
}
};
object.foo(); // true
同樣的原理適用于當調(diào)用一個函數(shù)時通過new的操作符創(chuàng)建一個對象的實例。當以這種方式調(diào)用時,this 的值將被設置為新創(chuàng)建的實例:
代碼如下:
function foo(){
alert(this);
}
foo() // window
new foo() // foo
當調(diào)用一個未綁定函數(shù),this 將被默認設置為 全局上下文(global context) 或window對象(如果在瀏覽器中)。然而如果函數(shù)在嚴格模式下被執(zhí)行("use strict"),this的值將被默認設置為undefined。
執(zhí)行上下文和作用域鏈
javascript是一個單線程語言,這意味著在瀏覽器中同時只能做一件事情。當javascript解釋器初始執(zhí)行代碼,它首先默認竟如全局上下文。每次調(diào)用一個函數(shù)將會創(chuàng)建一個新的執(zhí)行上下文。
這里經(jīng)常發(fā)生混淆,這術語”執(zhí)行上下文(execution context)“在這里的所要表達的意思是作用域,不是前面討論的上下文。這是槽糕的命名,然而這術語ECMAScript規(guī)范所定義的,無奈的遵守吧。
每次新創(chuàng)建一個執(zhí)行上下文,會被添加到作用域鏈的頂部,又是也成為執(zhí)行或調(diào)用棧。瀏覽器總是運行在位于作用域鏈頂部當前執(zhí)行上下文。一旦完成,它(當前執(zhí)行上下文)將從棧頂被移除并且將控制權歸還給之前的執(zhí)行上下文。例如:
代碼如下:
function first(){
second();
function second(){
third();
function third(){
fourth();
function fourth(){
// do something
}
}
}
}
first();
運行前面的代碼將會導致嵌套的函數(shù)被從上倒下執(zhí)行直到 fourth 函數(shù),此時作用域鏈從上到下為: fourth, third, second, first, global。fourth 函數(shù)能夠訪問全局變量和任何在first,second和third函數(shù)中定義的變量,就如同訪問自己的變量一樣。一旦fourth函數(shù)執(zhí)行完成,fourth暈高興上下文將被從作用域鏈頂端移除并且執(zhí)行將返回到thrid函數(shù)。這一過程持續(xù)進行直到所有代碼已完成執(zhí)行。
不同執(zhí)行上下文之間的變量命名沖突通過攀爬作用域鏈解決,從局部直到全局。這意味著具有相同名稱的局部變量在作用域鏈中有更高的優(yōu)先級。
簡單的說,每次你試圖訪問函數(shù)執(zhí)行上下文中的變量時,查找進程總是從自己的變量對象開始。如果在自己的變量對象中沒發(fā)現(xiàn)要查找的變量,繼續(xù)搜索作用域鏈。它將攀爬作用域鏈檢查每一個執(zhí)行上下文的變量對象去尋找和變量名稱匹配的值。
閉包
當一個嵌套的函數(shù)在定義(作用域)的外面被訪問,以至它可以在外部函數(shù)返回后被執(zhí)行,此時一個閉包形成。它(閉包)維護(在內(nèi)部函數(shù)中)對外部函數(shù)中局部變量,arguments和函數(shù)聲明的訪問。封裝允許我們從外部作用域中隱藏和保護執(zhí)行上下文,而暴露公共接口,通過接口進一步操作。一個簡單的例子看起來如下:
代碼如下:
function foo(){
var local = 'private variable';
return function bar(){
return local;
}
}
var getLocalVariable = foo();
getLocalVariable() // private variable
其中最流行的閉包類型是廣為人知的模塊模式。它允許你模擬公共的,私有的和特權成員:
代碼如下:
var Module = (function(){
var privateProperty = 'foo';
function privateMethod(args){
//do something
}
return {
publicProperty: "",
publicMethod: function(args){
//do something
},
privilegedMethod: function(args){
privateMethod(args);
}
}
})();
模塊實際上有些類似于單例,在末尾添加一對括號,當解釋器解釋完后立即執(zhí)行(立即執(zhí)行函數(shù))。閉包執(zhí)行上下位的外部唯一可用的成員是返回對象中公用的方法和屬性(例如Module.publicMethod)。然而,所有的私有屬性和方法在整個程序的生命周期中都將存在,由于(閉包)使執(zhí)行上下文收到保護,和變量的交互要通過公用的方法。
另一種類型的閉包叫做立即調(diào)用函數(shù)表達式(immediately-invoked function expression IIFE),無非是一個在window上下文中的自調(diào)用匿名函數(shù)(self-invoked anonymous function)。
代碼如下:
function(window){
var a = 'foo', b = 'bar';
function private(){
// do something
}
window.Module = {
public: function(){
// do something
}
};
})(this);
對保護全局命名空間,這種表達式非常有用,所有在函數(shù)體內(nèi)聲明的變量都是局部變量,并通過閉包在整個運行環(huán)境保持存在。這種封裝源代碼的方式對程序和框架都是非常流行的,通常暴露單一全局接口與外界交互。
Call 和 Apply
這兩個簡單的方法,內(nèi)建在所有的函數(shù)中,允許在自定義上下文中執(zhí)行函數(shù)。call 函數(shù)需要參數(shù)列表而 apply 函數(shù)允許你傳遞參數(shù)為數(shù)組:
代碼如下:
function user(first, last, age){
// do something
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);
執(zhí)行的結(jié)果是相同的,user 函數(shù)在window上下文上被調(diào)用,并提供了相同的三個參數(shù)。
ECMAScript 5 (ES5)引入了Function.prototype.bind方法來控制上下文,它返回一個新函數(shù),這函數(shù)(的上下文)被永久綁定到bind方法的第一個參數(shù),無論函數(shù)被如何調(diào)用。它通過閉包修正函數(shù)的上下文,下面是為不支持的瀏覽器提供的方案:
代碼如下:
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);
}
}
}
它常用在上下文丟失:面向?qū)ο蠛褪录幚?。這點有必要的因為 節(jié)點的addEventListener 方法總保持函數(shù)執(zhí)行的上下文為事件處理被綁定的節(jié)點,這點很重要。然而如果你使用高級面向?qū)ο蠹夹g并且需要維護回調(diào)函數(shù)的上下文是方法的實例,你必須手動調(diào)整上下文。這就是bind 帶來的方便:
代碼如下:
function MyClass(){
this.element = document.createElement('div');
this.element.addEventListener('click', this.onClick.bind(this), false);
}
MyClass.prototype.onClick = function(e){
// do something
};
當回顧bind函數(shù)的源代碼,你可能注意到下面這一行相對簡單的代碼,調(diào)用Array的一個方法:
代碼如下:
Array.prototype.slice.call(arguments, 1);
有趣的是,這里需要注意的是arguments對象實際上并不是一個數(shù)組,然而它經(jīng)常被描述為類數(shù)組(array-like)對象,很向 nodelist(document.getElementsByTagName()方法返回的結(jié)果)。他們包含lenght屬性,值能夠被索引,但他們?nèi)匀徊皇菙?shù)組,由于他們不支持原生的數(shù)組方法,比如slice和push。然而,由于他們有和數(shù)組類似的行為,數(shù)組的方法能被調(diào)用和劫持。如果你想這樣,在類數(shù)組的上下文中執(zhí)行數(shù)組方法,可參照上面的例子。
這種調(diào)用其他對象方法的技術也被應用到面向?qū)ο笾?,當在javascript中模仿經(jīng)典繼承(類繼承):
代碼如下:
MyClass.prototype.init = function(){
// call the superclass init method in the context of the "MyClass" instance
MySuperClass.prototype.init.apply(this, arguments);
}
通過在子類(MyClass)的實例中調(diào)用超類(MySuperClass)的方法,我們能重現(xiàn)這種強大的設計模式。
結(jié)論
在你開始學習高級設計模式之前理解這些概念是非常重要的,由于作用域和上下文在現(xiàn)代javascript中扮演重要的和根本的角色。無論我們談論閉包,面向?qū)ο?,和繼承或各種原生實現(xiàn),上下文和作用域都扮演重要角色。如果你的目標是掌握javascript語言并深入了解它的組成,作用域和上下文應該是你的起點。
譯者補充
作者實現(xiàn)的bind函數(shù)是不完全的,調(diào)用bind返回的函數(shù)時不能傳遞參數(shù),下面的代碼修復了這個問題:
代碼如下:
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(arguments));//fixed
}
}
}
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com