ES5.1下的做用域鏈

做用域鏈(ES5.1規格視角)

javascript做用域類型

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]]表示內部使用的抽象操做,用於描述語言規範自己
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可以動態生成函數,且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建立了本身的做用域,不會影響到當前調用的上下文做用域

with、catch

它們將做用變量添加到做用域鏈最前端,做用域鏈修改以下:

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

過程解析:

  1. x = 10, y = 10
  2. {x: 20} 添加到做用域頂端
  3. var x 、var y 在執行代碼前以及被提高到with外面
  4. 修改{x: 20} 中的20 爲 30
  5. 沒在{x: 30}中找到y,在全局做用域中找到了y,將其修改成30
  6. with結束,將{x: 30} 從做用域鏈中移出。
  7. 此時x爲全局做用域x,沒有變化,而y在with聲明時,變爲30

catch實例

try {
  ...
} catch (ex) {
  console.log(ex)
}

做用域鏈修改成:

var catchObject = {
  ex: <exception object>
};
 
Scope = catchObject + AO|VO + [[Scope]]

知識關聯---二維做用域鏈(做用域鏈、原型鏈)查找

爲何會有二維做用域鏈呢,由於一個屬性在對象中沒有直接找到,查詢會在原型鏈中繼續。也就是搜索屬性

過程
  1. 做用域鏈環節
  2. 深刻到原型鏈環節

    可能的狀況
    二維做用域鏈是指在做用域查找以後,再去原型鏈上查找,分爲一下兩種狀況
  3. 基本屬性查找,查找到了做用域鏈末端,全局變量節點,此時會在全局變量的原型鏈上展開搜索。
  4. 對象屬性查找,先要在做用域鏈上找到對象自己,而後再去對象原型鏈上找到指定屬性。

狀況1示例

function foo() {
  console.log(x)
}
 
Object.prototype.x = 10;
 
foo(); // 10

過程解析:

  1. 在foo的VO對象沒有找到x
  2. 向做用域鏈下一個節點查找,此時爲window
  3. 沒法在window上查找到屬性x
  4. 此時已是做用域頂端,便向着window其原型鏈找到x,辛酸過程以下圖

    能夠看到window原型鏈的末端節點爲Object,終於在這裏找了x,輸出了10

狀況2示例

function foo() {
  console.log(o.x)
}

let o = {
}
 
Object.prototype.x = 200
foo();

過程解析:

  1. 在foo的VO對象沒有找到o
  2. 在window對象找到了o
  3. 在o自身查找x屬性,沒有找到
  4. 在o的原型上Object.prototype上找到了x = 200
  5. 輸出200

感謝

參考了顏海鏡大大翻譯的ES5.1規格,感謝他和湯姆大叔的無私奉獻,而後我才能發現一些東西別人幾年前就看透了,本身好菜呀(劃掉),這裏是湯姆大叔的原文,我推薦大看一下他的深刻js系列,雖然在2012寫的,可是放在如今仍然是教科書級別的乾貨,例子出於他的文章和《你不知道的javascript》

相關文章
相關標籤/搜索