深入理解javascript原型和閉包
1、一切都是對象
“一切都是對象”這句話的重點在于如何去理解“對象”這個概念。
——當然,也不是所有的都是對象,值類型就不是對象。
首先咱們還是先看看javascript中一個常用的函數——typeof()。typeof應該算是咱們的老朋友,還有誰沒用過它?
typeof函數輸出的一共有幾種類型,在此列出:
function show(x) {
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)屬于簡單的值類型,不是對象。剩下的幾種情況——函數、數組、對象、null、new Number(10)都是對象。他們都是引用類型。
判斷一個變量是不是對象非常簡單。值類型的類型判斷用typeof,引用類型的類型判斷用instanceof。
var fn = function () { };
console.log(fn instanceof Object); // true 好了,上面說了半天對象,各位可能也經常在工作中應對對象,在生活中還得應對活生生的對象。有些個心理不正常或者愛開玩笑的單身人士,還對于系統提示的“找不到對象”耿耿于懷。那么在javascript中的對象,到底該如何定義呢?
對象——若干屬性的集合。
java或者C#中的對象都是new一個class出來的,而且里面有字段、屬性、方法,規定的非常嚴格。但是javascript就比較隨意了 ——數組是對象,函數是對象,對象還是對象。對象里面的一切都是屬性,只有屬性,沒有方法。那么這樣方法如何表示呢?——方法也是一種屬性。因為它的屬性 表示為鍵值對的形式。
而且,更加好玩的事,javascript中的對象可以任意的擴展屬性,沒有class的約束。這個大家應該都知道,就不再強調了。
先說個最常見的例子:
以上代碼中,obj是一個自定義的對象,其中a、b、c就是它的屬性,而且在c的屬性值還是一個對象,它又有name、year兩個屬性。
這個可能比較好理解,那么函數和數組也可以這樣定義屬性嗎?——當然不行,但是它可以用另一種形式,總之函數/數組之流,只要是對象,它就是屬性的集合。
以函數為例子:
var fn = function () {
alert(100);
};
fn.a = 10;
fn.b = function () {
alert(123);
};
fn.c = {
name: "王福朋",
year: 1988
}; 上段代碼中,函數就作為對象被賦值了a、b、c三個屬性——很明顯,這就是屬性的集合嗎。
你問:這個有用嗎?
回答:可以看看jQuery源碼!
在jQuery源碼中,“jQuery”或者“$”,這個變量其實是一個函數,不信你可以叫咱們的老朋友typeof驗證一下。
console.log(typeof ($)); // function console.log($.trim(" ABC "));
驗明正身!的確是個函數。那么咱們常用的 $.trim() 也是個函數,經常用,就不用驗了吧!
很明顯,這就是在$或者jQuery函數上加了一個trim屬性,屬性值是函數,作用是截取前后空格。
javascript與java/C#相比,首先最需要解釋的就是弱類型,因為弱類型是最基本的用法,而且最常用,就不打算做一節來講。
其次要解釋的就是本文的內容——一切(引用類型)都是對象,對象是屬性的集合。最需要了解的就是對象的概念,和java/C#完全不一樣。所以,切記切記!
最后,有個疑問。在typeof的輸出類型中,function和object都是對象,為何卻要輸出兩種答案呢?都叫做object不行嗎?——當然不行。
2、函數和對象的關系
上文已經提到,函數就是對象的一種,因為通過instanceof函數可以判斷。
var fn = function () { }; console.log(fn instanceof Object); // true
對!函數是一種對象,但是函數卻不像數組一樣——你可以說數組是對象的一種,因為數組就像是對象的一個子集一樣。但是函數與對象之間,卻不僅僅是一種包含和被包含的關系,函數和對象之間的關系比較復雜,甚至有一點雞生蛋蛋生雞的邏輯,咱們這一節就縷一縷。
還是先看一個小例子吧。
function Fn() { this.name = '王福朋'; this.year = 1988; } var fn1 = new Fn();
上面的這個例子很簡單,它能說明:對象可以通過函數來創建。對!也只能說明這一點。
但是我要說——對象都是通過函數創建的——有些人可能反駁:不對!因為:
var obj = { a: 10, b: 20 }; var arr = [5, 'x', true];
但是不好意思,這個——真的——是一種——“快捷方式”,在編程語言中,一般叫做“語法糖”。
做“語法糖”做的最好的可謂是微軟大哥,它把他們家C#那小子弄的不男不女從的,本想圖個人見人愛,誰承想還得到處跟人解釋——其實它是個男孩!
話歸正傳——其實以上代碼的本質是:
//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 都是函數:
console.log(typeof (Object)); // function console.log(typeof (Array)); // function
所以,可以很負責任的說——對象都是通過函數來創建的。
現在是不是糊涂了—— 對象是函數創建的,而函數卻又是一種對象——天哪!函數和對象到底是什么關系啊?
別著急!揭開這個謎底,還得先去了解一下另一位老朋友——prototype原型。
3、prototype原型
prototype也是我們的老朋友,即使不了解的人,也應該都聽過它的大名。如果它還是您的新朋友,我估計您也是javascript的新朋友。
在咱們的第一節中說道,函數也是一種對象。他也是屬性的集合,你也可以對函數進行自定義屬性。
不用等咱們去試驗,javascript自己就先做了表率,人家就默認的給函數一個屬性——prototype。對,每個函數都有一個屬性叫做prototype。
這個prototype的屬性值是一個對象(屬性的集合,再次強調!),默認的只有一個叫做constructor的屬性,指向這個函數本身。
如上圖,SuperType是是一個函數,右側的方框就是它的原型。
原型既然作為對象,屬性的集合,不可能就只弄個constructor來玩玩,肯定可以自定義的增加許多屬性。例如這位Object大哥,人家的prototype里面,就有好幾個其他屬性。
咦,有些方法怎么似曾相似?
對!別著急,之后會讓你知道他們為何似曾相識。
接著往下說,你也可以在自己自定義的方法的prototype中新增自己的屬性
function Fn() { } Fn.prototype.name = '王福朋'; Fn.prototype.getYear = function () { return 1988; };
看到沒有,這樣就變成了
沒問題!
但是,這樣做有何用呢? —— 解決這個問題,咱們還是先說說jQuery吧。
var $div = $('div'); $div.attr('myName', '王福朋');
以上代碼中,$('div')返回的是一個對象,對象——被函數創建的。假設創建這一對象的函數是 myjQuery。它其實是這樣實現的。
myjQuery.prototype.attr = function () { //…… }; $('div') = new myjQuery();
不知道大家有沒有看明白。
如果用咱們自己的代碼來演示,就是這樣
function Fn() { } Fn.prototype.name = '王福朋'; Fn.prototype.getYear = function () { return 1988; }; var fn = new Fn(); console.log(fn.name); console.log(fn.getYear());
即,Fn是一個函數,fn對象是從Fn函數new出來的,這樣fn對象就可以調用Fn.prototype中的屬性。
因為每個對象都有一個隱藏的屬性——“__proto__”,這個屬性引用了創建這個對象的函數的prototype。即:fn.__proto__ === Fn.prototype
這里的"__proto__"成為“隱式原型”。
4、隱式原型
上節已經提到,每個函數function都有一個prototype,即原型。這里再加一句話——每個對象都有一個__proto__,可成為隱式原型。
這個__proto__是一個隱藏的屬性,javascript不希望開發者用到這個屬性值,有的低版本瀏覽器甚至不支持這個屬性值。所以你在 Visual Studio 2012這樣很高級很智能的編輯器中,都不會有__proto__的智能提示,但是你不用管它,直接寫出來就是了。
上面截圖看來,obj.__proto__和Object.prototype的屬性一樣!這么巧!
答案就是一樣。
obj這個對象本質上是被Object函數創建的,因此obj.__proto__=== Object.prototype。我們可以用一個圖來表示。
即,每個對象都有一個__proto__屬性,指向創建該對象的函數的prototype。
那么上圖中的“Object prototype”也是一個對象,它的__proto__指向哪里?
好問題!
在說明“Object prototype”之前,先說一下自定義函數的prototype。自定義函數的prototype本質上就是和 var obj = {} 是一樣的,都是被Object創建,所以它的__proto__指向的就是Object.prototype。
但是Object.prototype確實一個特例——它的__proto__指向的是null,切記切記!
還有——函數也是一種對象,函數也有__proto__嗎?
又一個好問題!——當然有。
函數也不是從石頭縫里蹦出來的,函數也是被創建出來的。誰創建了函數呢?——Function——注意這個大寫的“F”。
且看如下代碼。
以上代碼中,第一種方式是比較傳統的函數創建方式,第二種是用new Functoin創建。
首先根本不推薦用第二種方式。
這里只是向大家演示,函數是被Function創建的。
好了,根據上面說的一句話——對象的__proto__指向的是創建它的函數的prototype,就會出現:Object.__proto__ === Function.prototype。用一個圖來表示。
上圖中,很明顯的標出了:自定義函數Foo.__proto__指向Function.prototype,Object.__proto__指向 Function.prototype,唉,怎么還有一個……Function.__proto__指向Function.prototype?這不成了 循環引用了?
對!是一個環形結構。
其實稍微想一下就明白了。Function也是一個函數,函數是一種對象,也有__proto__屬性。既然是函數,那么它一定是被Function創建。所以——Function是被自身創建的。所以它的__proto__指向了自身的Prototype。
篇幅不少了,估計也都看煩了。快結束了。
最后一個問題:Function.prototype指向的對象,它的__proto__是不是也指向Object.prototype?
答案是肯定的。因為Function.prototype指向的對象也是一個普通的被Object創建的對象,所以也遵循基本的規則。
5、instanceof
對于值類型,你可以通過typeof判斷,string/number/boolean都很清楚,但是typeof在判斷到引用類型的時候,返回值只有object/function,你不知道它到底是一個object對象,還是數組,還是new Number等等。
這個時候就需要用到instanceof。例如:
上圖中,f1這個對象是被Foo創建,但是“f1 instanceof Object”為什么是true呢?
至于為什么過會兒再說,先把instanceof判斷的規則告訴大家。根據以上代碼看下圖:
Instanceof運算符的第一個變量是一個對象,暫時稱為A;第二個變量一般是一個函數,暫時稱為B。
Instanceof的判斷隊則是:沿著A的__proto__這條線來找,同時沿著B的prototype這條線來找,如果兩條線能找到同一個引用,即同一個對象,那么就返回true。如果找到終點還未重合,則返回false。
按照以上規則,大家看看“ f1 instanceof Object ”這句代碼是不是true? 根據上圖很容易就能看出來,就是true。
通過上以規則,你可以解釋很多比較怪異的現象,例如:
這些看似很混亂的東西,答案卻都是true,這是為何?
正好,這里也接上了咱們上一節說的“亂”。
上一節咱們貼了好多的圖片,其實那些圖片是可以聯合成一個整體的,即:
看這個圖片,千萬不要嫌煩,必須一條線一條線挨著分析。如果上一節你看的比較仔細,再結合剛才咱們介紹的instanceof的概念,相信能看懂這個圖片的內容。
看看這個圖片,你也就知道為何上面三個看似混亂的語句返回的是true了。
問題又出來了。Instanceof這樣設計,到底有什么用?到底instanceof想表達什么呢?
重點就這樣被這位老朋友給引出來了——繼承——原型鏈。
即,instanceof表示的就是一種繼承關系,或者原型鏈的結構。
6、繼承
為何用“繼承”為標題,而不用“原型鏈”?
原型鏈如果解釋清楚了很容易理解,不會與常用的java/C#產生混淆。而“繼承”確實常用面向對象語言中最基本的概念,但是java中的繼承與javascript中的繼承又完全是兩回事兒。因此,這里把“繼承”著重拿出來,就為了體現這個不同。
javascript中的繼承是通過原型鏈來體現的。先看幾句代碼
以上代碼中,f1是Foo函數new出來的對象,f1.a是f1對象的基本屬性,f1.b是怎么來的呢?——從Foo.prototype得來,因為f1.__proto__指向的是Foo.prototype
訪問一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿著__proto__這條鏈向上找,這就是原型鏈。
看圖說話:
上圖中,訪問f1.b時,f1的基本屬性中沒有b,于是沿著__proto__找到了Foo.prototype.b。
那么我們在實際應用中如何區分一個屬性到底是基本的還是從原型中找到的呢?大家可能都知道答案了——hasOwnProperty,特別是在for…in…循環中,一定要注意。
等等,不對! f1的這個hasOwnProperty方法是從哪里來的? f1本身沒有,Foo.prototype中也沒有,哪兒來的?
好問題。
它是從Object.prototype中來的,請看圖:
對象的原型鏈是沿著__proto__這條線走的,因此在查找f1.hasOwnProperty屬性時,就會順著原型鏈一直查找到Object.prototype。
由于所有的對象的原型鏈都會找到Object.prototype,因此所有的對象都會有Object.prototype的方法。這就是所謂的“繼承”。
當然這只是一個例子,你可以自定義函數和對象來實現自己的繼承。
說一個函數的例子吧。
我們都知道每個函數都有call,apply方法,都有length,arguments,caller等屬性。為什么每個函數都有?這肯定是“繼 承”的。函數由Function函數創建,因此繼承的Function.prototype中的方法。不信可以請微軟的Visual Studio老師給我們驗證一下:
看到了吧,有call、length等這些屬性。
那怎么還有hasOwnProperty呢?——那是Function.prototype繼承自Object.prototype的方法。有疑問 可以看看上一節將instanceof時候那個大圖,看看Function.prototype.__proto__是否指向 Object.prototype。
原型、原型鏈,大家都明白了嗎?
7、原型的靈活性
在Java和C#中,你可以簡單的理解class是一個模子,對象就是被這個模子壓出來的一批一批月餅(中秋節剛過完)。壓個啥樣,就得是個啥樣,不能隨便動,動一動就壞了。
而在javascript中,就沒有模子了,月餅被換成了面團,你可以捏成自己想要的樣子。
首先,對象屬性可以隨時改動。
對象或者函數,剛開始new出來之后,可能啥屬性都沒有。但是你可以這會兒加一個,過一會兒在加兩個,非常靈活。
在jQuery的源碼中,對象被創建時什么屬性都沒有,都是代碼一步一步執行時,一個一個加上的。
其次,如果繼承的方法不合適,可以做出修改。
如上圖,Object和Array的toString()方法不一樣。肯定是Array.prototype.toString()方法做了修改。
同理,我也可以自定義一個函數,并自己去修改prototype.toString()方法。
最后,如果感覺當前缺少你要用的方法,可以自己去創建。
例如在json2.js源碼中,為Date、String、Number、Boolean方法添加一個toJSON的屬性。
如果你要添加內置方法的原型屬性,最好做一步判斷,如果該屬性不存在,則添加。如果本來就存在,就沒必要再添加了。
8、執行上下文
什么是“執行上下文”(也叫做“執行上下文環境”)?暫且不下定義,先看一段代碼:
第一句報錯,a未定義,很正常。第二句、第三句輸出都是undefined,說明瀏覽器在執行console.log(a)時,已經知道了a是undefined,但卻不知道a是10(第三句中)。
在一段js代碼拿過來真正一句一句運行之前,瀏覽器已經做了一些“準備工作”,其中就包括對變量的聲明,而不是賦值。變量賦值是在賦值語句執行的時候進行的。可用下圖模擬:
這是第一種情況。
下面還有。先來個簡單的。
有js開發經驗的朋友應該都知道,你無論在哪個位置獲取this,都是有值的。至于this的取值情況,比較復雜,會專門拿出一篇文章來講解。
與第一種情況不同的是:第一種情況只是對變量進行聲明(并沒有賦值),而此種情況直接給this賦值。這也是“準備工作”情況要做的事情之一。
下面還有。。。第三種情況。
在第三種情況中,需要注意代碼注釋中的兩個名詞——“函數表達式”和“函數聲明”。雖然兩者都很常用,但是這兩者在“準備工作”時,卻是兩種待遇。
看以上代碼。“函數聲明”時我們看到了第二種情況的影子,而“函數表達式”時我們看到了第一種情況的影子。
沒錯。在“準備工作”中,對待函數表達式就像對待“ var a = 10 ”這樣的變量一樣,只是聲明。而對待函數聲明時,卻把函數整個賦值了。
好了,“準備工作”介紹完畢。
我們總結一下,在“準備工作”中完成了哪些工作:
- 變量、函數表達式——變量聲明,默認賦值為undefined;
- this——賦值;
- 函數聲明——賦值;
這三種數據的準備情況我們稱之為“執行上下文”或者“執行上下文環境”。
這里插一句題外話:通過以上三種情況,你可能會聯想到網上的有些考js語法的題目/面試題。的確,幾乎每個js語法題中都有這種題目出現。之前你遇到這種題目是不是靠背誦來解決?背過了,隔幾天又忘記了。——任何問題,都要去追根溯源,要知道這個問題是真正出自哪一塊知識點,要真正去理解。光靠背誦是沒用的。
細心的朋友可能會發現,我們上面所有的例子都是在全局環境下執行的。
其實,javascript在執行一個代碼段之前,都會進行這些“準備工作”來生成執行上下文。這個“代碼段”其實分三種情況——全局代碼,函數體,eval代碼。
這里解釋一下為什么代碼段分為這三種。
所謂“代碼段”就是一段文本形式的代碼。
首先,全局代碼是一種,這個應該沒有非議,本來就是手寫文本到<script>標簽里面的。
其次,eval代碼接收的也是一段文本形式的代碼。
最后,函數體是代碼段是因為函數在創建時,本質上是 new Function(…) 得來的,其中需要傳入一個文本形式的參數作為函數體。
這樣解釋應該能理解了。
最后,eval不常用,也不推薦大家用。
如果在函數中,除了以上數據之外,還會有其他數據。先看以下代碼:
以上代碼展示了在函數體的語句執行之前,arguments變量和函數的參數都已經被賦值。從這里可以看出,函數每被調用一次,都會產生一個新的執行上下文環境。因為不同的調用可能就會有不同的參數。
另外一點不同在于,函數在定義的時候(不是調用的時候),就已經確定了函數體內部自由變量的作用域。至于“自由變量”和“作用域”是后面要專門拿出來講述的重點,這里就先點到為止。用一個例子說明一下:
好了,總結完了函數的附加內容,我們就此要全面總結一下上下文環境的數據內容。
全局代碼的上下文環境數據內容為:
|
普通變量(包括函數表達式), 如: var a = 10; |
聲明(默認賦值為undefined) |
|
函數聲明, 如: function fn() { } |
賦值 |
|
this |
賦值 |
如果代碼段是函數體,那么在此基礎上需要附加:
|
參數 |
賦值 |
|
arguments |
賦值 |
|
自由變量的取值作用域 |
賦值 |
給執行上下文環境下一個通俗的定義——在執行代碼之前,把將要用到的所有的變量都事先拿出來,有的直接賦值了,有的先用undefined占個空。
了解了執行上下文環境中的數據信息,你就不用再去死記硬背那些可惡的面試題了。理解了就不用背誦!
講完了上下文環境,又來了新的問題——在執行js代碼時,會有數不清的函數調用次數,會產生許多個上下文環境。這么多上下文環境該如何管理,以及如何銷毀而釋放內存呢?下一節將通過“執行上下文棧”來解釋這個問題。
不過別著急,在解釋“執行上下文棧”之前,還需要把this說一下,this還是挺重要的。
說完this,接著說執行上下文棧。
上下文環境和作用域的關系
再說明之前,咱們先用簡單的語言來概括一下這兩個的區別。
00 上下文環境:
可以理解為一個看不見摸不著的對象(有若干個屬性),雖然看不見摸不著,但確實實實在在存在的,因為所有的變量都在里面存儲著,要不然咱們定義的變量在哪里存?
另外,對于函數來說,上下文環境是在調用時創建的,這個很好理解。拿參數做例子,你不調用函數,我哪兒知道你要給我傳什么參數?
01 作用域:
首先,它很抽象。第二,記住一句話:除了全局作用域,只有函數才能創建作用域。創建一個函數就創建了一個作用域,無論你調用不調用,函數只要創建了,它就有獨立的作用域,就有自己的一個“地盤”。
02 兩者:
一個作用域下可能包含若干個上下文環境。有可能從來沒有過上下文環境(函數從來就沒有被調用過);有可能有過,現在函數被調用完畢后,上下文環境被銷毀了;有可能同時存在一個或多個(閉包)。
上面的文字不理解沒關系,且看下面的例子。
第一,除了全局作用域外,每個函數都要創建一個作用域。作用域之間的變量是相互獨立的。因此,全局作用域中的x和fn作用域中的x,兩者毫無關系,互不影響,和平相處。
第二,程序執行之前,會生成全局上下文環境,并在程序執行時,對其中的變量賦值。
第三,程序執行到第17行,調用fn(5),會產生fn(5)的上下文環境,并壓棧,并設置為活動狀態。
第四,執行完第17行,fn(5)的返回值賦值給了f1。此時執行上下文環境又重新回到全局,但是fn(5)的上下文環境不能就此銷毀,因為其中有閉包的引用(可翻看前面文章,此處不再贅述)。
第五,繼續執行第18行,再次調用fn函數——fn(10)。產生fn(5)的上下文環境,并壓棧,并設置為活動狀態。但是此時fn(5)的上下文環境還在內存中——一個作用域下同時存在兩個上下文環境。
講到這里,重點已經講出來了,之后的場景這里就不再贅述了。
目的還是希望大家能通過這個例子,來理清楚上下文環境和作用域的關系。當然,也不是非得像個學院派似的一字一文的把概念說出來,簡單理解一下,對用閉包是有幫助的。
9、this
其實,this的取值,分四種情況。我們來挨個看一下。
在此再強調一遍一個非常重要的知識點:在函數中this到底取何值,是在函數真正被調用執行的時候確定的,函數定義的時候確定不了。因為this的取值是執行上下文環境的一部分,每次調用函數,都會產生一個新的執行上下文環境。
情況1:構造函數
所謂構造函數就是用來new對象的函數。其實嚴格來說,所有的函數都可以new一個對象,但是有些函數的定義是為了new一個對象,而有些函數則不是。另外注意,構造函數的函數名第一個字母大寫(規則約定)。例如:Object、Array、Function等。
以上代碼中,如果函數作為構造函數用,那么其中的this就代表它即將new出來的對象。
注意,以上僅限new Foo()的情況,即Foo函數作為構造函數的情況。如果直接調用Foo函數,而不是new Foo(),情況就大不一樣了。
這種情況下this是window,我們后文中會說到。
情況2:函數作為對象的一個屬性
如果函數作為對象的一個屬性時,并且作為對象的一個屬性被調用時,函數中的this指向該對象。
以上代碼中,fn不僅作為一個對象的一個屬性,而且的確是作為對象的一個屬性被調用。結果this就是obj對象。
注意,如果fn函數不作為obj的一個屬性被調用,會是什么結果呢?
如上代碼,如果fn函數被賦值到了另一個變量中,并沒有作為obj的一個屬性被調用,那么this的值就是window,this.x為undefined。
情況3:函數用call或者apply調用
當一個函數被call和apply調用時,this的值就取傳入的對象的值。至于call和apply如何使用,不會的朋友可以去查查其他資料,本系列教程不做講解。
情況4:全局 & 調用普通函數
在全局環境下,this永遠是window,這個應該沒有非議。
普通函數在調用時,其中的this也都是window。
以上代碼很好理解。
不過下面的情況你需要注意一下:
函數f雖然是在obj.fn內部定義的,但是它仍然是一個普通的函數,this仍然指向window。
完了。
看到了吧,this有關的知識點還是挺多的,不僅多而且非常重要。
最后,既然提到了this,有必要把一個非常經典的案例介紹給大家,又是jQuery源碼的。
以上代碼是從jQuery中摘除來的部分代碼。jQuery.extend和jQuery.fn.extend都指向了同一個函數,但是當執行時,函數中的this是不一樣的。
執行jQuery.extend(…)時,this指向jQuery;執行jQuery.fn.extend(…)時,this指向jQuery.fn。
這樣就巧妙的將一段代碼同時共享給兩個功能使用,更加符合設計原則。
在構造函數的prototype中,this代表著什么。
如上代碼,在Fn.prototype.getName函數中,this指向的是f1對象。因此可以通過this.name獲取f1.name的值。
其實,不僅僅是構造函數的prototype,即便是在整個原型鏈中,this代表的也都是當前對象的值。
10、執行上下文棧
執行全局代碼時,會產生一個執行上下文環境,每次調用函數都又會產生執行上下文環境。當函數調用完成時,這個上下文環境以及其中的數據都會被消除,再重新回到全局上下文環境。處于活動狀態的執行上下文環境只有一個。
其實這是一個壓棧出棧的過程——執行上下文棧。如下圖:
可根據以下代碼來詳細介紹上下文棧的壓棧、出棧過程。
如上代碼。
在執行代碼之前,首先將創建全局上下文環境。
然后是代碼執行。代碼執行到第12行之前,上下文環境中的變量都在執行過程中被賦值。
執行到第13行,調用bar函數。
跳轉到bar函數內部,執行函數體語句之前,會創建一個新的執行上下文環境。
并將這個執行上下文環境壓棧,設置為活動狀態。
執行到第5行,又調用了fn函數。進入fn函數,在執行函數體語句之前,會創建fn函數的執行上下文環境,并壓棧,設置為活動狀態。
待第5行執行完畢,即fn函數執行完畢后,此次調用fn所生成的上下文環境出棧,并且被銷毀(已經用完了,就要及時銷毀,釋放內存)。
同理,待第13行執行完畢,即bar函數執行完畢后,調用bar函數所生成的上下文環境出棧,并且被銷毀(已經用完了,就要及時銷毀,釋放內存)。
好了,我很耐心的給大家介紹了一段簡短代碼的執行上下文環境的變化過程,一個完整的閉環。其中上下文環境的變量賦值過程我省略了許多,因為那些并不難,一看就知道。
講到這里,我不得不很遺憾的跟大家說:其實以上我們所演示的是一種比較理想的情況。有一種情況,而且是很常用的一種情況,無法做到這樣干凈利落的說銷毀就銷毀。這種情況就是偉大的——閉包。
要說閉包,咱們還得先從自由變量和作用域說起。
11、簡介【作用域】
提到作用域,有一句話大家(有js開發經驗者)可能比較熟悉:“javascript沒有塊級作用域”。所謂“塊”,就是大括號“{}”中間的語句。例如if語句:
再比如for語句:
所以,我們在編寫代碼的時候,不要在“塊”里面聲明變量,要在代碼的一開始就聲明好了。以避免發生歧義。如:
其實,你光知道“javascript沒有塊級作用域”是完全不夠的,你需要知道的是——javascript除了全局作用域之外,只有函數可以創建的作用域。
所以,我們在聲明變量時,全局代碼要在代碼前端聲明,函數中要在函數體一開始就聲明好。除了這兩個地方,其他地方都不要出現變量聲明。而且建議用“單var”形式。
jQuery就是一個很好的示例:
下面繼續說作用域。作用域是一個很抽象的概念,類似于一個“地盤”
如上圖,全局代碼和fn、bar兩個函數都會形成一個作用域。而且,作用域有上下級的關系,上下級關系的確定就看函數是在哪個作用域下創建的。例如,fn作用域下創建了bar函數,那么“fn作用域”就是“bar作用域”的上級。
作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。例如以上代碼中,三個作用域下都聲明了“a”這個變量,但是他們不會有沖突。各自的作用域下,用各自的“a”。
說到這里,咱們又可以拿出jquery源碼來講講了。
jQuery源碼的最外層是一個自動執行的匿名函數:
為什么要這樣做呢?
原因就是在jQuery源碼中,聲明了大量的變量,這些變量將通過一個函數被限制在一個獨立的作用域中,而不會與全局作用域或者其他函數作用域的同名變量產生沖突。
全世界的開發者都在用jQuery,如果不這樣做,很可能導致jQuery源碼中的變量與外部javascript代碼中的變量重名,從而產生沖突。
作用域這塊只是很不好解釋,咱們就小步快跑,一步一步慢慢展示給大家。
下一節將把作用域和執行上下文環境結合起來說一說。
可見,要理解閉包,不是一兩句話能說清楚的。。。
12、作用域 和 上下文環境
上文簡單介紹了作用域,本文把作用域和上下文環境結合起來說一下,會理解的更深一些。
如上圖,我們在上文中已經介紹了,除了全局作用域之外,每個函數都會創建自己的作用域,作用域在函數定義時就已經確定了。而不是在函數調用時確定。
下面我們將按照程序執行的順序,一步一步把各個上下文環境加上。另外,對上下文環境不了解的朋友,可以去看看之前的兩篇文章:
http://www.cnblogs.com/wangfupeng1988/p/3986420.html
http://www.cnblogs.com/wangfupeng1988/p/3987563.html
第一步,在加載程序時,已經確定了全局上下文環境,并隨著程序的執行而對變量就行賦值。
第二步,程序執行到第27行,調用fn(10),此時生成此次調用fn函數時的上下文環境,壓棧,并將此上下文環境設置為活動狀態。
第三步,執行到第23行時,調用bar(100),生成此次調用的上下文環境,壓棧,并設置為活動狀態。
第四步,執行完第23行,bar(100)調用完成。則bar(100)上下文環境被銷毀。接著執行第24行,調用bar(200),則又生成bar(200)的上下文環境,壓棧,設置為活動狀態。
第五步,執行完第24行,則bar(200)調用結束,其上下文環境被銷毀。此時會回到fn(10)上下文環境,變為活動狀態。
第六步,執行完第27行代碼,fn(10)執行完成之后,fn(10)上下文環境被銷毀,全局上下文環境又回到活動狀態。
結束了。像老太太的裹腳布——又臭又長!
最后我們可以把以上這幾個圖片連接起來看看。
連接起來看,還是挺有意思的。作用域只是一個“地盤”,一個抽象的概念,其中沒有變量。要通過作用域對應的執行上下文環境來獲取變量的值。同一個作用域下,不同的調用會產生不同的執行上下文環境,繼而產生不同的變量的值。所以,作用域中變量的值是在執行過程中產生的確定的,而作用域卻是在函數創建時就確定了。
所以,如果要查找一個作用域下某個變量的值,就需要找到這個作用域對應的執行上下文環境,再在其中尋找變量的值。
13、從 自由變量 到 作用域鏈
先解釋一下什么是“自由變量”。
在A作用域中使用的變量x,卻沒有在A作用域中聲明(即在其他作用域中聲明的),對于A作用域來說,x就是一個自由變量。如下圖
如上程序中,在調用fn()函數時,函數體中第6行。取b的值就直接可以在fn作用域中取,因為b就是在這里定義的。而取x的值時,就需要到另一個作用域中取。到哪個作用域中取呢?
有人說過要到父作用域中取,其實有時候這種解釋會產生歧義。例如:
所以,不要在用以上說法了。相比而言,用這句話描述會更加貼切——要到創建這個函數的那個作用域中取值——是“創建”,而不是“調用”,切記切記——其實這就是所謂的“靜態作用域”。
對于本文第一段代碼,在fn函數中,取自由變量x的值時,要到哪個作用域中取?——要到創建fn函數的那個作用域中取——無論fn函數將在哪里調用。
上面描述的只是跨一步作用域去尋找。
如果跨了一步,還沒找到呢?——接著跨!——一直跨到全局作用域為止。要是在全局作用域中都沒有找到,那就是真的沒有了。
這個一步一步“跨”的路線,我們稱之為——作用域鏈。
我們拿文字總結一下取自由變量時的這個“作用域鏈”過程:(假設a是自由量)
第一步,現在當前作用域查找a,如果有則獲取并結束。如果沒有則繼續;
第二步,如果當前作用域是全局作用域,則證明a未定義,結束;否則繼續;
第三步,(不是全局作用域,那就是函數作用域)將創建該函數的作用域作為當前作用域;
第四步,跳轉到第一步。
以上代碼中:第13行,fn()返回的是bar函數,賦值給x。執行x(),即執行bar函數代碼。取b的值時,直接在fn作用域取出。取a的值時,試圖在fn作用域取,但是取不到,只能轉向創建fn的那個作用域中去查找,結果找到了。
14、閉包
至于“閉包”這個詞的概念的文字描述,確實不好解釋,我看過很多遍,但是現在還是記不住。
但是你只需要知道應用的兩種情況即可——函數作為返回值,函數作為參數傳遞。
第一,函數作為返回值
如上代碼,bar函數作為返回值,賦值給f1變量。執行f1(15)時,用到了fn作用域下的max變量的值。至于如何跨作用域取值,可以參考上一節。
第二,函數作為參數被傳遞
如上代碼中,fn函數作為一個參數被傳遞進入另一個函數,賦值給f參數。執行f(15)時,max變量的取值是10,而不是100。
上一節講到自由變量跨作用域取值時,曾經強調過:要去創建這個函數的作用域取值,而不是“父作用域”。理解了這一點,以上兩端代碼中,自由變量如何取值應該比較簡單。(不明白的朋友一定要去上一節看看,這個很重要!)
另外,講到閉包,除了結合著作用域之外,還需要結合著執行上下文棧來說一下。
在前面講執行上下文棧時(http://www.cnblogs.com/wangfupeng1988/p/3989357.html),我們提到當一個函數被調用完成之后,其執行上下文環境將被銷毀,其中的變量也會被同時銷毀。
但是在當時那篇文章中留了一個問號——有些情況下,函數調用完成之后,其執行上下文環境不會接著被銷毀。這就是需要理解閉包的核心內容。
咱們可以拿本文的第一段代碼(稍作修改)來分析一下。
第一步,代碼執行前生成全局上下文環境,并在執行時對其中的變量進行賦值。此時全局上下文環境是活動狀態。
第二步,執行第17行代碼時,調用fn(),產生fn()執行上下文環境,壓棧,并設置為活動狀態。
第三步,執行完第17行,fn()調用完成。按理說應該銷毀掉fn()的執行上下文環境,但是這里不能這么做。注意,重點來了:因為執行fn()時,返回的是一個函數。函數的特別之處在于可以創建一個獨立的作用域。而正巧合的是,返回的這個函數體中,還有一個自由變量max要引用fn作用域下的fn()上下文環境中的max。因此,這個max不能被銷毀,銷毀了之后bar函數中的max就找不到值了。
因此,這里的fn()上下文環境不能被銷毀,還依然存在與執行上下文棧中。
——即,執行到第18行時,全局上下文環境將變為活動狀態,但是fn()上下文環境依然會在執行上下文棧中。另外,執行完第18行,全局上下文環境中的max被賦值為100。如下圖:
第四步,執行到第20行,執行f1(15),即執行bar(15),創建bar(15)上下文環境,并將其設置為活動狀態。
執行bar(15)時,max是自由變量,需要向創建bar函數的作用域中查找,找到了max的值為10。這個過程在作用域鏈一節已經講過。
這里的重點就在于,創建bar函數是在執行fn()時創建的。fn()早就執行結束了,但是fn()執行上下文環境還存在與棧中,因此bar(15)時,max可以查找到。如果fn()上下文環境銷毀了,那么max就找不到了。
使用閉包會增加內容開銷,現在很明顯了吧!
第五步,執行完20行就是上下文環境的銷毀過程,這里就不再贅述了。
閉包和作用域、上下文環境有著密不可分的關系,真的是“想說愛你不容易”!
另外,閉包在jQuery中的應用非常多,在這里就不一一舉例子了。所以,無論你是想了解一個經典的框架/類庫,還是想自己開發一個插件或者類庫,像閉包、原型這些基本的理論,是一定要知道的。否則,到時候出了BUG你都不知道為什么,因為這些BUG可能完全在你的知識范圍之外。
本文轉自王福朋的博客深入理解javascript原型和閉包系列