利用了一個下午的時間,將原型和閉包這塊的知識去了解了一下,做了些筆記和總結(jié),感興趣的童鞋可以移步王福朋的博客,寫的很清楚明了~
首先咱們還是先看看javascript中一個常用的運算符——typeof。typeof應(yīng)該算是咱們的老朋友,還有誰沒用過它?
typeof函數(shù)輸出的一共有幾種類型,在此列出:
console.log(typeof x); // undefined
console.log(typeof 10); // number
console.log(typeof 'abc'); // string
console.log(typeof true); // boolean
console.log(typeof function () {}); //function
console.log(typeof [1, 'a', true]); //object
console.log(typeof { a: 10, b: 20 }); //object
console.log(typeof null); //object
console.log(typeof new Number(10)); //object
}
show();
以上代碼列出了typeof輸出的集中類型標識,其中上面的四種(undefined, number, string, boolean)屬于簡單的值類型,不是對象。剩下的幾種情況——函數(shù)、數(shù)組、對象、null、new Number(10)都是對象。他們都是引用類型。
判斷一個變量是不是對象非常簡單。值類型的類型判斷用typeof,引用類型的類型判斷用instanceof。
var fn = function () { };
console.log(fn instanceof Object); // true
對象——若干屬性的集合。
java或者C#中的對象都是new一個class出來的,而且里面有字段、屬性、方法,規(guī)定的非常嚴格。但是javascript就比較隨意了——數(shù)組是對象,函數(shù)是對象,對象還是對象。對象里面的一切都是屬性,只有屬性,沒有方法。那么這樣方法如何表示呢?——方法也是一種屬性。因為它的屬性表示為鍵值對的形式。
而且,更加好玩的事,javascript中的對象可以任意的擴展屬性,沒有class的約束。這個大家應(yīng)該都知道,就不再強調(diào)了。
先說個最常見的例子:

以上代碼中,obj是一個自定義的對象,其中a、b、c就是它的屬性,而且在c的屬性值還是一個對象,它又有name、year兩個屬性。
一切(引用類型)都是對象,對象是屬性的集合
函數(shù)是一種對象,但是函數(shù)卻不像數(shù)組一樣——你可以說數(shù)組是對象的一種,因為數(shù)組就像是對象的一個子集一樣。但是函數(shù)與對象之間,卻不僅僅是一種包含和被包含的關(guān)系,函數(shù)和對象之間的關(guān)系比較復(fù)雜,甚至有一點雞生蛋蛋生雞的邏輯,咱們這一節(jié)就縷一縷。
還是先看一個小例子吧。
function Fn() {
this.name = '王福朋';
this.year = 1988;
}
var fn1 = new Fn();
上面的這個例子很簡單,它能說明:對象可以通過函數(shù)來創(chuàng)建。對!也只能說明這一點。
但是我要說——對象都是通過函數(shù)創(chuàng)建的——有些人可能反駁:不對!因為:
var obj = { a: 10, b: 20 };
var arr = [5, 'x', true];
但是不好意思,這個——真的——是一種——“快捷方式”,在編程語言中,一般叫做“語法糖”。
做“語法糖”做的最好的可謂是微軟大哥,它把他們家C#那小子弄的不男不女從的,本想圖個人見人愛,誰承想還得到處跟人解釋——其實它是個男孩!
話歸正傳——其實以上代碼的本質(zhì)是:
//var obj = { a: 10, b: 20 };
//var arr = [5, 'x', true];
var obj = new Object();
obj.a = 10;
obj.b = 20;
var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;
而其中的 Object 和 Array 都是函數(shù):
console.log(typeof (Object)); // function
console.log(typeof (Array)); // function
所以,可以很負責任的說
對象都是通過函數(shù)來創(chuàng)建的
現(xiàn)在是不是糊涂了—— 對象是函數(shù)創(chuàng)建的,而函數(shù)卻又是一種對象——天哪!函數(shù)和對象到底是什么關(guān)系???
別著急!揭開這個謎底,還得先去了解一下另一位老朋友——prototype原型。
之前說道,函數(shù)也是一種對象。他也是屬性的集合,你也可以對函數(shù)進行自定義屬性。
不用等咱們?nèi)ピ囼?,javascript自己就先做了表率,人家就默認的給函數(shù)一個屬性——prototype。對,每個函數(shù)都有一個屬性叫做prototype。
這個prototype的屬性值是一個對象(屬性的集合,再次強調(diào)!),默認的只有一個叫做constructor的屬性,指向這個函數(shù)本身。

如上圖,SuperType是是一個函數(shù),右側(cè)的方框就是它的原型。
原型既然作為對象,屬性的集合,不可能就只弄個constructor來玩玩,肯定可以自定義的增加許多屬性。例如這位Object大哥,人家的prototype里面,就有好幾個其他屬性。

當然,你也可以在自己自定義的方法的prototype中新增自己的屬性
function Fn() { }
Fn.prototype.name = '王福朋';
Fn.prototype.getYear = function () {
return 1988;
};
看到?jīng)]有,這樣就變成了

如果用咱們自己的代碼來演示,就是這樣
function Fn() { }
Fn.prototype.name = '王福朋';
Fn.prototype.getYear = function () {
return 1988;
};
var fn = new Fn();
console.log(fn.name);
console.log(fn.getYear());
即,F(xiàn)n是一個函數(shù),fn對象是從Fn函數(shù)new出來的,這樣fn對象就可以調(diào)用Fn.prototype中的屬性。
因為每個對象都有一個隱藏的屬性——“proto”,這個屬性引用了創(chuàng)建這個對象的函數(shù)的prototype。即:fn.proto === Fn.prototype
這里的"proto"成為“隱式原型”。
上面已經(jīng)提到,每個函數(shù)function都有一個prototype,即原型。這里再加一句話
每個對象都有一個proto,可成為隱式原型。
這個proto是一個隱藏的屬性,javascript不希望開發(fā)者用到這個屬性值,有的低版本瀏覽器甚至不支持這個屬性值。所以你在Visual Studio 2012這樣很高級很智能的編輯器中,都不會有proto的智能提示,但是你不用管它,直接寫出來就是了。
var obj = {};
console.log(obj.__proto__);
obj這個對象本質(zhì)上是被Object函數(shù)創(chuàng)建的,因此obj.proto=== Object.prototype。我們可以用一個圖來表示。

即,每個對象都有一個proto屬性,指向創(chuàng)建該對象的函數(shù)的prototype。
那么上圖中的“Object prototype”也是一個對象,它的proto指向哪里?
好問題!
在說明“Object prototype”之前,先說一下自定義函數(shù)的prototype。自定義函數(shù)的prototype本質(zhì)上就是和 var obj = {} 是一樣的,都是被Object創(chuàng)建,所以它的proto指向的就是Object.prototype。
但是
Object.prototype確實一個特例——它的proto指向的是null。
切記切記!

還有——函數(shù)也是一種對象,函數(shù)也有proto嗎?
又一個好問題!——當然有。
函數(shù)也不是從石頭縫里蹦出來的,函數(shù)也是被創(chuàng)建出來的。誰創(chuàng)建了函數(shù)呢?——Function——注意這個大寫的“F”。
且看如下代碼。
function fn(x,y){
return x+y;
};
console.log(fn(10,20));
var fn1 = new Function("x","y","return x+y;");
console.log(fn1(5,6));
以上代碼中,第一種方式是比較傳統(tǒng)的函數(shù)創(chuàng)建方式,第二種是用new Functoin創(chuàng)建。
首先根本不推薦用第二種方式。
這里只是向大家演示,函數(shù)是被Function創(chuàng)建的。
好了,根據(jù)上面說的一句話——對象的proto指向的是創(chuàng)建它的函數(shù)的prototype,就會出現(xiàn):Object.proto === Function.prototype。用一個圖來表示。

上圖中,很明顯的標出了:自定義函數(shù)Foo.proto指向Function.prototype,Object.proto指向Function.prototype,唉,怎么還有一個……Function.proto指向Function.prototype?這不成了循環(huán)引用了?
對!是一個環(huán)形結(jié)構(gòu)。
其實稍微想一下就明白了。Function也是一個函數(shù),函數(shù)是一種對象,也有proto屬性。既然是函數(shù),那么它一定是被Function創(chuàng)建。所以——Function是被自身創(chuàng)建的。所以它的proto指向了自身的Prototype。
最后一個問題:Function.prototype指向的對象,它的proto是不是也指向Object.prototype?
答案是肯定的。因為Function.prototype指向的對象也是一個普通的被Object創(chuàng)建的對象,所以也遵循基本的規(guī)則。

對于值類型,你可以通過typeof判斷,string/number/boolean都很清楚,但是typeof在判斷到引用類型的時候,返回值只有object/function,你不知道它到底是一個object對象,還是數(shù)組,還是new Number等等。
這個時候就需要用到instanceof。例如:
function Foo(){}
var f1 = new Foo();
console.log(f1 instanceof Foo);//true
console.log(f1 instanceof Object);//true
上圖中,f1這個對象是被Foo創(chuàng)建,但是“f1 instanceof Object”為什么是true呢?
至于為什么過會兒再說,先把instanceof判斷的規(guī)則告訴大家。根據(jù)以上代碼看下圖:

Instanceof運算符的第一個變量是一個對象,暫時稱為A;第二個變量一般是一個函數(shù),暫時稱為B。
Instanceof的判斷隊則是:
沿著A的proto這條線來找,同時沿著B的prototype這條線來找,如果兩條線能找到同一個引用,即同一個對象,那么就返回true。如果找到終點還未重合,則返回false。
按照以上規(guī)則,大家看看“ f1 instanceof Object ”這句代碼是不是true? 根據(jù)上圖很容易就能看出來,就是true。
通過上以規(guī)則,你可以解釋很多比較怪異的現(xiàn)象,例如:
console.log(Object instanceof Function);//true
console.log(Function instanceof Object);//true
console.log(Function instanceof Function);//true
這些看似很混亂的東西,答案卻都是true,這是為何?
上一節(jié)咱們貼了好多的圖片,其實那些圖片是可以聯(lián)合成一個整體的,即:

看這個圖片,千萬不要嫌煩,必須一條線一條線挨著分析。如果上一節(jié)你看的比較仔細,再結(jié)合剛才咱們介紹的instanceof的概念,相信能看懂這個圖片的內(nèi)容。
看看這個圖片,你也就知道為何上面三個看似混亂的語句返回的是true了。
問題又出來了。Instanceof這樣設(shè)計,到底有什么用?到底instanceof想表達什么呢?
重點就這樣被這位老朋友給引出來了——繼承——原型鏈。
即,instanceof表示的就是一種繼承關(guān)系,或者原型鏈的結(jié)構(gòu)。
javascript中的繼承是通過原型鏈來體現(xiàn)的。先看幾句代碼
function Foo(){}
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a);//10
console.log(f2.b);//200
以上代碼中,f1是Foo函數(shù)new出來的對象,f1.a是f1對象的基本屬性,f1.b是怎么來的呢?——從Foo.prototype得來,因為f1.proto指向的是Foo.prototype
訪問一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿著proto這條鏈向上找,這就是原型鏈。

上圖中,訪問f1.b時,f1的基本屬性中沒有b,于是沿著proto找到了Foo.prototype.b。
那么我們在實際應(yīng)用中如何區(qū)分一個屬性到底是基本的還是從原型中找到的呢?大家可能都知道答案了——hasOwnProperty,特別是在for…in…循環(huán)中,一定要注意。

等等,不對! f1的這個hasOwnProperty方法是從哪里來的? f1本身沒有,F(xiàn)oo.prototype中也沒有,哪兒來的?
好問題。
它是從Object.prototype中來的,請看圖:

對象的原型鏈是沿著proto這條線走的,因此在查找f1.hasOwnProperty屬性時,就會順著原型鏈一直查找到Object.prototype。
由于所有的對象的原型鏈都會找到Object.prototype,因此所有的對象都會有Object.prototype的方法。這就是所謂的“繼承”。
當然這只是一個例子,你可以自定義函數(shù)和對象來實現(xiàn)自己的繼承。
說一個函數(shù)的例子吧。
我們都知道每個函數(shù)都有call,apply方法,都有l(wèi)ength,arguments,caller等屬性。為什么每個函數(shù)都有?這肯定是“繼承”的。函數(shù)由Function函數(shù)創(chuàng)建,因此繼承的Function.prototype中的方法。不信可以請微軟的Visual Studio老師給我們驗證一下:

看到了吧,有call、length等這些屬性。
那怎么還有hasOwnProperty呢?——那是Function.prototype繼承自O(shè)bject.prototype的方法。
我們總結(jié)一下,在“準備工作”中完成了哪些工作:
變量、函數(shù)表達式——變量聲明,默認賦值為undefined;
this——賦值;
函數(shù)聲明——賦值;
這三種數(shù)據(jù)的準備情況我們稱之為“執(zhí)行上下文”或者“執(zhí)行上下文環(huán)境”。
其實,javascript在執(zhí)行一個代碼段之前,都會進行這些“準備工作”來生成執(zhí)行上下文。這個“代碼段”其實分三種情況——全局代碼,函數(shù)體,eval代碼。
這里解釋一下為什么代碼段分為這三種。
所謂“代碼段”就是一段文本形式的代碼。
首先,全局代碼是一種,這個應(yīng)該沒有非議,本來就是手寫文本到<script>標簽里面的。

其次,eval代碼接收的也是一段文本形式的代碼。

最后,函數(shù)體是代碼段是因為函數(shù)在創(chuàng)建時,本質(zhì)上是 new Function(…) 得來的,其中需要傳入一個文本形式的參數(shù)作為函數(shù)體。

這樣解釋應(yīng)該能理解了。
最后,eval不常用,也不推薦大家用。
全局代碼的上下文環(huán)境數(shù)據(jù)內(nèi)容為:
| 普通變量(包括函數(shù)表達式),如: var a = 10; | 聲明(默認賦值為undefined) |
|---|---|
| 函數(shù)聲明,如: function fn() { } | 賦值 |
| this | 賦值 |
| 參數(shù) | 賦值 |
|---|---|
| arguments | 賦值 |
| 自由變量的取值作用域 | 賦值 |
給執(zhí)行上下文環(huán)境下一個通俗的定義
在執(zhí)行代碼之前,把將要用到的所有的變量都事先拿出來,有的直接賦值了,有的先用undefined占個空。
this的取值,分四種情況。我們來挨個看一下。
在此再強調(diào)一遍一個非常重要的知識點
在函數(shù)中this到底取何值,是在函數(shù)真正被調(diào)用執(zhí)行的時候確定的,函數(shù)定義的時候確定不了。
因為this的取值是執(zhí)行上下文環(huán)境的一部分,每次調(diào)用函數(shù),都會產(chǎn)生一個新的執(zhí)行上下文環(huán)境。
情況1:構(gòu)造函數(shù)
所謂構(gòu)造函數(shù)就是用來new對象的函數(shù)。其實嚴格來說,所有的函數(shù)都可以new一個對象,但是有些函數(shù)的定義是為了new一個對象,而有些函數(shù)則不是。另外注意,構(gòu)造函數(shù)的函數(shù)名第一個字母大寫(規(guī)則約定)。例如:Object、Array、Function等。

以上代碼中,如果函數(shù)作為構(gòu)造函數(shù)用,那么其中的this就代表它即將new出來的對象。
注意,以上僅限new Foo()的情況,即Foo函數(shù)作為構(gòu)造函數(shù)的情況。如果直接調(diào)用Foo函數(shù),而不是new Foo(),情況就大不一樣了。

這種情況下this是window,我們后文中會說到。
情況2:函數(shù)作為對象的一個屬性
如果函數(shù)作為對象的一個屬性時,并且作為對象的一個屬性被調(diào)用時,函數(shù)中的this指向該對象。

以上代碼中,fn不僅作為一個對象的一個屬性,而且的確是作為對象的一個屬性被調(diào)用。結(jié)果this就是obj對象。
注意,如果fn函數(shù)不作為obj的一個屬性被調(diào)用,會是什么結(jié)果呢?

如上代碼,如果fn函數(shù)被賦值到了另一個變量中,并沒有作為obj的一個屬性被調(diào)用,那么this的值就是window,this.x為undefined。
情況3:函數(shù)用call或者apply調(diào)用
當一個函數(shù)被call和apply調(diào)用時,this的值就取傳入的對象的值。至于call和apply如何使用,不會的朋友可以去查查其他資料,本系列教程不做講解。

情況4:全局 & 調(diào)用普通函數(shù)
在全局環(huán)境下,this永遠是window,這個應(yīng)該沒有非議。

普通函數(shù)在調(diào)用時,其中的this也都是window。

以上代碼很好理解。
不過下面的情況你需要注意一下:

函數(shù)f雖然是在obj.fn內(nèi)部定義的,但是它仍然是一個普通的函數(shù),this仍然指向window。
情況五:構(gòu)造函數(shù)中的prototype
在構(gòu)造函數(shù)的prototype中,this代表著什么。

如上代碼,在Fn.prototype.getName函數(shù)中,this指向的是f1對象。因此可以通過this.name獲取f1.name的值。
其實,不僅僅是構(gòu)造函數(shù)的prototype,即便是在整個原型鏈中,this代表的也都是當前對象的值。
完了。
看到了吧,this有關(guān)的知識點還是挺多的,不僅多而且非常重要。
最后,既然提到了this,有必要把一個非常經(jīng)典的案例介紹給大家,又是jQuery源碼的。

以上代碼是從jQuery中摘除來的部分代碼。jQuery.extend和jQuery.fn.extend都指向了同一個函數(shù),但是當執(zhí)行時,函數(shù)中的this是不一樣的。
執(zhí)行jQuery.extend(…)時,this指向jQuery;執(zhí)行jQuery.fn.extend(…)時,this指向jQuery.fn。
這樣就巧妙的將一段代碼同時共享給兩個功能使用,更加符合設(shè)計原則。
我們在聲明變量時,全局代碼要在代碼前端聲明,函數(shù)中要在函數(shù)體一開始就聲明好。除了這兩個地方,其他地方都不要出現(xiàn)變量聲明。而且建議用“單var”形式。
先解釋一下什么是“自由變量”。
在A作用域中使用的變量x,卻沒有在A作用域中聲明(即在其他作用域中聲明的),對于A作用域來說,x就是一個自由變量。如下圖

如上程序中,在調(diào)用fn()函數(shù)時,函數(shù)體中第6行。取b的值就直接可以在fn作用域中取,因為b就是在這里定義的。而取x的值時,就需要到另一個作用域中取。到哪個作用域中取呢?
有人說過要到父作用域中取,其實有時候這種解釋會產(chǎn)生歧義。例如:

所以,不要在用以上說法了。相比而言,用這句話描述會更加貼切——要到創(chuàng)建這個函數(shù)的那個作用域中取值——是“創(chuàng)建”,而不是“調(diào)用”,切記切記——其實這就是所謂的“靜態(tài)作用域”。
對于本文第一段代碼,在fn函數(shù)中,取自由變量x的值時,要到哪個作用域中取?——要到創(chuàng)建fn函數(shù)的那個作用域中取——無論fn函數(shù)將在哪里調(diào)用。
上面描述的只是跨一步作用域去尋找。
如果跨了一步,還沒找到呢?——接著跨!——一直跨到全局作用域為止。要是在全局作用域中都沒有找到,那就是真的沒有了。
這個一步一步“跨”的路線,我們稱之為——作用域鏈。
我們拿文字總結(jié)一下取自由變量時的這個“作用域鏈”過程:(假設(shè)a是自由量)
第一步,現(xiàn)在當前作用域查找a,如果有則獲取并結(jié)束。如果沒有則繼續(xù);
第二步,如果當前作用域是全局作用域,則證明a未定義,結(jié)束;否則繼續(xù);
第三步,(不是全局作用域,那就是函數(shù)作用域)將創(chuàng)建該函數(shù)的作用域作為當前作用域;
第四步,跳轉(zhuǎn)到第一步。

以上代碼中:第13行,fn()返回的是bar函數(shù),賦值給x。執(zhí)行x(),即執(zhí)行bar函數(shù)代碼。取b的值時,直接在fn作用域取出。取a的值時,試圖在fn作用域取,但是取不到,只能轉(zhuǎn)向創(chuàng)建fn的那個作用域中去查找,結(jié)果找到了。
閉包
前面提到的上下文環(huán)境和作用域的知識,除了了解這些知識之外,還是理解閉包的基礎(chǔ)。
至于“閉包”這個詞的概念的文字描述,確實不好解釋,我看過很多遍,但是現(xiàn)在還是記不住。
但是你只需要知道應(yīng)用的兩種情況即可——函數(shù)作為返回值,函數(shù)作為參數(shù)傳遞。
第一,函數(shù)作為返回值

如上代碼,bar函數(shù)作為返回值,賦值給f1變量。執(zhí)行f1(15)時,用到了fn作用域下的max變量的值。至于如何跨作用域取值,可以參考上一節(jié)。
第二,函數(shù)作為參數(shù)被傳遞

如上代碼中,fn函數(shù)作為一個參數(shù)被傳遞進入另一個函數(shù),賦值給f參數(shù)。執(zhí)行f(15)時,max變量的取值是10,而不是100。
上一節(jié)講到自由變量跨作用域取值時,曾經(jīng)強調(diào)過:要去創(chuàng)建這個函數(shù)的作用域取值,而不是“父作用域”。理解了這一點,以上兩端代碼中,自由變量如何取值應(yīng)該比較簡單。(不明白的朋友一定要去上一節(jié)看看,這個很重要?。?/p>
另外,講到閉包,除了結(jié)合著作用域之外,還需要結(jié)合著執(zhí)行上下文棧來說一下。
在前面講執(zhí)行上下文棧時(http://www.cnblogs.com/wangfupeng1988/p/3989357.html),我們提到當一個函數(shù)被調(diào)用完成之后,其執(zhí)行上下文環(huán)境將被銷毀,其中的變量也會被同時銷毀。
但是在當時那篇文章中留了一個問號——有些情況下,函數(shù)調(diào)用完成之后,其執(zhí)行上下文環(huán)境不會接著被銷毀。這就是需要理解閉包的核心內(nèi)容。
咱們可以拿本文的第一段代碼(稍作修改)來分析一下。

第一步,代碼執(zhí)行前生成全局上下文環(huán)境,并在執(zhí)行時對其中的變量進行賦值。此時全局上下文環(huán)境是活動狀態(tài)。

第二步,執(zhí)行第17行代碼時,調(diào)用fn(),產(chǎn)生fn()執(zhí)行上下文環(huán)境,壓棧,并設(shè)置為活動狀態(tài)。

第三步,執(zhí)行完第17行,fn()調(diào)用完成。按理說應(yīng)該銷毀掉fn()的執(zhí)行上下文環(huán)境,但是這里不能這么做。注意,重點來了:因為執(zhí)行fn()時,返回的是一個函數(shù)。函數(shù)的特別之處在于可以創(chuàng)建一個獨立的作用域。而正巧合的是,返回的這個函數(shù)體中,還有一個自由變量max要引用fn作用域下的fn()上下文環(huán)境中的max。因此,這個max不能被銷毀,銷毀了之后bar函數(shù)中的max就找不到值了。
因此,這里的fn()上下文環(huán)境不能被銷毀,還依然存在與執(zhí)行上下文棧中。
——即,執(zhí)行到第18行時,全局上下文環(huán)境將變?yōu)榛顒訝顟B(tài),但是fn()上下文環(huán)境依然會在執(zhí)行上下文棧中。另外,執(zhí)行完第18行,全局上下文環(huán)境中的max被賦值為100。如下圖:

第四步,執(zhí)行到第20行,執(zhí)行f1(15),即執(zhí)行bar(15),創(chuàng)建bar(15)上下文環(huán)境,并將其設(shè)置為活動狀態(tài)。

執(zhí)行bar(15)時,max是自由變量,需要向創(chuàng)建bar函數(shù)的作用域中查找,找到了max的值為10。這個過程在作用域鏈一節(jié)已經(jīng)講過。
這里的重點就在于,創(chuàng)建bar函數(shù)是在執(zhí)行fn()時創(chuàng)建的。fn()早就執(zhí)行結(jié)束了,但是fn()執(zhí)行上下文環(huán)境還存在與棧中,因此bar(15)時,max可以查找到。如果fn()上下文環(huán)境銷毀了,那么max就找不到了。
使用閉包會增加內(nèi)容開銷,現(xiàn)在很明顯了吧!
第五步,執(zhí)行完20行就是上下文環(huán)境的銷毀過程,這里就不再贅述了。