參考資料javascript
代碼的執行所處的環境,也叫執行上下文,它肯定了代碼的做用域,做用域鏈,this屬性,代碼的生存期等等,讓咱們從解釋器的角度看代碼是如何執行的。
EC能夠用以下的數據結構表達,它有不少屬性,VO,[[scope]],this等等。html
EC={ Variable Object:...., [[scope]]:..., this:... }
一段JS代碼進入解釋器到執行分爲2步驟,這2步各自有各自的事要處理java
咱們聲明的變量,聲明的函數,傳入的參數都放到哪裏去了?引擎是在哪裏尋找它們的?其實它們都放入到一個叫VO的對象裏去了,能夠說了解VO是很重要的。VO的數據結構能夠以下表達chrome
VO={ 聲明的變量, 聲明的函數, 參數(函數內部VO擁有) }
ECMAScript是經過上下文來區分的,若是function foo(){}是做爲賦值表達式的一部分的話,那它就是一個函數表達式,反之爲函數聲明式。數組
function foo(){} // 聲明 var bar = function foo(){}; // 表達式,由於它是賦值表達式的一部分 new function bar(){}; // 表達式,由於它是new表達式 (function(){ function bar(){} // 聲明 })(); (function foo(){}); // 函數表達式:包含在分組操做符()內,而分組符裏的表達式
注意:根據表達式的生成函數的函數名稱是不會放入函數所處的EC的VO中的。瀏覽器
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分類。閉包
全部在global聲明的函數,變量都會在global的VO中存在。app
global.vo = { Math: <...>, String: <...> ... ... window: global //引用自身 };
當進入執行上下文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
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
總結:
[[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];
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,
從本質上了解了做用域鏈,就很容易理解閉包了。
當函數內部定義了其餘函數,就建立了閉包,閉包(子函數)有權訪問父級函數的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的內存吧。
this是在代碼進入執行環境時確認的,因此按代碼進入執行環境時所在說明this更爲清晰。
代碼在global中執行,this永遠都是global。
函數的EC中的this是由函數調用的方式來肯定的。
this指向這些函數的第一個參數
var sth = "global"; function f() { alert(this.sth); } f();//global var o = {sth:"o"}; f.apply(o);//o f.call(o);//o
這是的函數叫構造函數
執行時this的值取決於()左邊的值所屬的對象
若是函數被引用,那麼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 連續運算符還是同上
this指向null,可是瀏覽器不會讓你這麼幹,它會把null變爲global。
注:第5版的ECMAScript中,已經不強迫轉換成全局變量了,而是賦值爲undefined。
(function (){ alert(this);//window })();
eval('alert(this)');//window
以這個概念註冊事件處理函數有2種實現方法,可是異曲同工——都是給節點對象的事件屬性註冊事件處理函數。
<div onclick="alert(this.innerHTML);">1</div> ==>'1'
<div id="J_Demo1">2</div> <script type="text/javascript"> document.getElementById("J_Demo1").onclick = function () { alert(this.innerHTML);//2 }; </script>
其實說這2個方法對理解function的this有點跑偏,可是仍是要標記下。
2這不一樣的是addEventListener綁定事件處理函數後函數的this指向這個節點對象,attachEvent指向window(attachEvent存在於<=IE8)。
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