前言:
在我看來javascript是一門非常鬆散靈活的語言,鬆散並不是貶義,而恰恰相反,是對js的一種讚美。javascript相對於c#等靜態編譯語言來說,就像一個騎單車已經十分熟練的頑皮孩子,已經不再需要遵循上車必須從車右邊扶車把,左腳踏踏板右腳蹬地幾下的標準動作了,他可以做出若干不同的上車花式,但始終不會背離平衡這一原則。正式這樣的靈活性甚至看上去有點怪異的行為令javascript與c#這些語言表現格格不入:它是物件導向,卻沒有類,能繼承又搞出個prototype,“this”的神出鬼沒難以捉摸,閉包,範圍,弱類型等概念更是讓人摸不著頭腦。
但是請不要忘記,我們覺得js怪異只是因為我們早已習慣類C語言的思維,js還是一門電腦語言,就像那個愛玩花式的騎車小男孩沒有背離“平衡”這一原則一樣,js並沒有背離電腦語言的一個原則(就跟其他電腦語言一樣):控制電腦去做事 。
從另一個角度來說,正是因為js的靈活與怪異,我們更應該像學其他靜態語言一樣深入學習它。在“學習另一種頗為不同的語言”的原則指導下, 深入學習js還是很有價值的。學習js能令人從一種與c# java非常不同的思維去認識電腦語言和編程,去認識一些萬變不離其宗的核心東西和思想。
在我學習javascript的過程中,很自然地對很多概念迷惑不解,網上雖然有很多“深入理解javascript XXX”的教程,但始終覺得不夠深入,就像你問別人棧是什麼,別人只告訴你“先進後出”一樣不痛不癢。
但幸運的是,前幾天終於發現了俄國人Dmitry Soshnikov寫的一個系列部落格教程: 《ECMA-262-3 in detail》,在園子裡也有精彩的“官方翻譯”:http://www.cnblogs.com/justinw/archive/2010/04/16/1713086.html。這系列部落格真的是非常的有用,從ECMA規範的角度來解析js更加之深入和徹底,而且涵蓋了js進階的所有話題。但雖然該系列部落格寫的非常好,例子恰當,但畢竟其描述的都是js中比較進階的內容,因此不免有很多點很難馬上弄懂,需要反覆閱讀斟酌,因此本編部落格便是我在學習的過程中的筆記,隨時記錄一些心得和其他需要銘記的點。
第一章:Execution Contexts
函數遞迴調用的時候也會產生execution context
execution context是邏輯堆棧結構
第二章:Variable Object
即使是在永遠不會被執行的else分支中聲明的變數,或者function定義,也會被存放在vo中
function中的vo叫做Activation Object(AO) ,而global中的vo也就是global,VO只是泛稱。
scope chain=list of parent's VOs + function's own AO
另外,有一個現象可能跟一句話有關:An activation object is created on entering the context of a function and initialized by propertyarguments
which value is the Arguments object:
這個現象就是:如果一個函數中聲明一個變數,且該變數的名稱與函數的形參的名稱相同,那麼,這個變數和這個形參其實是同一個東西,看例子:
function foo(i){
var i=5;
alert(arguments[0]);
}
foo(10);//5,而不是10
而反過來也成立:
function foo(i){
var i;
arguments[0]=10
alert(i);
}
foo(5);//10,而不是5
至於底層的實現是不是就是這樣現在還在研究中,但好像以這樣來解析也行得通。
第三章:this.
Identifier 是變數名,函數名(例如 function foo(){}中的"foo",函數參數名及在global中未識別的屬性)
參考型別只出現在兩種情況:處理Identifier的時候,和屬性訪問器。屬性訪問器就是"."和"[]",例如"obj.foo"和"obj["foo"]"
參考型別的base屬性指向該對象的所有者
第四章:Scope chain.
“scope”其實就是[[scope]],通常說scope的時候是指函數對象的[[scope]]屬性,而 scope chain是指函數對象中[[scope]]鏈表(當然不一定就是鏈表結構)
[[scope]]是在一個函數建立的時候建立的,而不是在一個函數被執行的時候(進入execution context)時候建立,並且並不會在函數執行其中改變,因此,才有閉包的機制:
var x=10;
function a(){
alert(x);
}
function b(){
var x=20;
a();
}
b();//10,而不是20
第六章:閉包
現在要解析閉包有點難,因為要說清楚閉包就必須要搞清楚scope/scope chain和 context,反正和前幾章都有關聯,要詳細解析閉包可能還要寫一遍比較完整的筆記。現在有些零碎的理解,先記下來吧,反正會有用到,先看一個閉包的經典例子:
function foo(){
var x=10;
return function(){
alert(x);
}
}
var bar=foo();
bar();//10
如果不是閉包的話,按邏輯來說,x是foo中的局部變數,當foo執行完返回的時候,x已經被釋放掉。那麼,從表象的層面來解析,閉包就是在函數返回後仍能訪問該函數內部資料的一種機制,從原理來說,就是被調用的函數(這裡是foo中的那個匿名函數)的scope包含了外部函數(foo)的資料(“資料”一詞是故意用的,這樣說比較籠統,因為我還不清楚這應該是什麼,難道是VO?遲些再研究一下),而從範圍的層面去看,就是因為“js是基於靜態範圍”的,所以《ecmascript in detail》中有一句話:Referencing to algorithm of functions creation, we see that all functions in ECMAScript are closures, since all of them at creation save scope chain of a parent context.(ecmascript中所有函數都是閉包)
這裡必須看看“動態範圍”和“靜態範圍”。先看一個例子:
//假定這裡是global
var x=5;
function foo(){
alert(x);
}
function bar(){
var x=10;
foo();
}
bar();//5,而不是10
靜態範圍,有些地方稱詞法範圍,是指:一個範圍在函數建立的時候,就已經確定好了,而不是在執行的時候。也可以用另外一句話說:函數範圍是在原始碼中確定的。
通過上面例子來看看這個“原始碼中確定“是什麼意思:上面例子中,調用bar的時候,進入bar的上下文,bar裡面聲明了一個x,並賦值x=10,然後調用foo,如果是動態範圍,也就是在代碼執行時刻確定範圍的話,那麼,foo裡面被alert的x應該是bar裡面的x,也就是10,但是在靜態範圍的原理下”函數範圍是在原始碼中確定的”,也就是說,調用foo的時候,解析引擎會回到foo定義的地方(原始碼的前4句)去找,而在這裡,很明顯,x是第一句原始碼定義的那個值為5的x,而且在foo被定義的原始碼中,值為10的x都還沒有出世~~~。這個就是我自己對“靜態範圍”的理解,不過對於動態範圍我理解不多,有空看看維基百科上關於動態範圍。
在這裡順便提一下,C語言也是靜態範圍:
#include "stdafx.h"
int x=5;
void fun1(){
printf("%d",x);
}
void fun2()
{
int x=10;
fun1();
}
int _tmain(int argc, _TCHAR* argv[])
{
fun2();//5
scanf("%d",&x);
}
ecmascript只使用靜態範圍(文法範圍)
ECMAScript中,閉包指的是:
從理論角度:所有的函數。因為它們都在建立的時候就將上層內容相關的資料儲存起來了。哪怕是簡單的全域變數也是如此,因為函數中訪問全域變數就相當於是在訪問自由變數,這個時候使用最外層的範圍。從實踐角度:以下函數才算是閉包:1.即使建立它的上下文已經銷毀,它仍然存在(比如,內建函式從父函數中返回)2.在代碼中引用了自由變數