一、原型、原型鏈
原型對象
在JavaScript 中,每當定義一個對象(函數)時候,對象中都會包含一些預定義的屬性。其中函數對象的一個屬性就是原型對象 prototype。注:普通對象沒有prototype,但有__proto__屬性。
原型對象其實就是普通對象(Function.prototype除外,它是函數對象,但它很特殊,他沒有prototype屬性(前面說道函數對象都有prototype屬性))。看下面的例子:
function f1(){};
console.log(f1.prototype) //f1{}
console.log(typeof f1. prototype) //Object
console.log(typeof Function.prototype) // Function,這個特殊
console.log(typeof Object.prototype) // Object
console.log(typeof Function.prototype.prototype) //undefined
從這句console.log(f1.prototype) //f1 {} 的輸出就結果可以看出,f1.prototype就是f1的一個實例對象。就是在f1創建的時候,創建了一個它的實例對象並賦值給它的prototype,基本過程如下:
var temp = new f1();
f1. prototype = temp;
所以,Function.prototype為什么是函數對象就迎刃而解了,上文提到凡是new Function ()產生的對象都是函數對象,所以temp1是函數對象。
var temp1 = new Function ();
Function.prototype = temp1;
那原型對象是用來做什么的呢?主要作用是用於繼承。舉了例子:
var person = function(name){
this.name = name
};
person.prototype.getName = function(){
return this.name;
}
var zjh = new person(‘zhangjiahao’);
zjh.getName(); //zhangjiahao
從這個例子可以看出,通過給person.prototype設置了一個函數對象的屬性,那有person實例(例中:zjh)出來的普通對象就繼承了這個屬性。具體是怎么實現的繼承,就要講到下面的原型鏈了。
三.原型鏈
JS在創建對象(不論是普通對象還是函數對象)的時候,都有一個叫做__proto__的內置屬性,用於指向創建它的函數對象的原型對象prototype。以上面的例子為例:
console.log(zjh.__proto__ === person.prototype) //true
同樣,person.prototype對象也有__proto__屬性,它指向創建它的函數對象(Object)的prototype
console.log(person.prototype.__proto__ === Object.prototype) //true
繼續,Object.prototype對象也有__proto__屬性,但它比較特殊,為null
console.log(Object.prototype.__proto__) //null
我們把這個有__proto__串起來的直到Object.prototype.__proto__為null的鏈叫做原型鏈。如下圖:
四.內存結構圖
為了更加深入和直觀的進行理解,下面我們畫一下上面的內存結構圖:
畫圖約定:
疑點解釋:
1.Object.__proto__ === Function.prototype // true
Object是函數對象,是通過new Function()創建,所以Object.__proto__指向Function.prototype。
2.Function.__proto__ === Function.prototype // true
Function 也是對象函數,也是通過new Function()創建,所以Function.__proto__指向Function.prototype。
自己是由自己創建的,好像不符合邏輯,但仔細想想,現實世界也有些類似,你是怎么來的,你媽生的,你媽怎么來的,你姥姥生的,……類人猿進化來的,那類人猿從哪來,一直追溯下去……,就是無,(NULL生萬物)
正如《道德經》里所說“無,名天地之始”。
3.Function.prototype.__proto__ === Object.prototype //true
其實這一點我也有點困惑,不過也可以試着解釋一下。
Function.prototype是個函數對象,理論上他的__proto__應該指向 Function.prototype,就是他自己,自己指向自己,沒有意義。
JS一直強調萬物皆對象,函數對象也是對象,給他認個祖宗,指向Object.prototype。Object.prototype.__proto__ === null,保證原型鏈能夠正常結束。
五.constructor
原型對象prototype中都有個預定義的constructor屬性,用來引用它的函數對象。這是一種循環引用
person.prototype.constructor === person //true
Function.prototype.constructor === Function //true
Object.prototype.constructor === Object //true
完善下上面的內存結構圖:
有兩點需要注意:
(1)注意Object.constructor===Function;//true 本身Object就是Function函數構造出來的
(2)如何查找一個對象的constructor,就是在該對象的原型鏈上尋找碰到的第一個constructor屬性所指向的對象
六.總結
1.原型和原型鏈是JS實現繼承的一種模型。
2.原型鏈的形成是真正是靠__proto__ 而非prototype
二、閉包
一、什么是閉包?
官方”的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
相信很少有人能直接看懂這句話,因為他描述的太學術。其實這句話通俗的來說就是:JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產生的閉包更為強大,也是大部分時候我們所謂的“閉包”。看下面這段代碼:
function a() {
var i = 0;
function b() { alert(++i); }
return b;
}
var c = a();
c();
這段代碼有兩個特點:
1、函數b嵌套在函數a內部;
2、函數a返回函數b。
引用關系如圖:
這樣在執行完var c=a()后,變量c實際上是指向了函數b,再執行c()后就會彈出一個窗口顯示i的值(第一次為1)。這段代碼其實就創建了一個閉包,為什么?因為函數a外的變量c引用了函數a內的函數b,就是說:
當函數a的內部函數b被函數a外的一個變量引用的時候,就創建了一個閉包。
讓我們說的更透徹一些。所謂“閉包”,就是在構造函數體內定義另外的函數作為目標對象的方法函數,而這個對象的方法函數反過來引用外層函數體中的臨時變量。這使得只要目標 對象在生存期內始終能保持其方法,就能間接保持原構造函數體當時用到的臨時變量值。盡管最開始的構造函數調用已經結束,臨時變量的名稱也都消失了,但在目 標對象的方法內卻始終能引用到該變量的值,而且該值只能通這種方法來訪問。即使再次調用相同的構造函數,但只會生成新對象和方法,新的臨時變量只是對應新 的值,和上次那次調用的是各自獨立的。
二、閉包有什么作用?
簡而言之,閉包的作用就是在a執行完並返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內部函數b的執行需要依賴a中的變量。這是對閉包作用的非常直白的描述,不專業也不嚴謹,但大概意思就是這樣,理解閉包需要循序漸進的過程。
在上面的例子中,由於閉包的存在使得函數a返回后,a中的i始終存在,這樣每次執行c(),i都是自加1后alert出i的值。
那 么我們來想象另一種情況,如果a返回的不是函數b,情況就完全不同了。因為a執行完后,b沒有被返回給a的外界,只是被a所引用,而此時a也只會被b引 用,因此函數a和b互相引用但又不被外界打擾(被外界引用),函數a和b就會被GC回收。(關於Javascript的垃圾回收機制將在后面詳細介紹)
三、閉包內的微觀世界
如果要更加深入的了解閉包以及函數a和嵌套函數b的關系,我們需要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程為例闡述這幾個概念。
- 當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置為定義a時a所在的“環境”,如果a是一個全局函數,則scope chain中只有window對象。
- 當執行函數a的時候,a會進入相應的執行環境(excution context)。
- 在創建執行環境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值就為第1步中的scope chain。即a.scope=a的作用域鏈。
- 然后執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
- 下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
- 最后把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置為b所被定義的環境,即a的作用域。
到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含了對函數a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數。函數b被c引用,函數b又依賴函數a,因此函數a在返回后不會被GC回收。
當函數b執行的時候亦會像以上步驟一樣。因此,執行時b的作用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,如下圖所示:
如圖所示,當在函數b中訪問一個變量的時候,搜索順序是:
- 先搜索自身的活動對象,如果存在則返回,如果不存在將繼續搜索函數a的活動對象,依次查找,直到找到為止。
- 如果函數b存在prototype原型對象,則在查找完自身的活動對象后先查找自身的原型對象,再繼續查找。這就是Javascript中的變量查找機制。
- 如果整個作用域鏈上都無法找到,則返回undefined。
小結,本段中提到了兩個重要的詞語:函數的定義與執行。文中提到函數的作用域是在定義函數時候就已經確定,而不是在執行的時候確定(參看步驟1和3)。用一段代碼來說明這個問題:
function f(x) {
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h());
這段代碼中變量h指向了f中的那個匿名函數(由g返回)。
- 假設函數h的作用域是在執行alert(h())確定的,那么此時h的作用域鏈是:h的活動對象->alert的活動對象->window對象。
- 假設函數h的作用域是在定義時確定的,就是說h指向的那個匿名函數在定義的時候就已經確定了作用域。那么在執行的時候,h的作用域鏈為:h的活動對象->f的活動對象->window對象。
如果第一種假設成立,那輸出值就是undefined;如果第二種假設成立,輸出值則為1。
運行結果證明了第2個假設是正確的,說明函數的作用域確實是在定義這個函數的時候就已經確定了。
四、閉包的應用場景
保護函數內的變量安全。以最開始的例子為例,函數a中i只有函數b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。
- 在內存中維持一個變量。依然如前例,由於閉包,函數a中i的一直存在於內存中,因此每次執行c(),都會給i自加1。
- 通過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)
私有屬性和方法在Constructor外是無法被訪問的function Constructor(...) {
var that = this;
var membername = value;
function membername(...) {...}
}
以上3點是閉包最基本的應用場景,很多經典案例都源於此。
三、繼承
JS繼承的實現方式
既然要實現繼承,那么首先我們得有一個父類,代碼如下:
// 定義一個動物類
function Animal (name) {
// 屬性
this.name = name || 'Animal';
// 實例方法
this.sleep = function(){
console.log(this.name + '正在睡覺!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1、原型鏈繼承
核心: 將父類的實例作為子類的原型
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
特點:
- 非常純粹的繼承關系,實例是子類的實例,也是父類的實例
- 父類新增原型方法/原型屬性,子類都能訪問到
- 簡單,易於實現
缺點:
- 要想為子類新增屬性和方法,必須要在
new Animal()
這樣的語句之后執行,不能放到構造器中 - 無法實現多繼承
- 來自原型對象的引用屬性是所有實例共享的(詳細請看附錄代碼: 示例1)
- 創建子類實例時,無法向父類構造函數傳參
2、構造繼承
核心:使用父類的構造函數來增強子類實例,等於是復制父類的實例屬性給子類(沒用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特點:
- 解決了1中,子類實例共享父類引用屬性的問題
- 創建子類實例時,可以向父類傳遞參數
- 可以實現多繼承(call多個父類對象)
缺點:
- 實例並不是父類的實例,只是子類的實例
- 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
- 無法實現函數復用,每個子類都有父類實例函數的副本,影響性能
3、實例繼承
核心:為父類實例添加新特性,作為子類實例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
特點:
- 不限制調用方式,不管是
new 子類()
還是子類()
,返回的對象具有相同的效果
缺點:
- 實例是父類的實例,不是子類的實例
- 不支持多繼承
4、拷貝繼承
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特點:
- 支持多繼承
缺點:
- 效率較低,內存占用高(因為要拷貝父類的屬性)
- 無法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)
5、組合繼承
核心:通過調用父類構造,繼承父類的屬性並保留傳參的優點,然后通過將父類實例作為子類原型,實現函數復用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特點:
- 彌補了方式2的缺陷,可以繼承實例屬性/方法,也可以繼承原型屬性/方法
- 既是子類的實例,也是父類的實例
- 不存在引用屬性共享問題
- 可傳參
- 函數可復用
缺點:
- 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
6、寄生組合繼承
核心:通過寄生方式,砍掉父類的實例屬性,這樣,在調用兩次父類的構造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 創建一個沒有實例方法的類
var Super = function(){};
Super.prototype = Animal.prototype;
//將實例作為子類的原型
Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
特點:
- 堪稱完美
缺點:
- 實現較為復雜