Execution Context(EC) in ECMAScript

參考資料javascript

代碼的執行所處的環境,也叫執行上下文,它肯定了代碼的做用域,做用域鏈,this屬性,代碼的生存期等等,讓咱們從解釋器的角度看代碼是如何執行的。
EC能夠用以下的數據結構表達,它有不少屬性,VO,[[scope]],this等等。html

EC={
    Variable Object:....,
    [[scope]]:...,
    this:...
}

1 三種EC(代碼執行環境)

  • global
  • function
  • eval

2 代碼執行的過程

一段JS代碼進入解釋器到執行分爲2步驟,這2步各自有各自的事要處理java

  • 進入執行環境
  • 執行代碼

3 Variable Object(VO)

咱們聲明的變量,聲明的函數,傳入的參數都放到哪裏去了?引擎是在哪裏尋找它們的?其實它們都放入到一個叫VO的對象裏去了,能夠說了解VO是很重要的。VO的數據結構能夠以下表達chrome

VO={
    聲明的變量,
    聲明的函數,
    參數(函數內部VO擁有)
}

3.1 函數的聲明與表達式

  • 函數聲明式:function 函數名稱 (參數:可選){ 函數體 }
  • 函數表達式:function 函數名稱(可選)(參數:可選){ 函數體 }

ECMAScript是經過上下文來區分的,若是function foo(){}是做爲賦值表達式的一部分的話,那它就是一個函數表達式,反之爲函數聲明式。數組

function foo(){} // 聲明
var bar = function foo(){}; // 表達式,由於它是賦值表達式的一部分

new function bar(){}; // 表達式,由於它是new表達式

(function(){
    function bar(){} // 聲明
})();

(function foo(){}); // 函數表達式:包含在分組操做符()內,而分組符裏的表達式

注意:根據表達式的生成函數的函數名稱是不會放入函數所處的EC的VO中的。瀏覽器

3.1.1 DEMO

function f() {
    var bar = function foo() {};//這是表達式聲明函數

    typeof bar;//function;
    typeof foo;//undefined;
}

f.VO.foo=undefined;//不管是在代碼進入環境仍是代碼執行的時候

可是這個foo只在foo函數EC中有效,由於規範規定了標示符foo不能在外圍的EC有效,並且是在foo的VO中存在,有些瀏覽器(chrome)是沒法用debug訪問到的,可是firefox是能夠訪問到的,可是IE6~IE8是在foo的外圍能夠訪問到foo的,IE9已經修復了這個問題,能夠用IE8執行以下代碼。數據結構

alert(typeof foo);//undefined
var bar = function foo() {
    alert(typeof foo);//function
    function k() {
    }

    return function () {
        alert(typeof foo);//function
        alert(typeof k);//function
    }
}

bar()();

EC肯定了VO的不一樣,因此按EC給VO分類。閉包

3.2 全局環境的VO

全部在global聲明的函數,變量都會在global的VO中存在。app

global.vo = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global //引用自身
};

3.3 函數的VO

當進入執行上下文VO會有以下屬性:dom

  • 函數的全部形參(若是咱們是在函數執行上下文中)

    由名稱和對應值組成的一個變量對象的屬性被建立;沒有傳遞對應參數的話,那麼由名稱和undefined值組成的一種變量對象的屬性也將被建立。

    function f(a, b, a) {
          debugger;
      }
      f(1, 2, 3);

    執行的時候f的VO

    f.VO={
          a:3,
          b:2
      }

    由於形參名字重複,而VO的key是不能夠重複的(VO是一個對象),因此在代碼執行給VO賦值時根據前後順序最後一個實參會覆蓋第一個實參的值。

  • 全部函數聲明(FunctionDeclaration, FD)

    由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被建立;若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性。

  • 全部變量聲明(var, VariableDeclaration)

    由名稱和對應值(undefined)組成一個變量對象的屬性被建立;若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。

能夠看到聲明的函數優先級大於變量的聲明

alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

3.3.1 DEMO

test1

function f(a, b, c) {
    var a = a, g = 1;
    function g() {
        //function body
    }
    var k = 1;
}

f(1, 2, 3);

引擎進入執行環境時,把EC中的變量,形參,聲明的函數放入VO,成爲VO的屬性

f.VO={
    a:undefined,//這個是形參的a,優先級高於聲明的變量a
    b:undefined,//這個是形參的b
    c:undefined,//這個是形參的c
    g:function,//函數的優先級最高,覆蓋了變量g
    k:undefined//聲明的k
};

代碼執行時給VO屬性賦值,按代碼執行過程

f.VO={
    a:1,
    b:2,
    c:3,
    g:function,
    k:1
};

test2

function f(a) {
    a = a;
    b = a;
}
f(1);
alert(a);//undefined
alert(b);//1

test2很奇怪是吧,咱們認爲會alert(a)提示「1」,可是結果是undefined。在咱們的腦海裏老是有這個概念:沒有聲明的變量會變成全局變量,其實根本沒這回事。事實是:給沒有聲明的變量賦值形成的現象是變量變爲了global的屬性(也就是window屬性),而不是一個全局變量。讓咱們來看下代碼的流程。

進入環境,把形參標示放入VO中,並賦值爲undefined

f.VO={
    a:undefined
}

這裏沒有把b算入f的VO,由於b不是聲明出來的

執行時

形參a根據實參1,被賦予1,代碼第一行a=a,右邊的a爲1,解釋給左邊的a賦值,解釋器從VO開始尋找a,發現VO中有a,就給其賦予形參a的值——1。解釋器尋找b,從[[scope]]尋找一直到global的VO都沒找到b,因而就給global添加一個屬性——b,賦值於1。期間沒有產生新的變量

test3

function f() {
    var a = 1;

    return {
        set:function (b) {
            a = b;
        },
        get:function () {
            return a;
        }
    }
}

var o=f();
o.set(2);
o.get();//2

總結:

  • 因此咱們不必在函數體內又聲明一個和形參相同的變量,直接訪問形參變量即是。
  • 咱們聲明的變量能夠放到一塊兒聲明,緣由已經在前面闡述了,而代碼執行過程只是根據你的賦值表達式往VO中屬性賦值。

4 [[scope]]

[[scope]]是函數的內部屬性,它指向一個數組對象(俗稱做用域鏈對象),這個數組對象會包含父親函數的VO一直到global的VO。

[[scope]]-->VO+[[scope]]

這個對象在2種環境(進入執行環境,執行代碼)有着不一樣狀態。

eg

function f() {
    var a = 1;
}

針對這個函數來講。

  • 進入執行環境(執行代碼前),函數的EC中的VO和[[scope]]

    f.VO={
          a:undefined
      }
      f.scope=[global.VO];//全局vo在進入f的執行環境前已經建立了。
  • 執行時,把f的VO推入[[scope]]指向的數據對象第一位。

    f.VO={
          a:1
      }
      f.scope=[f.VO,global.VO];

4.1 catch,with能夠改變[[scope]]指向的數組對象結構

catch,with關鍵詞會在執行時把參數推入[[scope]]指向的數組對象第一位

witch({a:1}){
    alert(a);//1
    var a=2;
    alert(a);//2
}
  • 進入環境

    vo={
          a:undefined
      }
  • 執行

    scope=[{a:1},vo,global.vo]--->alert(a)//1
      var a=2;//執行到這裏時a的值發生了改變而且影響到scope
      scope=[{a:2},vo,global.vo]--->alert(a)//2,

從本質上了解了做用域鏈,就很容易理解閉包了。

4.2 閉包

當函數內部定義了其餘函數,就建立了閉包,閉包(子函數)有權訪問父級函數的VO全部變量。

若是子函數[[scope]]持續引用了父函數的VO,就會使父函數的VO沒法銷燬掉。因此咱們要妥善處理閉包的特性。

function f() {
    var val = 1;
    return function () {
        return val;
    }
}

var temp=f();

返回的函數[[scope]]持有f函數的VO,已至於f執行後沒法釋放VO等所佔用的內存。

var temp=null;//讓GC去處理f的內存吧。

5 this

this是在代碼進入執行環境時確認的,因此按代碼進入執行環境時所在說明this更爲清晰。

5.1 頂級執行環境global。

代碼在global中執行,this永遠都是global。

5.2 代碼在函數執行環境

函數的EC中的this是由函數調用的方式來肯定的。

5.2.1 使用apply,call方法調用函數

this指向這些函數的第一個參數

var sth = "global";
function f() {
    alert(this.sth);
}
f();//global
var o = {sth:"o"};
f.apply(o);//o
f.call(o);//o

5.2.2 使用new調用函數

這是的函數叫構造函數

  • 先生成一個對象
  • this指向這個新對象~

5.2.3 單獨使用()調用函數

執行時this的值取決於()左邊的值所屬的對象

5.2.3.1 當生成的函數對象有被引用

若是函數被引用,那麼this指向這個引用函數的東東的所屬環境,可是函數被函數的VO引用那麼this指向null,再而轉爲global。

test1

var sth = "global";
function f() {
    alert(this.sth);
}
f();//global
var o = {sth:"o"};
o.f = f;
o.f();//o

test2

var a = 'global';
function f() {
    alert(this);
}
f();//global
f.prototype.constructor();//f.prototype

test3

這種狀況下,f被k的vo引用,f的執行環境的this指向null,轉爲global。

function k() {
    function f() {
        alert(this);
    }
    f();//window
}

k.vo.f=function;
f.this=null==>global;

test4

var foo = {
  bar: function () {
    alert(this);
  }
};
 
foo.bar(); // Reference, OK => foo ()左邊的引用類型屬於foo,this指向foo
(foo.bar)(); // Reference, OK => foo 「()」對foo.bar沒有任何處理,返回還是foo.bar。
 
(foo.bar = foo.bar)(); // global 賦值操做符使返回值是foo.bar所指向的函數。返回的是一個沒有東西引用的function
(false || foo.bar)(); // global ||同上
(foo.bar, foo.bar)(); // global 連續運算符還是同上
5.2.3.2 當函數沒有被引用

this指向null,可是瀏覽器不會讓你這麼幹,它會把null變爲global。

注:第5版的ECMAScript中,已經不強迫轉換成全局變量了,而是賦值爲undefined。

(function (){
    alert(this);//window
})();

5.3 eval執行環境中的this

eval('alert(this)');//window

5.4 讓咱們回想下DOM事件

5.4.1 以節點對象的屬性註冊事件處理函數

以這個概念註冊事件處理函數有2種實現方法,可是異曲同工——都是給節點對象的事件屬性註冊事件處理函數。

5.4.1.1 直接在html裏寫事件處理函數
<div onclick="alert(this.innerHTML);">1</div>
==>'1'
5.4.1.2 用對象特性寫事件處理函數
<div id="J_Demo1">2</div>
<script type="text/javascript">
    document.getElementById("J_Demo1").onclick = function () {
        alert(this.innerHTML);//2
    };
</script>

5.4.2 addEventListener & attachEvent

其實說這2個方法對理解function的this有點跑偏,可是仍是要標記下。

2這不一樣的是addEventListener綁定事件處理函數後函數的this指向這個節點對象,attachEvent指向window(attachEvent存在於<=IE8)。

6 Test

test1

有時候咱們想這樣作

var $=document.getElementById;//引用這個方法
$("J_Head");//Illegal invocation 非法調用

爲何會這樣呢?由於咱們調用getElementById的方式不對。$()執行是getElementById的中的this指向的是window,用document.getElementById方式調用getElementById,其this指向的是document,緣由已經說過了。因此...

$.apply(document,["login-container"]);//指定this指向對象就ok了
(1,document.getElementById)();//Illegal invocation,連續運算符將getElementById已經從document中取出,執行時this指向null,進而指向global
相關文章
相關標籤/搜索