引言

基於比較早的基礎文章,鏈接如 link,現在可能有些方法已經變的不常用了

javascript 的 scope 和 context 是這門語言獨特的 feature,某種程度上來說是因為它非常的靈活。函數能夠通過各種各樣的 context 和 scope 去實現封裝和保存。javascript 就是用這些概念去實現各種各樣強大的設計模式的。然而,另一方面,這個優點也是引起很多開發者迷惑的根源。下面就是介紹 scope 和 context 的概念,比較它們的不同,以及如何通過它們去實現豐富的設計模式。

Context(上下文) 和 Scope(域)

首先我們需要知道 context 和 scope 是不同的。雖然很多開發者(包括我自己)經常把這兩者混淆,常常把一個說成另一個。準確的說,過了很多年,這兩個概念依舊很混亂。

每個函數調用都有一個域和一個 context 跟它相關。從根本上說,scope 是基於函數的,context 是基於對象的。換句話說,scope 屬於函數被調用時候的變數許可權,並且每次調用這個許可權都是唯一的。context 是這個 this 關鍵字的值,總是指向當前這段執行代碼本身。

變數域

變數定義在當前或者全局的域(scope),定義在不同的域決定了運行時變數不同的訪問許可權。全局定義的變數,意味著整個生命週期都存在,並且在任何域內都可以選擇是否去訪問。

但是本地變數,只在函數定義的內部存儲,並且每次調用的時候都有不同的訪問域(scope)。主要是用在賦值,減少,並且只在調用的內部維護,在外部不能被訪問。

ECMASCript 6 介紹了 let 和const 關鍵字,支持塊的作用域和當前作用域。比如 if 和 for 循環,用 {} 包括的一個塊的作用域。這個跟 var 是相反的, var 是在 塊的外部定義,但是在塊內部可以被訪問。let 和 const 的不同在於,const 定義的變數只有一個引用,並且是不能被重新分配的。

「this「 的 context 是什麼?

context 常常由一個函數是如何被調用來決定的。當一個函數被作為一個對象的方法被調用的時候 ,this 指向這個被調用的對象。。

var obj = {
foo: function() {
return this;
}
};

obj.foo() === obj;

當用一個 new 的操作去創建函數的實例,這個道理同樣適用,this 也是指向當前這個方法實例的。 按照這種方式,函數域內容的 this 指向最新的實例。

function foo() {
alert(this);
}

foo() // window
new foo() // f

當調用一個沒有綁定到任何對象上的函數 ,this 默認指向全家的內容或者瀏覽器上的window 對象 。然而如果這個函數如果在嚴格模式下執行,那麼 context 默認會是 undefined

執行上下文(context)

JavaScript 是一個單線程的語言,意味著一次只能執行一次任務。當 javascript 最初執行代碼的時候,默認會進入一個全局的執行 context。每次通過 this 調用一個函數,會創建一個新的執行上下文。

這個地方比較容易引起困惑,「執行上下文」實際上指向更多的域,有跟多的目的和意圖,並不是我們前面討論的 context,這是一個不幸的命名規範, 然而這是 ECMAScript 規範定義的術語,我們不得不接受。

每次一個新的執行內容被創建,它就被加到執行堆棧的頂部。瀏覽器總是執行當前最頂部的執行內容。 一旦完成,它就會被從stack 的頂部移出去,控制權就會被返回執行內容的下一個。

執行一次上下文,分成創建和執行階段。在創建階段,翻譯器會創建一個變數對象,也被叫做激活對象,函數和參數在執行內容裡面被定義。接下來,會初始化作用域鏈,this 的值被最終決定。接著在執行階段,代碼被解析和執行。

域鏈

每次執行上下文,都有一個域鏈。這個域鏈包含了執行堆棧裡面的滅國執行上下文的變數對象。用來決定最後變數的訪問許可權和執行方案。如下例子:

function first() {
second();
function second() {
third();
function third() {
fourth();
function fourth() {
// do something
}
}
}
}
first();

在嵌套函數裡面,前面的代碼被執行,一直到 fourth 函數。在這個域鏈裡面,是從頂部到底部依次是 forth, third, sercond,first, golbal. fourth 函數有全局變數的訪問許可權 和 定義在 first, second, third 函數裡面的的變數,以及函數本身的訪問許可權。

在不同的執行上下文有變數衝突的時候,按照作用域鏈依次往上找。從本地變數,到全局變數。意味著本地變數和外層的作用域裡面的變數如果同名,會優先讀本地變數。

閉包

函數a 的內部函數被函數 b 外部的一個變數引用的時候,就創建了一個閉包。一個簡單的例子

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

作為一個單例模式,編譯器解析的時候,它就會執行。因此在函數的末尾有()括弧。所以的屬性和方法會在執行上下文被保留的一整個生命週期。意味著變數主要是通過公有方法去做進一步的交互。

另一個閉包的

(function(window) {

var foo, bar;

function private() {
// do something
}

window.Module = {

public: function() {
// do something
}
};

})(this);

call 和 apply

這個兩個方式所有函數都有,能讓你靈活的設置你想要的任何 context。 apply 要求把參數作為數組,拿到合適的context。

function user(firstName, lastName, age) {
// do something
}

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

一個polyfill是一段代碼(或者插件),提供了那些開發者們希望瀏覽器原生提供支持的功能。下面是一個適配其他瀏覽器 polyfill 的代碼:

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

通過擴展 bind,能讓一些組件的操作事件的使用變的簡單

function Widget() {
this.element = document.createElement(div);
this.element.addEventListener(click, this.onClick.bind(this), false);
}

Widget.prototype.onClick = function(e) {
// do something
};

通過看bind 的代碼,你會注意到數組的 slice 方法

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

這裡需要提到的是 arguments 對象並不少一個數組。它描繪了一個節點列表(通過 element.childNodes 返回的任何內容)。這個實現方式也可以應用到面向對象,以下是一個模擬繼承的寫法:

SubClass.prototype.init = function(){
// call the superclass init method in the context of the "SubClass" instance
SuperClass.prototype.init.apply(this, arguments);
}

通過這個 SuperClass 的方法, 子類執行 init 的時候,覆蓋父類的 init,並且調用父類的 init。

總結

scope 和 context 是作為 javascript 裡面一個很重要的角色。 我們談論閉包,對象和繼承,或者各種各樣的實現, context 和 scope 扮演了重要的角色。 如果你要掌握 javascript 這門語言,或者更好的理解它,scope和 context 是你必須要了解的。

推薦閱讀:

相關文章