DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 深入淺析JavaScript中的作用域和上下文
深入淺析JavaScript中的作用域和上下文
編輯:關於JavaScript     

javascript中的作用域(scope)和上下文(context)是這門語言的獨到之處,這部分歸功於他們帶來的靈活性。每個函數有不同的變量上下文和作用域。這些概念是javascript中一些強大的設計模式的後盾。然而這也給開發人員帶來很大困惑。下面全面揭示了javascript中的上下文和作用域的不同,以及各種設計模式如何使用他們。

上下文(Context)和作用域(Scope)

首先需要知道的是,上下文和作用域是兩個完全不同的概念。多年來,我發現很多開發者會混淆這兩個概念(包括我自己), 錯誤的將兩個概念混淆了。平心而論,這些年來很多術語都被混亂的使用了。

函數的每次調用都有與之緊密相關的作用域和上下文。從根本上來說,作用域是基於函數的,而上下文是基於對象的。 換句話說,作用域涉及到所被調用函數中的變量訪問,並且不同的調用場景是不一樣的。上下文始終是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() // foo
foo() // window

當調用一個為綁定函數時,this默認情況下是全局上下文,在浏覽器中它指向window對象。需要注意的是,ES5引入了嚴格模式的概念, 如果啟用了嚴格模式,此時上下文默認為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();
// 這裡只能訪問color
console.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); // 3
f.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(嚴格模式),而在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中的作用域和上下文,希望對大家有所幫助 !

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved