1. 程式人生 > >理解javaScript中的作用域和上下文Understanding Scope and Context in JavaScript

理解javaScript中的作用域和上下文Understanding Scope and Context in JavaScript

譯者注:一直對於作用域和上下文感到很混亂,無意中看到這篇文章,覺得講得很好,故翻譯來與大家分享。翻譯不好之處,請大家多多指教。

原文連結:http://ryanmorr.com/understanding-scope-and-context-in-javascript/

 

前言部分,不做翻譯。

Context vs. Scope

    首先我們要明確,上下文和作用域是不同的概念。我注意到許多的開發者(包括我)長久以來都對這二者感到迷惑,錯誤地將其中一方描述為另一方。公平地說,這些術語已經被混淆很多年了。

    每一次函式呼叫都有一個作用域以及與之對應的上下文。

從根本上講,作用域是基於函式的而上下文是基於物件的。即,作用域是關於函式被呼叫時對變數的訪問。並且,每一次呼叫,作用域都是不同的。上下文是關鍵字 this 的值,即指向“擁有”當前執行程式碼的物件。

 

Variable Scope

一個變數可以被定義為區域性變數或者全域性變數,它確立了在執行的時候在不同的作用域中,是否可以訪問到它。任何被定義了的全域性變數,即那些在函式體外定義的變數,會在整個執行週期中存在,並且在每個作用域中訪問和修改。區域性變數只存在於定義它的函式中,每一次呼叫的時候都會有不同的作用域。它被在函式中用於賦值,檢索,操作並且無法在該作用域外面被接觸到。

ECMAScript6(ES6/ES2015)引入了關鍵字 let 和 const,它們支援塊級作用域。這意味著變數可以被限制在定義它的塊級作用域中。let和const的區別是,const,從它的名字可以看出,它是一個只讀的值。但這不意味著它的值是不變的只是它的變數不可以重新被定義。(譯者注:比如一個引用型別的值,如物件,我們不可以改變它指向的物件,但是可以改變它的物件的屬性值)

 

What is “this” Context

    上下文是由函式如何被呼叫決定的。當一個函式被作為物件的方法呼叫時,this指向呼叫這個方法的物件:

var obj = {

foo: function() {

return this;

}};

 

obj.foo() === obj; // true

 

    同樣的準則也適用於當使用 new操作符來建立一個物件例項的時候。使用這個方法呼叫時,函式作用域中的this會指向新建立的例項。

function foo() {

alert(
this);} foo() // windownew foo() // foo

 

    當一個未被繫結的函式被呼叫時,this 預設指向全域性上下文,在瀏覽器中指向window物件。當韓式在嚴格模式下執行時,此時的上下文是undefined。

 

Execution Context
    javaScript是一門單執行緒語言,意味著每一次它只能執行一個任務。當javascript開始解釋程式碼時,它會預設進入全域性execution context。從此刻開始,每一次的函式呼叫都會建立一個新的執行上下文。

    然而,這就是迷惑產生的地方。“exection context”更多的是涉及作用域,而不是我們之前提到的上下文。這是一個令人遺憾的命名規範。然而,它是由ECMAScript 規範決定的,所以我們只能接受了。

    每當一個新的execution context被建立時,它被新增到execution棧的頂端。瀏覽器總是會執行當前的在exection stack頂端的execution context。一旦執行完,它會被從棧的頂端移除,控制權會返回給下面的execution context。

    一個execution context 可以被分成建立和執行兩個時期。在建立時期,直譯器會先建立一個變數物件(variable object)(也叫活動物件)。他由所有的在execution context定義的變數和函式宣告,以及函式引數組成。然後,在活動物件中,作用域鏈被初始化。最後,this確定指向。之後,在執行階段,程式碼被解釋和執行。(譯者注:活動物件無法被我們訪問,但是直譯器在處理資料是會在後臺使用到它.參考《javaScript高階程式設計(第3版)》)

 

The Scope Chain

對於每一個執行上下文,都有一條與之對應的作用域鏈。作用域鏈包括了execution stack中每一個execution context的活動物件,它被用來決定去哪獲取變數以及識別符號的解析。

 

function first() {

second();

function second() {

third();

function third() {

fourth();

function fourth() {

// do something

}

}

}

}

first();

 

執行前面的程式碼回導致後面的程式碼被執行,直到fourth這個函式。此時,作用域鏈的頂端到底部分別是 fourth, third, second, first, global.。fourth這個函式可以獲取全域性變數以及first,second,third函式中定義的任何變數,正如這些函式本身一樣。

    命名衝突時通過從區域性到全域性順著作用域鏈尋找來解決的。這意味著,在作用域鏈更頂端的變數擁有更高的優先順序。

    簡而言之,每次你想要在一個執行上下文中存取變數時,查詢的過程總會從當前的變數物件開始。如果變數物件沒被找到,那麼就會順著作用域鏈一直尋找。它會順著作用域鏈,搜尋每一個活動物件,來匹配當前的變數名稱。

 

Closures

在直接語法作用域之外存取變數稱為閉包。換句話說,一個閉包就是在一個函式中定義另一個函式,並且允許裡面的函式存取外面的函式的值。返回的巢狀在裡面的函式允許你保持存取區域性變數,函式引數,以及外層函式的內層函式的權利。這種封裝允許我們在暴露一個公共介面時隱藏並且保留外層函式的執行上下文,從而允許進一步的封裝。簡單的例子如下。

function foo() {

var localVariable = 'private variable';

return function() {

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);

}

};})();

 

這個模組表現得像單例一般,直譯器開始解釋的時候它就執行了,因為函式後面有圓括號。在閉包的執行上下文外面唯一的變數方法是你的公共方法和返回物件中的那些屬性。然而,所有的私有屬性和方法會在應用的整個生命週期存在因為執行上下文被保留了。意味著變數會通過共有的方法進行更進一步的互動。

另一種閉包型別是立即執行函式表示式,不外乎就是在window上下文中自呼叫的並執行的匿名函式。

這個表示式在想要保留全域性名稱空間中任何在函式中宣告的變數的作用餘限定在閉包中,但是在應用執行的整個生命週期都存在,會非常好用。這是為應用和框架封裝原始碼的非常受歡迎的方法,典型的是暴露一個單獨的全域性介面給互動物件。

 

Call and Apply

所用的函式都有兩種自帶的方法允許你在任何上下文中執行函式。這帶來了難以置信的功能。call函式要求引數是所有列明的引數,而apply允許使用引數陣列。

function user(firstName, lastName, age) {

// do something }

 

user.call(window, 'John', 'Doe', 30);

user.apply(window, ['John', 'Doe', 30]);

 

ES5引入了Function.prototype.bind方法來操作上下文。它返回一個永遠繫結在bind()函式第一個引數上的新函式,忽略函式是如何被使用的。這在一個閉包在適當的上下文中需要重定向是非常有用。

if(!('bind' in Function.prototype)){

Function.prototype.bind = function() {

var fn = this;

var context = arguments[0];

var args = Array.prototype.slice.call(arguments, 1);

return function() {

return fn.apply(context, args.concat([].slice.call(arguments)));

}

}}

 

它被普遍地使用在上下文缺失的情況下,面向物件設計和事件處理中。這很重要,因為結點的addEventListener方法,因為它的回掉函式必須在它繫結的結點的上下文中執行。然而,如果你使用先進的面向物件技術並且要求你的互調函式是一個方法的例項,你必須手動地調整上下文,這就是bind非常便利的地方。

function Widget() {

this.element = document.createElement('div');

this.element.addEventListener('click', this.onClick.bind(this), false);}

 

Widget.prototype.onClick = function(e) {

// do something};

 

從前面的Function.prototype.bind函式的程式碼中我們可以看到,Array的slice方法的兩種呼叫方式。

Array.prototype.slice.call(arguments, 1);[].slice.call(arguments);

 

有趣的是,arguments物件並不是嚴格意義上的陣列,就像nodelist一樣,它被描述為類陣列的東西。它包括了length屬性和索引值,但它不是陣列,並且後來也不支援array的自帶的方法,如slice和push.但是,由於他們相似的行為,如果你願意的話,array的方法可以被運用或者強制轉換,並且在類似陣列上下文的中執行,就想上面提到的那樣。

    採用另一種物件方法的技術也可以運用到面向物件中,當我們需要在javaScript中效仿經典的的繼承。

SubClass.prototype.init = function(){

// call the superclass init method in the context of the "SubClass" instance

SuperClass.prototype.init.apply(this, arguments);}

 

 

Conclusion

在開始學習先進的設計模式之前理解這些概念是很重要的,因為context和scope在現代的JavaScript中佔據了重要的地位。每當我們談及閉包,面向物件,繼承,以及各種原生實現時,context和scope扮演著舉足輕重的地位,如果你有志於鑽研javascript語言 ,最好明白它包含了什麼,而context和scope應該是最開始要明確學習的東西。