上下文與做用域之間有什麼樣的關係? 這一律念看似簡單,但不少人都講不清楚之間的關係。上下文和做用域都是編譯原理的知識,具體編程語言有具體的實現規則,本文關注的是 JavaScript 語言的實現。編程
上下文
(context)是一段程序運行所須要的最小數據集合。咱們能夠從上下文交換
(context switch)來理解上下文,在多進程或多線程環境中,任務切換時首先要中斷當前的任務,將計算資源交給下一個任務。由於稍後還要恢復以前的任務,因此中斷的時候要保存現場,即當前任務的上下文,也能夠叫作環境。segmentfault
做用域
(scope)是標識符(變量)在程序中的可見性範圍。做用域規則是按照具體規則維護標識符的可見性,以肯定當前執行的代碼對這些標識符的訪問權限。做用域是在具體的做用域規則之下肯定的。瀏覽器
上下文、環境有時候也稱做用域,即這兩個概念有時候是混用的;不過,上下文指代的是總體環境,做用域關注的是標識符(變量)的可訪問性(可見性)。上下文肯定了,根據具體編程語言的做用域規則,做用域也就肯定了。這就是上下文與做用域的關係。緩存
function callWithContext(fn, context) {
return fn.call(context);
}
let name = 'Banana';
const apple = {
name: "Apple"
};
const orange = {
name: "Orange"
};
function echo() {
console.log(this.name);
}
echo(); // Banana
callWithContext(echo, apple); // Apple
callWithContext(echo, orange); // Orange
複製代碼
var a = 1;
function foo(){
// 返回一個箭頭函數
return () => {
// this 繼承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
foo()() // 1
var bar = foo.call( obj1 ); // 調用位置
bar.call( obj2 ); // 2
foo.call( obj2 )(); // 3
複製代碼
JavaScript代碼的整個執行過程,分爲兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段建立。多線程
當JavaScript代碼執行進入一個環境時,就會爲該環境建立一個執行上下文
,它會在你運行代碼前作一些準備工做,如肯定做用域,建立局部變量對象等。閉包
JS代碼的執行環境app
執行上下文的類型編程語言
JavaScript運行時首先會進入全局環境,對應會生成全局上下文。程序代碼中基本都會存在函數,那麼調用函數,就會進入函數執行環境,對應就會生成該函數的執行上下文。模塊化
函數編程中,代碼中會聲明多個函數,對應的執行上下文也會存在多個。在JavaScript中,經過棧的存取方式來管理執行上下文,咱們可稱其爲執行棧,或函數調用棧(Call Stack)。棧底永遠都是全局上下文,而棧頂就是當前正在執行的上下文。函數
程序執行進入一個執行環境時,它的執行上下文就會被建立,並被推入執行棧中(入棧);程序執行完成時,它的執行上下文就會被銷燬,並從棧頂被推出(出棧),控制權交由下一個執行上下文。棧結構
由於JS執行中最早進入全局環境,因此處於"棧底的永遠是全局環境的執行上下文"。而處於"棧頂的是當前正在執行函數的執行上下文",當函數調用完成後,它就會從棧頂被推出。
"全局環境只有一個,對應的全局執行上下文也只有一個,只有當頁面被關閉以後它纔會從執行棧中被推出,不然一直存在於棧底"
let color = 'blue';
function changeColor() {
let anotherColor = 'red';
function swapColors() {
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
複製代碼
做用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
在 JavaScript 中,這個具體的做用域規則就是詞法做用域
(lexical scope),也就是 JavaScript 中的做用域鏈的規則。詞法做用域是的變量在編譯時(詞法階段)就是肯定的,因此詞法做用域又叫靜態做用域
(static scope),與之相對的是動態做用域
(dynamic scope)。
let a = 2;
function foo() {
console.log(a);
// 會輸出2仍是3?
}
function bar() {
let a = 3;
foo();
}
bar();
複製代碼
前面說過,詞法做用域也叫靜態做用域,變量在詞法階段肯定,也就是定義時肯定。雖然在 bar 內調用,但因爲 foo 是閉包函數,即便它在本身定義的詞法做用域之外的地方執行,它也一直保持着本身的做用域。所謂閉包函數
,即這個函數封閉了它本身的定義時的環境
,造成了一個閉包
。(即閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的全部局部變量)因此 foo 並不會從 bar 中尋找變量,這就是靜態做用域的特色。
而動態做用域並不關心函數和做用域是如何聲明以及在何處聲明的,只關心它們從何處調用。換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套。
詞法做用域是在寫代碼或者定義時肯定的,而動態做用域是在運行時肯定的。詞法做用域關注函數在何處聲明,而動態做用域關注函數從何處調用。
function foo() {
let a = 0;
function bar() {
console.log(a);
}
return bar;
}
let a = 1;
let sub = foo();
sub(); // 0;
複製代碼
一旦設置了參數的默認值,函數進行聲明初始化時,參數會造成一個單獨的做用域(context)。等到初始化結束,這個做用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
複製代碼
上面代碼中,函數foo
的參數造成一個單獨做用域。這個做用域裏面,首先聲明瞭變量x
,而後聲明瞭變量y
,y
的默認值是一個匿名函數。這個匿名函數內部的變量x
,指向同一個做用域的第一個參數x
。函數foo
內部又聲明瞭一個內部變量x
,該變量與第一個參數x
因爲不是同一個做用域,因此不是同一個變量,所以執行y
後,內部變量x
和外部全局變量x
的值都沒變。
模塊化、柯里化、模擬塊級做用域、命名空間、緩存數據
const tar = (function () {
let num = 0;
return {
addNum: function () {
num++;
},
showNum: function () {
console.log(num);
}
}
})()
tar.addNum();
tar.showNum();
複製代碼
let add = function(x){
return function(y){
return x + y
}
}
console.log(add(2)(4)) // 6
複製代碼
for (var i = 1; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
function func(){
for(var i = 0; i < 5; i++){
+ (i => { setTimeout(() => console.log(i),300) })(i)
}
}
func()
複製代碼
var MyNamespace = {};
MyNamespace.doSomething = function (){
//使用閉包產生的私有類變量
var label, icon;
//可訪問私有變量,但不可被外部訪問的私有方法
function setLabel(){
// do something...
}
//可訪問私有變量,也可被外部訪問的方法
this.getLabel = function(){
// do something...
};
}
// 該方法可被外部訪問,卻只能經過取/賦值器訪問私有類變量
MyNamespace.TreeItem.prototype = {
print: function(){
console.log( this.getLabel() );
}
}
複製代碼
import {readFileSync, readdirSync} from 'fs';
var readContent = (function(){
let contentCache = {};
return (bookName)=>{
let content = contentCache[bookName];
if (!content){
content = readFileSync(bookName+".txt", "utf8");
contentCache[bookName] = content;
}
return content;
};
})();
複製代碼
參考文章: