最近在看湯姆大叔的"深入理解JavaScript系列",寫得真的不錯,對於我而言特別是12章到19章,因為大叔研究的點,就主要是從底層來研究JavaScript為什麼會出現鐘種特有的語言現象,所以學習了大叔的文章後,自己對JavaScript的認知也更明白了,以前好多地方是知其然而不知其所以然,你要 ...
最近在看湯姆大叔的"深入理解JavaScript系列",寫得真的不錯,對於我而言特別是12章到19章,因為大叔研究的點,就主要是從底層來研究JavaScript為什麼會出現鐘種特有的語言現象,所以學習了大叔的文章後,自己對JavaScript的認知也更明白了,以前好多地方是知其然而不知其所以然,你要問我JavaScript為什麼會出現這些現象,我也只能說這是它語言本身的特性嘛。
以下是我初次看了大叔Javascript系列(12到19章)一時不能理解的點,但經過細細品味後才豁然開朗。
所以,自我總結如下:
註:總結中摻雜了個人的觀點以及理解層度,所以有什麼錯誤的地方,還請不吝指教。
一、深入理解JavaScript系列(12)之變數對象: |
在大叔這章中,大叔提到了一個概念就是‘變數對象(varibale object)’。
‘變數對象’,是與執行上下文有關的,因為JavaScript在執行表達式時,總得知道相應的變數存儲在哪吧?不然怎麼獲取或改變對應的變數值呢?
所以引入了一個‘變數對象’的概念。
都說了是對象嘛,就是以鍵值對的方式,存儲到變數對象中咯。
1、 在進入執行上下文時,‘變數對象’VO:
(1)會將函數的所有形參(如果我們是在函數執行上下文),以形參名和其對應的值作為變數對象的屬性,如果形參沒有對應的值,就是undefined咯。
(2)會將所有函數聲明,以函數名和對應的函數對象作為變數對象的屬性。如果,變數對象中已經存在了相同名稱的屬性,就完全替代。
(3)會將所有變數聲明,以變數名和其對應值(undefined)作為變數對象的屬性。如果變數名稱與上述(1)、(2)中的形參名或函數名撞車了,則變數聲明不會去影響 存在的這類屬性。
且,在上面提到,變數對象與執行上下文有關,那麼它們究竟什麼關係呢?
分兩種情況:
一種情況就是在進入任何執行上下文之前就創建的對象,此乃全局對象(Global object)global;
另一種就是,我們都知道在JavaScript中作用域是以函數function為基準的,所以函數的變數對象其實就是執行上下文對象;
具體見以下兩幅圖:
圖一
圖二
註:函數中的變數對象是不能訪問的。
那為什麼全局中的變數對象能訪問呢?
因為可以通過this或者window,因為window是全局變數global的一個屬性,且引用了global。這也就是為什麼在全局變數中訪問標識符時,用this或者直接訪問標識符時,會比用window快的原因。
2、 在執行代碼時
變數對象VO,已經在進入上下文時,做了預處理。So,接下來就是根據具體的代碼改變對應的值了。
二、深入理解JavaScript系列(13)之This: |
說到this,簡單點嘛就是由調用者決定的,誰調用的就是誰。
如下:
function fn(){ console.log(this); }; var foo = { bar: fn }; //輸出的this指向foo foo.bar();
但JavaScript底層到底是個怎樣的處理機制呢?
在大叔的這章中,引入了一個‘引用類型(Reference type)’的概念。
引用類型(Reference type),使用偽代碼可以將其表示為擁有兩個屬性的對象:
----base,即擁有屬性的那個對象;
----propertyName,即屬性名,從而可以獲取相應的值。
如下:
且,要返回引用類型的值,只存在兩種情況:
1、 處理一個標識符時;
2、 處理屬性訪問器( . 或 [ ] )時.
註意:只有兩種情況哦,要從引用類型中得到一個屬性值嘛,還需要一步,就是底層調用GetValue方法,從‘引用類型’對象中得到對應屬性值的值。
咦,講了這麼多和this有什麼相關?
直接摘至大叔:
好了,如果理解了上面的流程,下麵的幾個例子中this也就OK啦。
'use strict'; var foo = { bar: function(){ console.log(this); } }; foo.bar();//this為foo
(foo.bar)();//this為foo (foo.bar = foo.bar)();//this為undefined (false || foo.bar)();//this為undefined (foo.bar, foo.bar)();//this為undefined
三、深入理解JavaScript系列(14)之作用域鏈: |
作用域鏈是上下文所有‘變數對象(varibale object)’的列表,提到‘變數對象’,so此鏈用來變數查詢。且函數上下文的作用域鏈在函數調用時創建,包含活動對象和這個函數內部的[[scope]]屬性,而這個[[scope]]屬性是所有父變數對象的層級鏈,在函數創建時存在其中,且不會改變,即,函數一旦創建了,無論你調或不調用,[[scope]]已存儲在函數對象中了。
當函數被調用時,進入執行上下文activeExecutionContext,其中包含Scope屬性,即作用域鏈。
作用域鏈(Scope)在上下文中具體見下:
activeExecutionContext = { VO:{...},//or AO this: thisValue, Scope/*Scope chain*/: [ AO + [[Scope]] ] }
Scope又包含變數對象和函數的 [[scope]]屬性。
當我們解析一個標識符(標識符是變數名,函數名,函數參數名和全局對象中未識別的屬性名)時,解析過程將沿著這條鏈(Scope)去查找,查到就返回它的值,如果在Scope這條鏈中,沒有找到相應的值,就會沿著全局對象這條原型鏈去查找,因為函數活動對象沒有原型。
例子如下:
'use strict'; function foo(){ function bar(){ console.log(x); };
bar(); }; Object.prototype.x = 10; this.__proto__.x = 200; foo();//x為200
另外:通過函構造函數創建的函數的[[scope]]屬性總是唯一的全局對象.
四、深入理解Javascript系列(15)之函數: |
函數聲明,在‘變數對象’章中已經知道,在進入執行上下文時,會將所有的函數聲明以鍵值對的形式存儲到變數對象VO中。
但,
函數表達式不會添加到變數對象VO中,且在代碼執行階段創建,用完後立刻銷毀。命名函數表達式也一樣哦,因為它是表達式嘛。
例如:
<!DOCTYPE html> <head> <title>JavaScript</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> </head> <body> <script> 'use strict'; (function foo(x){ console.log(x); }(1)); foo(2);//此時會報錯:foo is not defined </script> </body> </html>
執行以上代碼,chrome效果圖如下:
和函數表達式不一樣麽,用完即刻銷毀。那命名函數表達式有什麼用呢?
命名函數表達式,可以通過名稱遞歸自己嘛。
但,剛纔不是說命名函數表達式和函數表達式都不會添加到變數對象VO中嗎?那它怎麼通過名稱自己調用自己的?
當解釋器在代碼執行階段,遇到命名的函數表達式,解釋器將創建一個輔助的特定對象,並添加到當前作用域鏈的最頂端。然後創建函數表達式,然後將命名函數表達式的名字添加到這個輔助的特定對象中,且值為該函數引用,當命名函數表達式在其自身調用時,它就在這個特定的對象中找到自己。最後,當命名函數表達式執行完成後,從父作用域鏈中移除那個輔助的特定對象。
具體演算法見下:
五、深入理解JavaScript系列(16)之閉包: |
因為作用域鏈,從而使得所有的函數都是閉包。
但,有一類函數比較特殊,那就是通過Function構造器創建的函數,因為其[[Scope]]只包含全局對象。
ECMAScript中,閉包指的是:
1、從理論角度:所有的函數。
因為它們都在創建的時候,就將上層上下文的數據保存起來了。哪怕是最簡單的全局變數也是如此,因為函數中訪問的全局變數就相當於是訪問自由變數,這個時候使用最外層 的作用域。
2、從實踐角度:以下函數才算閉包:
(1)、即時創建它的上下文已經銷毀,它任然存在(比如,內部函數從父函數中返回);
(2)、在代碼中引用了自由變數。
給出一段經典的代碼:
var data = []; for( var k = 0; k < 3; k++){ data[k] = function(){ alert(k); }; }; data[0]();// 3,而不是0 data[1]();// 3,而不是1 data[2]();// 3,而不是2
常用的解決方法是,通過閉包,如下:
var data = []; for( var k = 0; k < 3; k++){ data[k] = (function _helper(x){ return function(){ alert(x); }; })(k);//傳入"k"值 }; //現在結果正確了 data[0]();// 0 data[1]();// 1 data[2]();// 2
除了閉包,我們還可以怎麼解決呢?
如下:
var data = []; for( var k = 0; k < 3; k++){ (data[k] = function(){ alert(arguments.callee.x); }).x = k;//將k作為函數的一個屬性 }; //結果也是對的 data[0]();// 0 data[1]();// 1 data[2]();// 2
但是arguments.callee在ECMAScript5中的嚴格模式下是不能用的,所以我們可以用命名函數表達式來做。
如下:
var data = []; for( var k = 0; k < 3; k++){ (data[k] = function foo(){ alert(foo.x); }).x = k;//將k作為函數的一個屬性 }; //結果也是對的 data[0]();// 0 data[1]();// 1 data[2]();// 2
六、其他: |
ECMAScript中將對象作為參數傳遞的策略——按共用傳遞:修改參數的屬性將會影響到外部,而重新賦值將不會影響到外部對象。
其實不僅是參數傳遞,其他都是這樣的策略(對象都是賦予地址值)。
如下:
var foo = {}; //b其實是添加到function對象中的,而不是foo.a中的,因為foo.a只是引用了function,即得到了它的地址而已 (foo.a = function(){ }).b = 10; //得到10 console.log(foo.a.b); //將foo.a賦值予變數c var c = foo.a; //改變foo.a的值,如一個空對象 foo.a = {}; //輸出c.b,還是得到10 console.log(c.b);
數組也是一個對象,所以如果我將數組中的元素指向帶有this的函數,那麼其指向的是數組對象。
//聲明變數arr,並賦值為數組 var arr = []; //給arr數組的第一二個元素賦值 arr[0] = function(){ console.log(this); //因為this指向的是arr對象,所以this['1']或this[1],就相當於arr[1] this['1'](); }; arr[1] = function(){ console.log("I'm 1 "); }; //this指向的是arr對象 arr[0]();
好了,時間也不早了,晚安~