從上下文,到作用域(彩蛋:理解閉包)
前言
近幾天在程式設計群中的聊天,讓我發現了很多人並不清楚什麼是上下文(context)、什麼是作用域(scope),而且糾結在其中。我當初對這兩個概念也只有粗淺的理解,不過我從一開始就不怎麼困惑,因為我清楚自己對這一問題的認識邊界。現在,我對它們的認識也只加深了一點點。不過,群聊中小夥伴的熱情鼓舞了我——很多最最初學的小夥伴,想到和思考的是很多我從沒考慮過的問題,小夥伴們真是達到了“進一寸有一寸的歡喜”這一境界。見賢思齊,我決定把這一點點進步記錄下來。
上下文與作用域的關係
很多人弄不清除,原因當然是既不瞭解上下文,也不瞭解作用域——我是說,幾乎沒有人明白上下文是什麼而不明白作用域是什麼,反之亦然。上下文(context)
和作用域(scope)
都是編譯原理的知識,具體程式語言有具體的實現規則,本文關注 JavaScript 語言的實現。首先需要關注的是,這兩個概念的關係非常密切,所以先了解它們的關係,有助於理解它們到底是什麼。
上下文(context)
和作用域(scope)
的關係:
上下文是一段程式執行所需要的最小資料集合;作用域是當前上下文中,按照具體規則能夠訪問到的識別符號(變數)的範圍。
後文是對上下文和作用域更詳細的解釋,知道了上面指出的關係,往下閱讀時就可以加深對這一關係的理解了。
上下文
上下文(context)是一段程式執行所需要的最小資料集合。我們可以從上下文交換(context switch)來理解上下文,在多程式或多執行緒環境中,任務切換時首先要中斷當前的任務,將計算資源交給下一個任務。因為稍後還要恢復之前的任務,所以中斷的時候要儲存現場,即當前任務的上下文,也可以叫做環境。即上下文就是恢復現場所需的最小資料集合。容易把人弄暈的一點是,我們這裡說的上下文、環境有時候也稱作作用域(scope),即這兩個概念有時候是混用的。不過,它們有不同的側重點,下一節將會說明。
另外,JavaScript 中常見的情形是一個方法/函式的執行。從一段程式的角度看,這段程式執行所需的所有變數,就是它的上下文。
作用域
作用域(scope)是識別符號(變數)在程式中的可見性範圍。作用域規則是按照具體規則維護識別符號的可見性,以確定當前執行的程式碼對這些識別符號的訪問許可權。作用域(scope)是在具體的作用域規則之下確定的。
前面說過,有時候上下文、環境、作用域是同義詞;不過,上下文(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()
相關文章
- JavaScript從作用域到閉包JavaScript
- 深入理解執行上下文、作用域鏈和閉包
- 深入理解javascript原型和閉包(14)——從【自由變數】到【作用域鏈】JavaScript原型變數
- 從【預編譯】到【宣告提升】到【作用域鏈】再到【閉包】編譯
- 深入理解javascript原型和閉包(13)-【作用域】和【上下文環境】JavaScript原型
- 從這兩道題重新理解,JS的this、作用域、閉包、物件JS物件
- 【機制】js的閉包、執行上下文、作用域鏈JS
- 深入理解閉包之前置知識→作用域與詞法作用域
- 原型、原型鏈、作用域、作用域鏈、閉包原型
- JS作用域與閉包JS
- JS閉包作用域解析JS
- Javascript-this/作用域/閉包JavaScript
- 深入理解javascript原型和閉包(18)——補充:上下文環境和作用域的關係JavaScript原型
- JavaScript之作用域和閉包JavaScript
- 圖解作用域及閉包圖解
- 從 JS 編譯原理到作用域(鏈)及閉包JS編譯原理
- 深入理解javascript原型和閉包(12)——簡介【作用域】JavaScript原型
- 理解JavaScript中的作用域和上下文JavaScript
- 【JS基礎】作用域和閉包JS
- 淺談JS作用域、this及閉包JS
- javascript 基礎(作用域和閉包)JavaScript
- Javascript深入之作用域與閉包JavaScript
- 變數的作用域--js閉包變數JS
- JS作用域與閉包--例項JS
- 理解 JS 作用域鏈與執行上下文JS
- 深入理解 RxJava2:從 observeOn 到作用域(4)RxJava
- JavaScript物件導向~ 作用域和閉包JavaScript物件
- 從這兩套題,重新認識JS的this、作用域、閉包、物件JS物件
- 原型模式故事鏈(5)--JS變數作用域、作用域鏈、閉包原型模式JS變數
- JS 事件迴圈,閉包,作用域鏈題JS事件
- 《JavaScript 闖關記》之作用域和閉包JavaScript
- 迴圈輸出——閉包、變數作用域變數
- 從執行上下文(ES3,ES5)的角度來理解"閉包"S3
- JS基礎總結(3)——作用域和閉包JS
- 面試-JS基礎知識-作用域和閉包、this面試JS
- 【譯】終極指南:變數提升、作用域和閉包變數
- Python和Lua的預設作用域以及閉包Python
- 理解“閉包”