javascript採用的是詞法做用域(靜態做用域) 與之相對的是動態做用域,如下的例子能夠很好地說明javascript
let a = 3 function foo() { console.log(a); } function foowrapper() { let a = 4 foo() } foowrapper() // 3而不是4
靜態做用域是與咱們慣性思惟衝突的,從上述例子能夠看出,咱們第一次看會習慣性往函數調用位置去找變量(動態做用域思惟),可是javascript使用的是靜態做用域,也就是代碼寫在哪裏,做用域就從那裏連接,而不是運行的位置進行做用域連接,這是整個做用域的核心概念html
規格
僞代碼
執行環境由詞法環境、變量環境 this組成 ,今天咱們主要關注點在變量環境(VO|AO),和詞法環境[[scope]]前端
activeExecutionContext = { VO: {...}, // or AO this: thisValue, Scope: [ // Scope chain // 全部變量對象的列表 // 用於標識符查找 ] };
// [[Scope]]表示內部使用的抽象操做,用於描述語言規範自己 Scope = AO + [[Scope]]
在函數進入上下文(函數建立時)AO/VO,Scope定義以下:java
Scope = AO|VO + [[Scope]]
解析
活動對象是做用域數組的第一個對象,被添加到做用域的前端數組
Scope = [AO].concat([[Scope]])
函數激活連接實例app
var x = 10; function foo() { var y = 20; function bar() { var z = 30; alert(x + y + z); } bar(); } foo(); // 60
對此,咱們有以下的變量/活動對象,函數的的[[scope]]屬性以及上下文的做用域鏈:ecmascript
全局上下文的變量對象是:函數
globalContext.VO === Global = { x: 10 foo: <reference to function> };
在「foo」建立時,「foo」的[[scope]]屬性是:this
foo.[[Scope]] = [ globalContext.VO ];
在「foo」激活時(進入上下文),「foo」上下文的活動對象是:es5
fooContext.AO = { y: 20, bar: <reference to function> };
「foo」上下文的做用域鏈爲:
fooContext.Scope = fooContext.AO + foo.[[Scope]] fooContext.Scope = [ fooContext.AO, globalContext.VO ];
內部函數「bar」建立時,其[[scope]]爲:
bar.[[Scope]] = [ fooContext.AO, globalContext.VO ];
在「bar」激活時,「bar」上下文的活動對象爲:
barContext.AO = { z: 30 };
「bar」上下文的做用域鏈爲:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.: barContext.Scope = [ barContext.AO, fooContext.AO, globalContext.VO ];
對「x」、「y」、「z」的標識符解析以下:
- "x" -- barContext.AO // not found -- fooContext.AO // not found -- globalContext.VO // found - 10 - "y" -- barContext.AO // not found -- fooContext.AO // found - 20 - "z" -- barContext.AO // found - 30
var x = 10; function foo() { var y = 20; function barFD() { // 函數聲明 alert(x); alert(y); } var barFE = function () { // 函數表達式 alert(x); alert(y); }; var barFn = Function('alert(x); alert(y);'); barFD(); // 10, 20 barFE(); // 10, 20 barFn(); // 10, "y" is not defined } foo();
正常來講,在函數建立時獲取函數的[[scope]]屬性,經過該屬性訪問到全部父上下文的變量,可是由函數構造函數建立的的函數,其[[scope]]老是惟一的全局對象屬性。
eval可以動態生成函數,且eval上下文與當前調用上下文的做用域鏈一致
示例
function foo(str, a) { eval( str ); // 4 var c = 4; console.log( a, b ); } var b = 2; var c = 3; foo( "var b = 3; console.log(c)", 1 ); // 1, 3
這裏咱們使用eval打印了c,能夠看到結果是foo做用中的c,這裏判斷出eval不是像函數構造函數的[[Scope]]老是惟一的全局變量,而b變量可以在foo中訪問,也說明了eval與foo是享受了同一個做用域,固然嚴格模式下,eval會建立本身的做用域,不會影響到當前調用上下文的做用域,以下例:
function foo(str, a) { 'use strict' eval(str) // 4 var c = 4 console.log(`foo-a ${a} foo-b ${b}`) // 1, 2 而不是 1 3 } var b = 2 var c = 3 foo('var b = 3; console.log("eval-b",b,"eval-c",c)', 1)
這裏訪問b時,輸出了2而不是eval中的3,說明嚴格模式下,eval建立了本身的做用域,不會影響到當前調用的上下文做用域
它們將做用變量添加到做用域鏈最前端,做用域鏈修改以下:
Scope = withObject | catchObject + AO | VO +[[Scope]]
with實例
var x = 10, y = 10; with ({x: 20}) { var x = 30, y = 30; console.log(x) // 30 console.log(y) // 30 } console.log(x) // 10 console.log(y) // 30
過程解析:
catch實例
try { ... } catch (ex) { console.log(ex) }
做用域鏈修改成:
var catchObject = { ex: <exception object> }; Scope = catchObject + AO|VO + [[Scope]]
爲何會有二維做用域鏈呢,由於一個屬性在對象中沒有直接找到,查詢會在原型鏈中繼續。也就是搜索屬性
深刻到原型鏈環節
對象屬性查找,先要在做用域鏈上找到對象自己,而後再去對象原型鏈上找到指定屬性。
狀況1示例
function foo() { console.log(x) } Object.prototype.x = 10; foo(); // 10
過程解析:
狀況2示例
function foo() { console.log(o.x) } let o = { } Object.prototype.x = 200 foo();
過程解析:
參考了顏海鏡大大翻譯的ES5.1規格,感謝他和湯姆大叔的無私奉獻,而後我才能發現一些東西別人幾年前就看透了,本身好菜呀(劃掉),這裏是湯姆大叔的原文,我推薦大看一下他的深刻js系列,雖然在2012寫的,可是放在如今仍然是教科書級別的乾貨,例子出於他的文章和《你不知道的javascript》