JavaScript:上下文相關

前言

  本文內容主要涵蓋了執行上下文棧、執行上下文、變量對象、函數變量提高等內容。java

  衆所周知,JavaScript是單線程編程語言,同一時間只能作一件事情,程序執行順序由上而下,程序的執行主要依託JavaScript引擎,JavaScript引擎也並不是一行一行的分析執行代碼,而是一段一段的分析執行。git

可執行代碼

  JavaScript引擎執行的代碼固然是可執行代碼,在JavaScript中可執行代碼有三類:全局代碼、函數代碼以及Eval代碼。github

javaScript運行原理

  JavaScript程序的執行主要分語法檢查和運行兩個階段,語法檢查包括詞法分析和語法分析,目的是將JavaScript高級語言程序轉成抽象語法樹。面試

  語法檢查完成後,到了執行階段,執行階段包括預解析和執行,預解析首先會建立執行上下文(本文重點),將語法檢查正確後生成的抽象語法樹複製到當前執行上下文中,而後作屬性填充,對語法樹當中的變量名、函數聲明以及函數的形參進行屬性填充。最後就是執行。編程

  JavaScript運行原理會在後面的文章輸出,不是本文的重點,本文只需知道程序運行的大體是怎樣的過程,執行上下文什麼時候建立。數組

執行上下文棧

  何爲執行上下文棧???瀏覽器

  在JavaScript解釋器運行階段(預解析)還維護了一個棧,用於管理執行上下文。在執行上下文棧中,全局執行上下文處於棧底,頂部爲當前的執行上下文。當頂部的執行完成,就會彈出棧,相似於數據結構中的棧,每當有當前的執行上下文執行完就會從棧頂彈出,這種管理執行上下文的棧叫作執行上下文棧。bash

  一個執行上下文能夠激活另外一個執行上下文,相似於函數調用另外一個函數,能夠一層一層的調用下去。數據結構

  激活其它執行上下文的某執行上下文被稱爲調用者(caller),被激活的執行上下文被稱爲被調用者(callee)。一個執行上下文便可能是調用者也有多是被調用者。dom

  當一個caller激活了一個callee時,caller會暫停自身的執行,將控制權交給callee,此時該callee被放進執行上下文棧,稱爲進行中的上下文,當這個callee上下文結束後,把控制權交還給它的callercaller會在剛纔暫停的地方繼續執行。在這個caller結束後,會繼續觸發其餘的上下文。

執行上下文棧在JavaScript中能夠數組模擬:

ECStack = [];
複製代碼

  當瀏覽器首次載入腳本,會默認先進入到全局執行上下文,位於執行上下文棧的最底部,此時全局代碼會開始初始化,初始化生成相應的對象和函數,在全局上下文執行的過程當中可能會激活一些其餘的方法(若是有的話),而後進入它們的執行上下文,並將元素壓入執行上下文棧中。能夠把全部的程序執行看做一個執行上下文棧,棧的頂部是正在激活的上下文。以下表所示:

EC stack
Active EC
...
EC
Global EC

  在程序結束以前,ECStack最底部永遠是globalContext

ECStack = [
    globalContext
];
複製代碼

   看看下面實例一,是一個怎麼的過程:

// 實例一
function bar() {
    console.log('bar');
}
function foo() {
    bar();
}
foo();
複製代碼

  當執行一個函數時,會建立一個執行上下文並壓入執行上下文棧中,當函數執行完畢,就將該執行上下文彈出執行上下文棧。

// 建立執行上下文棧
ECStack = [];

// foo() 建立該函數執行上下文並壓入棧中
ECStack.push(<foo> functionContext);

// foo()中調用了bar(),建立bar()執行上下文並壓入棧中
ECStack.push(<bar> functionContext);

// bar()執行完畢彈出
ECStack.pop();

// foo()執行完畢彈出
ECStack.pop();

複製代碼

執行上下文(Execution Context)

  執行上下文在程序運行的預解析階段建立,預解析也就是代碼的真正的執行前,能夠說是代碼執行前的準備工做,即建立執行上下文。

  執行上下文有何用,主要作了三件事:

  1. this綁定;
  2. 建立變量對象;
  3. 建立做用域鏈。

  this做用域做用域鏈也是JavaScript中很重要的知識點,後面的文章會詳細的輸出。

  何爲執行上下文?

  執行上下文理解爲是執行環境的抽象概念,當JavaScript代碼執行一段可執行代碼時,都會建立對應的執行上下文,一個執行上下文能夠抽象的理解爲object,都包括三個重要屬性:

executionContext: {
    variable object:vars, functions, arguments
    scope chain: variable object + all parents scopes
    thisValue: context object
}
複製代碼

全局代碼

  全局代碼不包含函數內代碼,在初始化階段,執行上下文棧底部有一個全局執行上下文:

ECStack = [
  globalContext
];
複製代碼

函數代碼

  當進入函數代碼時,函數執行,建立該函數執行上下文並壓入棧中。須要注意的是函數代碼不包含內部函數代碼。

ECStack = [
  <xxx> functionContext
  ...
  <xxx> functionContext
  globalContext
];
複製代碼

Eval代碼

  eval(...)有些陌生,平時也不多用到,eval(...)函數能夠接受一個字符串爲參數,並將其中的內容視爲好像在書寫就存在於程序中這個位置的代碼。

  換句話說,能夠在你寫的代碼中用程序生成代碼並運行,就好像是寫在那個位置的同樣。

eval('var x = 10');
 
(function foo() {
  eval('var y = 20');
})();
 
console.log(x); // 10
console.log(y); // 'y is not defined'
複製代碼

   上面實例執行過程:

ECStack = [
  globalContext
];
 
// eval('var x = 10')進棧
ECStack.push(
  evalContext,
  callingContext: globalContext
);
 
// eval出棧
ECStack.pop();
 
// foo funciton 進棧
ECStack.push(<foo> functionContext);
 
// eval('var y = 20') 進棧
ECStack.push(
  evalContext,
  callingContext: <foo> functionContext
);
 
// eval出棧
ECStack.pop();
 
// foo 出棧
ECStack.pop();
複製代碼

變量對象

  變量對象(variable object)是與執行上下文相關的數據做用域(scope of data),是與上下文相關的特殊對象,用與存儲被定義在上下文中的變量(variables)和函數聲明(function declarations)。變量對象是一個抽象的概念,不一樣的上下文,它表示使用不一樣的對象。

全局變量對象

  全局變量對象是全局上下文的變量對象。全局變量對象就是全局對象,爲啥這麼說:

全局對象(Global object) 是在進入任何執行上下文以前就已經建立了的對象;這個對象只存在一份,它的屬性在程序中任何地方均可以訪問,全局對象的生命週期終止於程序退出那一刻。

  • 全局對象初始建立階段將MathStringDateparseInt做爲自身屬性,等屬性初始化,一樣也能夠有額外建立的其它對象做爲屬性(其能夠指向到全局對象自身)
  • DOM中,全局對象的window屬性就能夠引用全局對象自身
  • 能夠經過全局上下文的this來訪問全局對象,一樣也能夠遞歸引用自身
  • 當訪問全局對象的屬性時一般會忽略掉前綴,全局對象是不能經過名稱直接訪問的
global = {
  Math: <...>, String: <...>, Date: <...>, ... window: global } console.log(Math.random()); //當訪問全局對象的屬性時一般會忽略掉前綴;初始建立階段將Math等做爲自身屬性 console.log(this.Math.random()); // 經過this來訪問全局對象 console.log(this) // window 經過全局上下文的this來訪問全局對象 var a = 1; console.log(this.a); // 1 console.log((window.a); // 1 全局對象有 window 屬性指向自身 console.log(a); // 1 當訪問全局對象的屬性時一般會忽略掉前綴 this.window.b = 2; console.log(this.b); // 2 複製代碼

  上面的全局對象的定義和變量對像的定義對比,能知道全局變量對象就是全局對象,簡單的說,由於變量對象用於存儲被定義在上下文中的變量和函數聲明,全局對象在進入任何執行上下文前就已經建立了,一樣存儲着在全局範圍內定義的變量和函數聲明。

  須要注意的是全局上下文的變量對象容許經過VO屬性名稱來間接訪問,緣由就是全局變量對象就是全局對象,在其餘上下文中是不能直接VO對象。

  全局變量對象VO會有下列屬性:

  • 函數聲明(FunctionDeclaration, FD)
  • 全部的變量聲明(var, VariableDeclaration)
  • 不存在所謂的函數形參

函數上下文變量對象(Variable object

  當進入執行上下文時,VO包含來下列屬性:

  • 函數形參,屬性名就是參數名,其值是實參值,若沒有傳遞的參數,其值爲undefined
  • 函數聲明(FunctionDeclaration, FD),由名稱和對應值組成一個變量對象的屬性被建立;若是變量對象已經存在相同屬性名稱,則徹底替換這個屬性。
  • 全部的變量聲明(var, VariableDeclaration),由名稱和對應值(undefined)組成一個變量對象的屬性被建立;若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。
function foo(a) {
  var b = 2;
  var c = function() {};
  function bar() {
    console.log('bar');
  }
}
foo(10);
複製代碼

  當進入帶有參數10的foo函數執行上下文時,VO

VO = {
  a: 10,
  bar:  <reference to FunctionDeclaration 'bar'>,
  b: undefined,
  c: undefined
}
複製代碼

  在函數聲明過程當中,若是變量對象已經存在相同的屬性名稱,則徹底替換這個屬性:

function foo(a) {
  console.log(a);
  function a() {}
}
foo(10) // function a(){}
複製代碼

  在變量聲明過程當中,若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性

// 與參數名同名
function foo(a) {
  console.log(a);
  var a = 20;
}
foo(10) // 10

// 與函數名同名
function bar(){
    console.log(a)
    var a = 10
    function a(){}
}
bar() // 'function a(){}'
複製代碼

  VO建立過程當中,函數形參的優先級是高於函數的聲明的,結果是函數體內部聲明的function a(){}覆蓋了函數形參a的聲明,所以最後輸出a是一個function

  從上面的實例說明,函數聲明比變量聲明的優先級高,在定義的過程當中不會被變量覆蓋,除非是賦值:

function foo(a){
    var a = 10
    function a(){}
    console.log(a)
}
foo(20) // 10

function bar(a){
    var a 
    function a(){}
    console.log(a)
}
bar(20) // 'function a(){}'

複製代碼

活動對象(Activation object

  活動對象想必你們對這個概念都不陌生,可是容易和變量對象混淆。

  活動對象就是函數上下文中的變量對象,只是不一樣階段的不一樣叫法,在建立函數執行上下文階段,變量對象被建立,變量對象的屬性不能被訪問,此時的函數尚未執行,當函數來到執行階段,變量對象被激活,變成了活動對象,而且裏面的屬性都能訪問到,開始進行執行階段的操做。

// 執行階段
VO -> AO

function foo(a) {
  var b = 2;
  var c = function() {};
  function bar() {
    console.log('bar');
  }
}
foo(10);

VO = {
  arguments: {
    0: 10,
    length: 1
  },
  a: 10,
  bar:  <reference to FunctionDeclaration 'bar'>,
  b: undefined,
  c: undefined
}

AO = {
  arguments: {
    0: 10,
    length: 1
  },
  a: 10,
  bar:  <reference to FunctionDeclaration 'bar'>,
  b: 2,
  c: reference to FunctionExpression "c"
}
複製代碼

  調用函數時,會爲其建立一個Arguments對象,並自動初始化局部變量arguments,指代該Arguments對象。全部做爲參數傳入的值都會成爲Arguments對象的數組元素。

  簡潔的總結下上面的內容:

  1. 全局上下文的變量對象是全局對象;
  2. 函數上下文的變量對象初始化只包含Arguments對象;
  3. 在進入執行上下文時會給變量對象添加形參、函數聲明及變量聲明等屬性;
  4. 在代碼執行,能夠經過賦值修改變量對象的屬性。

提高

  提高一個很常見的話題,是面試中常常被問到的一部分,函數聲明優先級比變量聲明高,這句話應該是大部分同窗都會回答,爲啥,上面的內容已經很好的作出瞭解釋。看下面實例:

function test() {
    console.log(foo); // function foo(){}
    console.log(bar); // undefined

    var foo = 'Hello';
    console.log(foo); // Hello
    var bar = function () {
      return 'world';
    }
    function foo() {
      return 'hello';
    }
}

test();
複製代碼
// 建立階段
VO = {
  arguments: {
    length: 0
  },
  foo: <reference to FunctionDeclaration 'foo'>, // 解釋了第一個輸出是foo引用,函數聲明優先變量被建立,同名屬性不會被幹擾,在函數尚未被調用前已經被建立了,即能輸出foo的引用
  bar: undefined // 解釋了第二個輸出是undefined,函數表達式仍是隻是一個變量聲明,不是函數聲明,不會被提高
}
複製代碼
// 執行階段
VO -> OV
OV = {
  arguments: {
    length: 0
  },
  foo: 'Hello', // 這裏解釋了爲何第三個輸出值爲‘Hello’,作了賦值操做
  bar: reference to FunctionExpression "bar"
}
複製代碼
// 實例真實的執行順序
function test() {
  function foo() {
      return 'hello';
    }
  }
  var foo;
  var bar;
  console.log(foo);
  console.log(bar);
  foo = 'Hello';
  console.log(foo);
  bar = function () {
    return 'world';
  }
}
複製代碼

  須要注意的是變量提高只存在使用var關鍵字聲明變量,若是是使用let聲明變量不存在變量提高。

  聲明變量的做用域限制在其聲明位置的上下文中,在上下文被建立的階段時建立了,若是沒有聲明的變量老是全局的,而且是在執行階段將賦值給未聲明的變量的值被隱式建立爲全局變量,能夠經過delete操做符刪除,聲明變量不能夠。

function foo() {
    console.log(a); // Uncaught ReferenceError: a is not defined;a不存在VO中
    a = 1;
    console.log(a);
}
foo(); 

function bar() {
    a = 1;
    console.log(a); // 1 能夠在全局變量中找到a的值
}
bar(); 

c = 10;
console.log(delete c); // true
var d = 10;
console.log(delete d); // false
複製代碼

  若是清楚上下文相關的內容,提高的問題很好的能解答,在學習中咱們仍是須要了解一些底層的知識,這樣有助咱們更好的進步。

結語

  文章若有不正確的地方歡迎各位大佬指正,也但願有幸看到文章的同窗也有收穫,一塊兒成長!

-----------------------------本文首發於我的公衆號---------------------------

最後,歡迎你們關注公衆號 ZeroToOneMe,或者 github博客,一塊兒學習交流。
相關文章
相關標籤/搜索