總結(jié)來說很簡單:
- 函數(shù)運行在定義時的作用域中
- 變量查找會從當前作用域開始查找,找不到則到下一層作用域查找,直到找到并返回或者返回 undefined
實際例子
var name = "global"
function echo() {
console.log(name)
}
echo()
打印global毫無疑問,因為在echo()函數(shù)作用域沒有找到,就到外層作用域中尋找,找到了值為global的name變量。
如何查找
在調(diào)用某個函數(shù)時,會從函數(shù)這個對象上拿到[[scope]]屬性對應(yīng)的作用域鏈,并且將函數(shù)的變量對象也放入到這個作用域鏈中,而該作用域鏈是在函數(shù)定義時就確定了的。
同樣是上面的例子,作用域鏈的變化是這樣的:
// 調(diào)用 echo() 函數(shù)前
scopes = {
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
重點在echo對象上有[[scope]]屬性,指向的就是最外面的這個scopes,即作用域鏈。
在調(diào)用echo()后,會將該函數(shù)的變量對象:
{
this: Window
}
放到echo對象的[[scope]]屬性對應(yīng)的scopes上,那就變成了這樣:
// 調(diào)用 echo() 時
scopes = {
1: {
this: Window
},
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
在echo函數(shù)內(nèi)尋找name就是指會先在
{
this: Window
}
這個變量對象上尋找名為name的鍵,如果沒有找到就向下(將 0 視為下)尋找,所以就會在:
{
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
這個變量對象上尋找,然后找到了值為global的name并返回。
進階問題
嘗試解答下面代碼會打印出什么?
var name = "global"
function echo () {
console.log(name) // 打印什么?
}
function change() {
var name = "local"
echo()
}
change()
具體分析
按照上面的分析過程,在調(diào)用change()之前,作用域鏈應(yīng)該是這樣的:
// 調(diào)用 change() 前
scopes = {
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
change: {
name: 'change',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
多了change這個鍵值對,其他沒什么,然后調(diào)用change函數(shù):
// 調(diào)用 change 函數(shù)時
scopes = {
1: {
name: 'local',
this: Window
},
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
change: {
name: 'change',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
如果在change函數(shù)內(nèi)尋找name變量,找到的肯定是local毫無疑問。最后是echo函數(shù)的調(diào)用,也是這段代碼的意義所在,調(diào)用時是這樣的:
// 調(diào)用 echo 函數(shù)時
scopes = {
1: {
this: Window
},
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
change: {
name: 'change',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
所以在echo函數(shù)內(nèi)尋找不到name,就到下層變量對象找,找到了global。所以這段代碼最終是打印出了global。
圖片可能更為直觀,灰色塊為作用域鏈,即scopes,綠色塊為變量對象,如下所示 ↓

淺拷貝?
雖然到目前為止能夠解釋絕大部分的作用域問題,但還存在疑問:
[[scope]]: scopes
這里是指
在一個函數(shù)被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數(shù)對象的[[scope]]屬性。
我們都知道,對象是引用類型,那從 2 -> 3 的過程中,尤其是echo函數(shù)執(zhí)行時,先從echo函數(shù)上拿到[[scope]]屬性,這時候不應(yīng)該是:
scopes = {
1: {
name: 'local',
this: Window
},
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: scopes
},
change: {
name: 'change',
function: function () {console.log(name)},
[[scope]]: scopes
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
這樣的嗎,因為[[scope]]保存的是scopes的內(nèi)存地址啊,所以我能夠自己解釋為
在函數(shù)定義時,將
scopes進行淺拷貝并保存到[[scope]]屬性上。
所以上面的例子,作用域鏈嚴格來說應(yīng)該是這樣的:
scopes = {
0: {
name: 'global',
echo: {
name: 'echo',
function: function () {console.log(name)},
[[scope]]: {
0: {...}
}
},
change: {
name: 'change',
function: function () {console.log(name)},
[[scope]]: {
0: {...}
}
},
console,
parseInt: function () {...}
// 等一些全局變量與方法
}
}
但是真的是這樣嗎?