近幾天在編程羣中的聊天,讓我發現了不少人並不清楚什麼是上下文(context)、什麼是做用域(scope),並且糾結在其中。我當初對這兩個概念也只有粗淺的理解,不過我從一開始就不怎麼困惑,由於我清楚本身對這一問題的認識邊界。如今,我對它們的認識也只加深了一點點。不過,羣聊中小夥伴的熱情鼓舞了我——不少最最初學的小夥伴,想到和思考的是不少我從沒考慮過的問題,小夥伴們真是達到了「進一寸有一寸的歡喜」這一境界。見賢思齊,我決定把這一點點進步記錄下來。javascript
不少人弄不清除,緣由固然是既不瞭解上下文,也不瞭解做用域——我是說,幾乎沒有人明白上下文是什麼而不明白做用域是什麼,反之亦然。上下文(context)
和做用域(scope)
都是編譯原理的知識,具體編程語言有具體的實現規則,本文關注 JavaScript 語言的實現。首先須要關注的是,這兩個概念的關係很是密切,因此先了解它們的關係,有助於理解它們究竟是什麼。java
上下文(context)
和做用域(scope)
的關係:git
上下文是一段程序運行所須要的最小數據集合;做用域是當前上下文中,按照具體規則可以訪問到的標識符(變量)的範圍。github
後文是對上下文和做用域更詳細的解釋,知道了上面指出的關係,往下閱讀時就能夠加深對這一關係的理解了。編程
上下文(context)是一段程序運行所須要的最小數據集合。咱們能夠從上下文交換(context switch)來理解上下文,在多進程或多線程環境中,任務切換時首先要中斷當前的任務,將計算資源交給下一個任務。由於稍後還要恢復以前的任務,因此中斷的時候要保存現場,即當前任務的上下文,也能夠叫作環境。即上下文就是恢復現場所需的最小數據集合。容易把人弄暈的一點是,咱們這裏說的上下文、環境有時候也稱做做用域(scope),即這兩個概念有時候是混用的。不過,它們有不一樣的側重點,下一節將會說明。多線程
另外,JavaScript 中常見的情形是一個方法/函數的執行。從一段程序的角度看,這段程序運行所需的全部變量,就是它的上下文。閉包
做用域(scope)是標識符(變量)在程序中的可見性範圍。做用域規則是按照具體規則維護標識符的可見性,以肯定當前執行的代碼對這些標識符的訪問權限。做用域(scope)是在具體的做用域規則之下肯定的。app
前面說過,有時候上下文、環境、做用域是同義詞;不過,上下文(context)指代的是總體環境,做用域關注的是標識符(變量)的可訪問性(可見性)。上下文肯定了,根據具體編程語言的做用域規則,做用域也就肯定了。這就是上下文與做用域的關係。編程語言
寫 JavaScript 代碼時,若是 Function 做爲參數,能夠指定它在具體對象上調用時,這個對象經常叫作 context:函數
function callWithContext(fn, context) {
return fn.call(context);
}
const apple = {
name: "Apple"
};
const orange = {
name: "Orange"
};
function echo() {
console.log(this.name);
}
callWithContext(echo, apple); // Apple
callWithContext(echo, orange); // Orange複製代碼
爲何將這個參數叫作 context?由於它關係到調用環境,指定了它,就指定了函數的調用上下文。再加上具體的做用域規則,做用域也肯定了。
在 JavaScript 中,這個具體的做用域規則就是詞法做用域(lexical scope),也就是 JavaScript 中的做用域鏈的規則。詞法做用域是的變量在編譯時(詞法階段)就是肯定的,因此詞法做用域又叫靜態做用域(static scope),與之相對的是動態做用域(dynamic scope)。
You Don't Know JS: Scope & Closures 用簡單例子解釋過動態做用域,下面用一個相似的例子說明一下:
function foo() {
console.log(a);
}
function bar() {
let a = 3;
foo();
}
let a = 2;
bar(); // 2複製代碼
有必定 JavaScript 編程經驗的人都能看出,這段程序會輸出 2,但若是在動態做用域的規則下,應該輸出 3,即 a 的引用再也不是編譯時肯定,而是調用時肯定的。這有點像 JavaScript 中的 this
,因此 MDN 中,function.bind 的方法簽名中第一個形參名稱用的是 thisArg
這一更科學的名字:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
一樣狀況的還可見於 Lodash 的文檔:
_.bind(func, thisArg, [partials])
上一節中的代碼中,之因此輸出 2,是由於 foo 是一個閉包函數。若是從本文中理解了上下文和做用域的概念,對於閉包是什麼這一問題是否是感到豁然開朗?
前面說過,詞法做用域也叫靜態做用域,變量在詞法階段肯定,也就是定義時肯定。雖然在 bar 內調用,但因爲 foo 是閉包函數,即便它在本身定義的詞法做用域之外的地方執行,它也一直保持着本身的做用域。所謂閉包函數,即這個函數封閉了它本身的定義時的環境,造成了一個閉包,因此 foo 並不會從 bar 中尋找變量,這就是靜態做用域的特色。
一個更加典型的例子是:
function fn() {
let a = 0;
function func() {
console.log(a);
}
return func;
}
let a = 1;
let sub = fn();
sub(); // 0;複製代碼
sub 就是 func 這一返回值,func 定義在 fn 內部而且被傳遞出來了,因此 fn 執行以後垃圾回收器依然沒有回收它的內部做用域,由於 func/sub 在使用。sub 依然持有 func 定義時的做用域的引用,而這個引用就叫做閉包。調用 sub 時,它能夠訪問 func 定義時的詞法做用域,所以找到的 a 是 fn 內部的變量 a,它的值是 0。
You Don't Know JS: Scope & Closures
Context (computing)
Scope (computer science)
Function.prototype.bind()
Function _.bind()