深入淺析JavaScript中的作用域和上下文
javascript中的作用域(scope)和上下文(context)是這門語言的獨(dú)到之處,這部分歸功于他們帶來的靈活性。每個(gè)函數(shù)有不同的變量上下文和作用域。這些概念是javascript中一些強(qiáng)大的設(shè)計(jì)模式的后盾。然而這也給開發(fā)人員帶來很大困惑。下面全面揭示了javascript中的上下文和作用域的不同,以及各種設(shè)計(jì)模式如何使用他們。
上下文(Context)和作用域(Scope)
首先需要知道的是,上下文和作用域是兩個(gè)完全不同的概念。多年來,我發(fā)現(xiàn)很多開發(fā)者會(huì)混淆這兩個(gè)概念(包括我自己), 錯(cuò)誤的將兩個(gè)概念混淆了。平心而論,這些年來很多術(shù)語都被混亂的使用了。
函數(shù)的每次調(diào)用都有與之緊密相關(guān)的作用域和上下文。從根本上來說,作用域是基于函數(shù)的,而上下文是基于對(duì)象的。 換句話說,作用域涉及到所被調(diào)用函數(shù)中的變量訪問,并且不同的調(diào)用場(chǎng)景是不一樣的。上下文始終是this關(guān)鍵字的值, 它是擁有(控制)當(dāng)前所執(zhí)行代碼的對(duì)象的引用。
變量作用域
一個(gè)變量可以被定義在局部或者全局作用域中,這建立了在運(yùn)行時(shí)(runtime)期間變量的訪問性的不同作用域范圍。 任何被定義的全局變量,意味著它需要在函數(shù)體的外部被聲明,并且存活于整個(gè)運(yùn)行時(shí)(runtime),并且在任何作用域中都可以被訪問到。 在ES6之前,局部變量只能存在于函數(shù)體中,并且函數(shù)的每次調(diào)用它們都擁有不同的作用域范圍。 局部變量只能在其被調(diào)用期的作用域范圍內(nèi)被賦值、檢索、操縱。
需要注意,在ES6之前,JavaScript不支持塊級(jí)作用域,這意味著在if語句、switch語句、for循環(huán)、while循環(huán)中無法支持塊級(jí)作用域。 也就是說,ES6之前的JavaScript并不能構(gòu)建類似于Java中的那樣的塊級(jí)作用域(變量不能在語句塊外被訪問到)。但是, 從ES6開始,你可以通過let關(guān)鍵字來定義變量,它修正了var關(guān)鍵字的缺點(diǎn),能夠讓你像Java語言那樣定義變量,并且支持塊級(jí)作用域??磧蓚€(gè)例子:
ES6之前,我們使用var關(guān)鍵字定義變量:
function func() { if (true) { var tmp = 123; } console.log(tmp); // 123 }
之所以能夠訪問,是因?yàn)関ar關(guān)鍵字聲明的變量有一個(gè)變量提升的過程。而在ES6場(chǎng)景,推薦使用let關(guān)鍵字定義變量:
function func() { if (true) { let tmp = 123; } console.log(tmp); // ReferenceError: tmp is not defined }
這種方式,能夠避免很多錯(cuò)誤。
什么是this上下文
上下文通常取決于函數(shù)是如何被調(diào)用的。當(dāng)一個(gè)函數(shù)被作為對(duì)象中的一個(gè)方法被調(diào)用的時(shí)候,this被設(shè)置為調(diào)用該方法的對(duì)象上:
var obj = { foo: function(){ alert(this === obj); } }; obj.foo(); // true
這個(gè)準(zhǔn)則也適用于當(dāng)調(diào)用函數(shù)時(shí)使用new操作符來創(chuàng)建對(duì)象的實(shí)例的情況下。在這種情況下,在函數(shù)的作用域內(nèi)部this的值被設(shè)置為新創(chuàng)建的實(shí)例:
function foo(){ alert(this); } new foo() // foo foo() // window
當(dāng)調(diào)用一個(gè)為綁定函數(shù)時(shí),this默認(rèn)情況下是全局上下文,在瀏覽器中它指向window對(duì)象。需要注意的是,ES5引入了嚴(yán)格模式的概念, 如果啟用了嚴(yán)格模式,此時(shí)上下文默認(rèn)為undefined。
執(zhí)行環(huán)境(execution context)
JavaScript是一個(gè)單線程語言,意味著同一時(shí)間只能執(zhí)行一個(gè)任務(wù)。當(dāng)JavaScript解釋器初始化執(zhí)行代碼時(shí), 它首先默認(rèn)進(jìn)入全局執(zhí)行環(huán)境(execution context),從此刻開始,函數(shù)的每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的執(zhí)行環(huán)境。
這里會(huì)經(jīng)常引起新手的困惑,這里提到了一個(gè)新的術(shù)語——執(zhí)行環(huán)境(execution context),它定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為。 它更偏向于作用域的作用,而不是我們前面討論的上下文(Context)。請(qǐng)務(wù)必仔細(xì)的區(qū)分執(zhí)行環(huán)境和上下文這兩個(gè)概念(注:英文容易造成混淆)。 說實(shí)話,這是個(gè)非常糟糕的命名約定,但是它是ECMAScript規(guī)范制定的,你還是遵守吧。
每個(gè)函數(shù)都有自己的執(zhí)行環(huán)境。當(dāng)執(zhí)行流進(jìn)入一個(gè)函數(shù)時(shí),函數(shù)的環(huán)境就會(huì)被推入一個(gè)環(huán)境棧中(execution stack)。在函數(shù)執(zhí)行完后,棧將其環(huán)境彈出, 把控制權(quán)返回給之前的執(zhí)行環(huán)境。ECMAScript程序中的執(zhí)行流正是由這個(gè)便利的機(jī)制控制著。
執(zhí)行環(huán)境可以分為創(chuàng)建和執(zhí)行兩個(gè)階段。在創(chuàng)建階段,解析器首先會(huì)創(chuàng)建一個(gè)變量對(duì)象(variable object,也稱為活動(dòng)對(duì)象 activation object), 它由定義在執(zhí)行環(huán)境中的變量、函數(shù)聲明、和參數(shù)組成。在這個(gè)階段,作用域鏈會(huì)被初始化,this的值也會(huì)被最終確定。 在執(zhí)行階段,代碼被解釋執(zhí)行。
每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對(duì)象(variable object),環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中。 需要知道,我們無法手動(dòng)訪問這個(gè)對(duì)象,只有解析器才能訪問它。
作用域鏈(The Scope Chain)
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行時(shí),會(huì)創(chuàng)建變量對(duì)象的一個(gè)作用域鏈(scope chain)。作用域鏈的用途是保證對(duì)執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問。 作用域鏈包含了在環(huán)境棧中的每個(gè)執(zhí)行環(huán)境對(duì)應(yīng)的變量對(duì)象。通過作用域鏈,可以決定變量的訪問和標(biāo)識(shí)符的解析。 注意,全局執(zhí)行環(huán)境的變量對(duì)象始終都是作用域鏈的最后一個(gè)對(duì)象。我們來看一個(gè)例子:
var color = "blue"; function changeColor(){ var anotherColor = "red"; function swapColors(){ var tempColor = anotherColor; anotherColor = color; color = tempColor; // 這里可以訪問color, anotherColor, 和 tempColor } // 這里可以訪問color 和 anotherColor,但是不能訪問 tempColor swapColors(); } changeColor(); // 這里只能訪問color console.log("Color is now " + color);
上述代碼一共包括三個(gè)執(zhí)行環(huán)境:全局環(huán)境、changeColor()的局部環(huán)境、swapColors()的局部環(huán)境。 上述程序的作用域鏈如下圖所示:
從上圖發(fā)現(xiàn)。內(nèi)部環(huán)境可以通過作用域鏈訪問所有的外部環(huán)境,但是外部環(huán)境不能訪問內(nèi)部環(huán)境中的任何變量和函數(shù)。 這些環(huán)境之間的聯(lián)系是線性的、有次序的。
對(duì)于標(biāo)識(shí)符解析(變量名或函數(shù)名搜索)是沿著作用域鏈一級(jí)一級(jí)地搜索標(biāo)識(shí)符的過程。搜索過程始終從作用域鏈的前端開始, 然后逐級(jí)地向后(全局執(zhí)行環(huán)境)回溯,直到找到標(biāo)識(shí)符為止。
閉包
閉包是指有權(quán)訪問另一函數(shù)作用域中的變量的函數(shù)。換句話說,在函數(shù)內(nèi)定義一個(gè)嵌套的函數(shù)時(shí),就構(gòu)成了一個(gè)閉包, 它允許嵌套函數(shù)訪問外層函數(shù)的變量。通過返回嵌套函數(shù),允許你維護(hù)對(duì)外部函數(shù)中局部變量、參數(shù)、和內(nèi)函數(shù)聲明的訪問。 這種封裝允許你在外部作用域中隱藏和保護(hù)執(zhí)行環(huán)境,并且暴露公共接口,進(jìn)而通過公共接口執(zhí)行進(jìn)一步的操作。可以看個(gè)簡單的例子:
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è)單例對(duì)象。由于在上面的代碼中我們利用了(function() { ... })();的匿名函數(shù)形式,因此當(dāng)編譯器解析它的時(shí)候會(huì)立即執(zhí)行。 在閉包的執(zhí)行上下文的外部唯一可以訪問的對(duì)象是位于返回對(duì)象中的公共方法和屬性。然而,因?yàn)閳?zhí)行上下文被保存的緣故, 所有的私有屬性和方法將一直存在于應(yīng)用的整個(gè)生命周期,這意味著我們只有通過公共方法才可以與它們交互。
另一種類型的閉包被稱為立即執(zhí)行的函數(shù)表達(dá)式(IIFE)。其實(shí)它很簡單,只不過是一個(gè)在全局環(huán)境中自執(zhí)行的匿名函數(shù)而已:
(function(window){ var foo, bar; function private(){ // do something } window.Module = { public: function(){ // do something } }; })(this);
對(duì)于保護(hù)全局命名空間免受變量污染而言,這種表達(dá)式非常有用,它通過構(gòu)建函數(shù)作用域的形式將變量與全局命名空間隔離, 并通過閉包的形式讓它們存在于整個(gè)運(yùn)行時(shí)(runtime)。在很多的應(yīng)用和框架中,這種封裝源代碼的方式用處非常的流行, 通常都是通過暴露一個(gè)單一的全局接口的方式與外部進(jìn)行交互。
Call和Apply
這兩個(gè)方法內(nèi)建在所有的函數(shù)中(它們是Function對(duì)象的原型方法),允許你在自定義上下文中執(zhí)行函數(shù)。 不同點(diǎn)在于,call函數(shù)需要參數(shù)列表,而apply函數(shù)需要你提供一個(gè)參數(shù)數(shù)組。如下:
var o = {}; function f(a, b) { return a + b; } // 將函數(shù)f作為o的方法,實(shí)際上就是重新設(shè)置函數(shù)f的上下文 f.call(o, 1, 2); // 3 f.apply(o, [1, 2]); // 3
兩個(gè)結(jié)果是相同的,函數(shù)f在對(duì)象o的上下文中被調(diào)用,并提供了兩個(gè)相同的參數(shù)1和2。
在ES5中引入了Function.prototype.bind方法,用于控制函數(shù)的執(zhí)行上下文,它會(huì)返回一個(gè)新的函數(shù), 并且這個(gè)新函數(shù)會(huì)被永久的綁定到bind方法的第一個(gè)參數(shù)所指定的對(duì)象上,無論該函數(shù)被如何使用。 它通過閉包將函數(shù)引導(dǎo)到正確的上下文中。對(duì)于低版本瀏覽器,我們可以簡單的對(duì)它進(jìn)行實(shí)現(xiàn)如下(polyfill):
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)); } } }
bind()方法通常被用在上下文丟失的場(chǎng)景下,例如面向?qū)ο蠛褪录幚怼V砸@么做, 是因?yàn)楣?jié)點(diǎn)的addEventListener方法總是為事件處理器所綁定的節(jié)點(diǎn)的上下文中執(zhí)行回調(diào)函數(shù), 這就是它應(yīng)該表現(xiàn)的那樣。但是,如果你想要使用高級(jí)的面向?qū)ο蠹夹g(shù),或需要你的回調(diào)函數(shù)成為某個(gè)方法的實(shí)例, 你將需要手動(dòng)調(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方法的源代碼,你可能會(huì)注意到有兩次調(diào)用涉及到了Array的slice方法:
Array.prototype.slice.call(arguments, 1); [].slice.call(arguments);
我們知道,arguments對(duì)象并不是一個(gè)真正的數(shù)組,而是一個(gè)類數(shù)組對(duì)象,雖然具有l(wèi)ength屬性,并且值也能夠被索引, 但是它們不支持原生的數(shù)組方法,例如slice和push。但是,由于它們具有和數(shù)組類似的行為,數(shù)組的方法能夠被調(diào)用和劫持, 因此我們可以通過類似于上面代碼的方式達(dá)到這個(gè)目的,其核心是利用call方法。
這種調(diào)用其他對(duì)象方法的技術(shù)也可以被應(yīng)用到面向?qū)ο笾校覀兛梢栽贘avaScript中模擬經(jīng)典的繼承方式:
MyClass.prototype.init = function(){ // call the superclass init method in the context of the "MyClass" instance MySuperClass.prototype.init.apply(this, arguments); }
也就是利用call或apply在子類(MyClass)的實(shí)例中調(diào)用超類(MySuperClass)的方法。
ES6中的箭頭函數(shù)
ES6中的箭頭函數(shù)可以作為Function.prototype.bind()的替代品。和普通函數(shù)不同,箭頭函數(shù)沒有它自己的this值, 它的this值繼承自外圍作用域。
對(duì)于普通函數(shù)而言,它總會(huì)自動(dòng)接收一個(gè)this值,this的指向取決于它調(diào)用的方式。我們來看一個(gè)例子:
var obj = { // ... addAll: function (pieces) { var self = this; _.each(pieces, function (piece) { self.add(piece); }); }, // ... }
在上面的例子中,最直接的想法是直接使用this.add(piece),但不幸的是,在JavaScript中你不能這么做, 因?yàn)閑ach的回調(diào)函數(shù)并未從外層繼承this值。在該回調(diào)函數(shù)中,this的值為window或undefined, 因此,我們使用臨時(shí)變量self來將外部的this值導(dǎo)入內(nèi)部。我們還有兩種方法解決這個(gè)問題:
使用ES5中的bind()方法
var obj = { // ... addAll: function (pieces) { _.each(pieces, function (piece) { this.add(piece); }.bind(this)); }, // ... }
使用ES6中的箭頭函數(shù)
var obj = { // ... addAll: function (pieces) { _.each(pieces, piece => this.add(piece)); }, // ... }
在ES6版本中,addAll方法從它的調(diào)用者處獲得了this值,內(nèi)部函數(shù)是一個(gè)箭頭函數(shù),所以它集成了外部作用域的this值。
注意:對(duì)回調(diào)函數(shù)而言,在瀏覽器中,回調(diào)函數(shù)中的this為window或undefined(嚴(yán)格模式),而在Node.js中, 回調(diào)函數(shù)的this為global。實(shí)例代碼如下:
function hello(a, callback) { callback(a); } hello('weiwei', function(a) { console.log(this === global); // true console.log(a); // weiwei });
小結(jié)
在你學(xué)習(xí)高級(jí)的設(shè)計(jì)模式之前,理解這些概念非常的重要,因?yàn)樽饔糜蚝蜕舷挛脑诂F(xiàn)代JavaScript中扮演著的最基本的角色。 無論我們談?wù)摰氖情]包、面向?qū)ο?、繼承、或者是各種原生實(shí)現(xiàn),上下文和作用域都在其中扮演著至關(guān)重要的角色。 如果你的目標(biāo)是精通JavaScript語言,并且深入的理解它的各個(gè)組成,那么作用域和上下文便是你的起點(diǎn)。
以上內(nèi)容是小編給大家介紹的JavaScript中的作用域和上下文,希望對(duì)大家有所幫助 !
相關(guān)文章
JavaScript中的this例題實(shí)戰(zhàn)總結(jié)詳析
使用JavaScript開發(fā)的時(shí)候,很多人多多少少都會(huì)被this的指向問題搞蒙圈,下面這篇文章主要給大家介紹了關(guān)于JavaScript中this例題實(shí)戰(zhàn)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06js+css實(shí)現(xiàn)select的美化效果
這篇文章主要為大家詳細(xì)介紹了js+css實(shí)現(xiàn)select的美化效果,如何針對(duì)select進(jìn)行美化,感興趣的小伙伴們可以參考一下2016-03-03JavaScript實(shí)現(xiàn)九宮格抽獎(jiǎng)
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)九宮格抽獎(jiǎng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06JavaScript在網(wǎng)頁中畫圓的函數(shù)arc使用方法
這篇文章主要介紹了JavaScript在網(wǎng)頁中畫圓的函數(shù)arc使用方法的相關(guān)資料,需要的朋友可以參考下2015-11-11javascript?實(shí)現(xiàn)純前端將數(shù)據(jù)導(dǎo)出excel兩種方式
這篇文章主要介紹了javascript?實(shí)現(xiàn)純前端將數(shù)據(jù)導(dǎo)出excel兩種方式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參一下2022-07-07JS實(shí)現(xiàn)仿QQ效果的三級(jí)豎向菜單
這篇文章主要介紹了JS實(shí)現(xiàn)仿QQ效果的三級(jí)豎向菜單,以實(shí)例形式分析了JavaScript遍歷頁面元素及動(dòng)態(tài)改變頁面css樣式的技巧,需要的朋友可以參考下2015-09-09BootStrap 實(shí)現(xiàn)各種樣式的進(jìn)度條效果
這篇文章主要介紹了BootStrap 實(shí)現(xiàn)各種樣式的進(jìn)度條效果,代碼分為動(dòng)態(tài)和疊加兩種效果,需要的朋友可以參考下2016-12-12js報(bào)錯(cuò) Object doesn''t support this property or method的原因分析
運(yùn)行js是出現(xiàn)Object doesn't support this property or method 錯(cuò)誤的可能原因分析。2011-03-03js 禁止選擇功能實(shí)現(xiàn)代碼(兼容IE/Firefox)
有時(shí)候出于某種需要,不希望用戶可以選擇某個(gè)區(qū)域,進(jìn)行下面的操作,這里給出簡單的代碼。2010-04-04詳解微信小程序「渲染層網(wǎng)絡(luò)層錯(cuò)誤」的解決方法
這篇文章主要介紹了詳解微信小程序「渲染層網(wǎng)絡(luò)層錯(cuò)誤」的解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01