標籤:listener pattern 事件處理 java語言 處理器 搜尋 prot bar 物件導向
JavaScript對於範圍(Scope)和上下文(Context)的實現是這門語言的一個非常獨到的地方,部分歸功於其獨特的靈活性。 函數可以接收不同的的上下文和範圍。這些概念為JavaScript中的很多強大的設計模式提供了堅實的基礎。 然而這也概念也非常容易給開發人員帶來困惑。為此,本文將全面的剖析這些概念,並闡述不同的設計模式是如何利用它們的。
範圍(Scope)和上下文(Context)
首先需要知道的是,上下文和範圍是兩個完全不同的概念。多年來,我發現很多開發人員會混淆這兩個概念(包括我自己), 錯誤的將兩個概念混淆了。平心而論,這些年來很多術語都被混亂的使用了。
函數的每次調用都有與之緊密相關的範圍和上下文。從根本上來說,範圍是基於函數的,而上下文是基於對象的。 換句話說,範圍涉及到所被調用函數中的變數訪問,並且不同的調用情境是不一樣的。上下文始終是this
關鍵字的值, 它是擁有(控制)當前所執行代碼的對象的引用。
變數範圍
一個變數可以被定義在局部或者全域範圍中,這建立了在運行時(runtime)期間變數的訪問性的不同範圍範圍。 任何被定義的全域變數,意味著它需要在函數體的外部被聲明,並且存活於整個運行時(runtime),並且在任何範圍中都可以被訪問到。 在ES6之前,局部變數只能存在於函數體中,並且函數的每次調用它們都擁有不同的範圍範圍。 局部變數只能在其被調用期的範圍範圍內被賦值、檢索、操縱。
需要注意,在ES6之前,JavaScript不支援塊級範圍,這意味著在if
語句、switch
語句、for
迴圈、while
迴圈中無法支援塊級範圍。 也就是說,ES6之前的JavaScript並不能構建類似於Java中的那樣的塊級範圍(變數不能在語句塊外被訪問到)。但是, 從ES6開始,你可以通過let
關鍵字來定義變數,它修正了var
關鍵字的缺點,能夠讓你像Java語言那樣定義變數,並且支援塊級範圍。看兩個例子:
ES6之前,我們使用var
關鍵字定義變數:
function func() { if (true) { var tmp = 123; } console.log(tmp); // 123}
之所以能夠訪問,是因為var
關鍵字聲明的變數有一個變數提升的過程。而在ES6情境,推薦使用let
關鍵字定義變數:
function func() { if (true) { let tmp = 123; } console.log(tmp); // ReferenceError: tmp is not defined}
這種方式,能夠避免很多錯誤。
什麼是
this
上下文
上下文通常取決於函數是如何被調用的。當一個函數被作為對象中的一個方法被調用的時候,this
被設定為調用該方法的對象上:
var obj = { foo: function(){ alert(this === obj); }};obj.foo(); // true
這個準則也適用於當調用函數時使用new
操作符來建立對象的執行個體的情況下。在這種情況下,在函數的範圍內部this
的值被設定為新建立的執行個體:
function foo(){ alert(this);}new foo() // foofoo() // window
當調用一個為綁定函數時,this
預設情況下是全域上下文,在瀏覽器中它指向window
對象。需要注意的是,ES5引入了strict 模式的概念, 如果啟用了strict 模式,此時上下文預設為undefined
。
執行環境(execution context)
JavaScript是一個單線程語言,意味著同一時間只能執行一個任務。當JavaScript解譯器初始化執行代碼時, 它首先預設進入全域執行環境(execution context),從此刻開始,函數的每次調用都會建立一個新的執行環境。
這裡會經常引起新手的困惑,這裡提到了一個新的術語——執行環境(execution context),它定義了變數或函數有權訪問的其他資料,決定了它們各自的行為。 它更偏向於範圍的作用,而不是我們前面討論的上下文(Context)。請務必仔細的區分執行環境和上下文這兩個概念(註:英文容易造成混淆)。 說實話,這是個非常糟糕的命名規範,但是它是ECMAScript規範制定的,你還是遵守吧。
每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中(execution stack)。在函數執行完後,棧將其環境彈出, 把控制權返回給之前的執行環境。ECMAScript程式中的執行流正是由這個便利的機制控制著。
執行環境可以分為建立和執行兩個階段。在建立階段,解析器首先會建立一個變數對象(variable object,也稱為使用中的物件 activation object), 它由定義在執行環境中的變數、函式宣告、和參數組成。在這個階段,範圍鏈會被初始化,this
的值也會被最終確定。 在執行階段,代碼被解釋執行。
每個執行環境都有一個與之關聯的變數對象(variable object),環境中定義的所有變數和函數都儲存在這個對象中。 需要知道,我們無法手動訪問這個對象,只有解析器才能訪問它。
範圍鏈(The Scope Chain)
當代碼在一個環境中執行時,會建立變數對象的一個範圍鏈(scope chain)。範圍鏈的用途是保證對執行環境有權訪問的所有變數和函數的有序訪問。 範圍鏈包含了在環境棧中的每個執行環境對應的變數對象。通過範圍鏈,可以決定變數的訪問和標識符的解析。 注意,全域執行環境的變數對象始終都是範圍鏈的最後一個對象。我們來看一個例子:
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();// 這裡只能訪問colorconsole.log("Color is now " + color);
上述代碼一共包括三個執行環境:全域環境、changeColor()的局部環境、swapColors()的局部環境。 上述程式的範圍鏈如所示:
從發現。內部環境可以通過範圍鏈訪問所有的外部環境,但是外部環境不能訪問內部環境中的任何變數和函數。 這些環境之間的聯絡是線性、有次序的。
對於標識符解析(變數名或函數名搜尋)是沿著範圍鏈一級一級地搜尋標識符的過程。搜尋過程始終從範圍鏈的前端開始, 然後逐級地向後(全域執行環境)回溯,直到找到標識符為止。
閉包
閉包是指有權訪問另一函數範圍中的變數的函數。換句話說,在函數內定義一個嵌套的函數時,就構成了一個閉包, 它允許嵌套函數訪問外層函數的變數。通過返回嵌套函數,允許你維護對外部函數中局部變數、參數、和內函式宣告的訪問。 這種封裝允許你在外部範圍中隱藏和保護執行環境,並且暴露公用介面,進而通過公用介面執行進一步的操作。可以看個簡單的例子:
function foo(){ var localVariable = ‘private variable‘; return function bar(){ return localVariable; }}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){ return privateMethod(args); } };})();
模組類似於一個單例對象。由於在上面的代碼中我們利用了(function() { ... })();
的匿名函數形式,因此當編譯器解析它的時候會立即執行。 在閉包的執行內容的外部唯一可以訪問的對象是位於返回對象中的公用方法和屬性。然而,因為執行內容被儲存的緣故, 所有的私人屬性和方法將一直存在於應用的整個生命週期,這意味著我們只有通過公用方法才可以與它們互動。
另一種類型的閉包被稱為立即執行的函數運算式(IIFE)。其實它很簡單,只不過是一個在全域環境中自執行的匿名函數而已:
(function(window){ var foo, bar; function private(){ // do something } window.Module = { public: function(){ // do something } };})(this);
對於保護全域命名空間免受變數汙染而言,這種運算式非常有用,它通過構建函數範圍的形式將變數與全域命名空間隔離, 並通過閉包的形式讓它們存在於整個運行時(runtime)。在很多的應用和架構中,這種封裝原始碼的方式用處非常的流行, 通常都是通過暴露一個單一的全域介面的方式與外部進行互動。
Call和Apply
這兩個方法內建在所有的函數中(它們是Function
對象的原型方法),允許你在自訂上下文中執行函數。 不同點在於,call
函數需要參數列表,而apply
函數需要你提供一個參數數組。如下:
var o = {};function f(a, b) { return a + b;}// 將函數f作為o的方法,實際上就是重新設定函數f的上下文f.call(o, 1, 2); // 3f.apply(o, [1, 2]); // 3
兩個結果是相同的,函數f
在對象o
的上下文中被調用,並提供了兩個相同的參數1
和2
。
在ES5中引入了Function.prototype.bind
方法,用於控制函數的執行內容,它會返回一個新的函數, 並且這個新函數會被永久的綁定到bind
方法的第一個參數所指定的對象上,無論該函數被如何使用。 它通過閉包將函數引導到正確的上下文中。對於低版本瀏覽器,我們可以簡單的對它進行實現如下(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()
方法通常被用在上下文丟失的情境下,例如物件導向和事件處理。之所以要這麼做, 是因為節點的addEventListener
方法總是為事件處理器所綁定的節點的上下文中執行回呼函數, 這就是它應該表現的那樣。但是,如果你想要使用進階的物件導向技術,或需要你的回呼函數成為某個方法的執行個體, 你將需要手動調整上下文。這就是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
方法的原始碼,你可能會注意到有兩次調用涉及到了Array
的slice
方法:
Array.prototype.slice.call(arguments, 1);[].slice.call(arguments);
我們知道,arguments
對象並不是一個真正的數組,而是一個類數組對象,雖然具有length屬性,並且值也能夠被索引, 但是它們不支援原生的數組方法,例如slice
和push
。但是,由於它們具有和數組類似的行為,數組的方法能夠被調用和劫持, 因此我們可以通過類似於上面代碼的方式達到這個目的,其核心是利用call
方法。
這種調用其他對象方法的技術也可以被應用到物件導向中,我們可以在JavaScript中類比經典的繼承方式:
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
)的執行個體中調用超類(MySuperClass
)的方法。
ES6中的箭頭函數
ES6中的箭頭函數可以作為Function.prototype.bind()
的替代品。和普通函數不同,箭頭函數沒有它自己的this
值, 它的this
值繼承自外圍範圍。
對於普通函數而言,它總會自動接收一個this
值,this
的指向取決於它調用的方式。我們來看一個例子:
var obj = { // ... addAll: function (pieces) { var self = this; _.each(pieces, function (piece) { self.add(piece); }); }, // ...}
在上面的例子中,最直接的想法是直接使用this.add(piece)
,但不幸的是,在JavaScript中你不能這麼做, 因為each
的回呼函數並未從外層繼承this
值。在該回呼函數中,this
的值為window
或undefined
, 因此,我們使用臨時變數self
來將外部的this
值匯入內部。我們還有兩種方法解決這個問題:
使用ES5中的bind()方法
var obj = { // ... addAll: function (pieces) { _.each(pieces, function (piece) { this.add(piece); }.bind(this)); }, // ...}
使用ES6中的箭頭函數
var obj = { // ... addAll: function (pieces) { _.each(pieces, piece => this.add(piece)); }, // ...}
在ES6版本中,addAll
方法從它的調用者處獲得了this
值,內建函式是一個箭頭函數,所以它整合了外部範圍的this
值。
注意:對回呼函數而言,在瀏覽器中,回呼函數中的this
為window
或undefined
(strict 模式),而在Node.js中, 回呼函數的this
為global
。執行個體代碼如下:
function hello(a, callback) { callback(a);}hello(‘weiwei‘, function(a) { console.log(this === global); // true console.log(a); // weiwei});
小結
在你學習進階的設計模式之前,理解這些概念非常的重要,因為範圍和上下文在現代JavaScript中扮演著的最基本的角色。 無論我們談論的是閉包、物件導向、繼承、或者是各種原生實現,上下文和範圍都在其中扮演著至關重要的角色。 如果你的目標是精通JavaScript語言,並且深入的理解它的各個組成,那麼範圍和上下文便是你的起點。
JavaScript的範圍(Scope)和上下文(Context)