我面試過不少同窗,其中能把原型繼承講明白的寥寥無幾,能把new操做符講明白的就更少了。但願這篇文章可以解決你的疑惑,帶你面試飛起來。 原文:詳解JS原型鏈與繼承javascript
繼承是OO語言中的一個最爲人津津樂道的概念.許多OO語言都支持兩種繼承方式: 接口繼承 和 實現繼承 .接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法.因爲js中方法沒有簽名,在ECMAScript中沒法實現接口繼承.ECMAScript只支持實現繼承,並且其 實現繼承
主要是依靠原型鏈來實現的.html
簡單回顧下構造函數,原型和實例的關係:java
每一個構造函數(constructor)都有一個原型對象(prototype),原型對象都包含一個指向構造函數的指針,而實例(instance)都包含一個指向原型對象的內部指針.git
JS對象的圈子裏有這麼個遊戲規則:github
若是試圖引用對象(實例instance)的某個屬性,會首先在對象內部尋找該屬性,直至找不到,而後纔在該對象的原型(instance.prototype)裏去找這個屬性.面試
若是讓原型對象指向另外一個類型的實例.....有趣的事情便發生了.數組
即: constructor1.prototype = instance2瀏覽器
鑑於上述遊戲規則生效,若是試圖引用constructor1構造的實例instance1的某個屬性p1:app
1).首先會在instance1內部屬性中找一遍;函數
2).接着會在instance1.__proto__(constructor1.prototype)中找一遍,而constructor1.prototype 其實是instance2, 也就是說在instance2中尋找該屬性p1;
3).若是instance2中仍是沒有,此時程序不會灰心,它會繼續在instance2.__proto__(constructor2.prototype)中尋找...直至Object的原型對象
搜索軌跡: instance1--> instance2 --> constructor2.prototype…-->Object.prototype
這種搜索的軌跡,形似一條長鏈, 又因prototype在這個遊戲規則中充當連接的做用,因而咱們把這種實例與原型的鏈條稱做 原型鏈 . 下面有個例子
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,致使Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true
複製代碼
instance實例經過原型鏈找到了Father原型中的getFatherValue方法.
注意: 此時instance.constructor指向的是Father,這是由於Son.prototype中的constructor被重寫的緣故.
以上咱們弄清楚了何爲原型鏈,若有不清楚請儘可能在下方給我留言
使用原型鏈後, 咱們怎麼去判斷原型和實例的這種繼承關係呢? 方法通常有兩種.
第一種是使用 instanceof 操做符, 只要用這個操做符來測試實例(instance)與原型鏈中出現過的構造函數,結果就會返回true. 如下幾行代碼就說明了這點.
alert(instance instanceof Object);//true
alert(instance instanceof Father);//true
alert(instance instanceof Son);//true
複製代碼
因爲原型鏈的關係, 咱們能夠說instance 是 Object, Father 或 Son中任何一個類型的實例. 所以, 這三個構造函數的結果都返回了true.
第二種是使用 isPrototypeOf() 方法, 一樣只要是原型鏈中出現過的原型,isPrototypeOf() 方法就會返回true, 以下所示.
alert(Object.prototype.isPrototypeOf(instance));//true
alert(Father.prototype.isPrototypeOf(instance));//true
alert(Son.prototype.isPrototypeOf(instance));//true
複製代碼
原理同上.
原型鏈並不是十分完美, 它包含以下兩個問題.
問題一: 當原型鏈中包含引用類型值的原型時,該引用類型值會被全部實例共享;
問題二: 在建立子類型(例如建立Son的實例)時,不能向超類型(例如Father)的構造函數中傳遞參數.
有鑑於此, 實踐中不多會單獨使用原型鏈.
爲此,下面將有一些嘗試以彌補原型鏈的不足.
爲解決原型鏈中上述兩個問題, 咱們開始使用一種叫作借用構造函數(constructor stealing)的技術(也叫經典繼承).
基本思想:即在子類型構造函數的內部調用超類型構造函數.
function Father(){
this.colors = ["red","blue","green"];
}
function Son(){
Father.call(this);//繼承了Father,且向父類型傳遞參數
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可見引用類型值是獨立的
複製代碼
很明顯,借用構造函數一舉解決了原型鏈的兩大問題:
其一, 保證了原型鏈中引用類型值的獨立,再也不被全部實例共享;
其二, 子類型建立時也可以向父類型傳遞參數.
隨之而來的是, 若是僅僅借用構造函數,那麼將沒法避免構造函數模式存在的問題--方法都在構造函數中定義, 所以函數複用也就不可用了.並且超類型(如Father)中定義的方法,對子類型而言也是不可見的. 考慮此,借用構造函數的技術也不多單獨使用.
組合繼承, 有時候也叫作僞經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式.
基本思路: 使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數來實現對實例屬性的繼承.
這樣,既經過在原型上定義方法實現了函數複用,又能保證每一個實例都有它本身的屬性. 以下所示.
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//繼承實例屬性,第一次調用Father()
this.age = age;
}
Son.prototype = new Father();//繼承父類方法,第二次調用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
複製代碼
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲 JavaScript 中最經常使用的繼承模式. 並且, instanceof 和 isPrototypeOf( )也能用於識別基於組合繼承建立的對象.
同時咱們還注意到組合繼承其實調用了兩次父類構造函數, 形成了沒必要要的消耗, 那麼怎樣才能避免這種沒必要要的消耗呢, 這個咱們將在後面講到.
該方法最初由道格拉斯·克羅克福德於2006年在一篇題爲 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式繼承) 的文章中提出. 他的想法是藉助原型能夠基於已有的對象建立新對象, 同時還沒必要所以建立自定義類型. 大意以下:
在object()函數內部, 先建立一個臨時性的構造函數, 而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例.
function object(o){
function F(){}
F.prototype = o;
return new F();
}
複製代碼
從本質上講, object() 對傳入其中的對象執行了一次淺複製. 下面咱們來看看爲何是淺複製.
var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
複製代碼
在這個例子中,能夠做爲另外一個對象基礎的是person對象,因而咱們把它傳入到object()函數中,而後該函數就會返回一個新對象. 這個新對象將person做爲原型,所以它的原型中就包含引用類型值屬性. 這意味着person.friends不只屬於person全部,並且也會被anotherPerson以及yetAnotherPerson共享.
在 ECMAScript5 中,經過新增 object.create() 方法規範化了上面的原型式繼承.
object.create() 接收兩個參數:
var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
複製代碼
object.create() 只有一個參數時功能與上述object方法相同, 它的第二個參數與Object.defineProperties()方法的第二個參數格式相同: 每一個屬性都是經過本身的描述符定義的.以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性.例如:
var person = {
name : "Van"
};
var anotherPerson = Object.create(person, {
name : {
value : "Louis"
}
});
alert(anotherPerson.name);//"Louis"
複製代碼
目前支持 Object.create() 的瀏覽器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.
提醒: 原型式繼承中, 包含引用類型值的屬性始終都會共享相應的值, 就像使用原型模式同樣.
寄生式繼承是與原型式繼承緊密相關的一種思路, 一樣是克羅克福德推而廣之.
寄生式繼承的思路與(寄生)構造函數和工廠模式相似, 即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象. 以下.
function createAnother(original){
var clone = object(original);//經過調用object函數建立一個新對象
clone.sayHi = function(){//以某種方式來加強這個對象
alert("hi");
};
return clone;//返回這個對象
}
複製代碼
這個例子中的代碼基於person返回了一個新對象--anotherPerson. 新對象不只具備 person 的全部屬性和方法, 並且還被加強了, 擁有了sayH()方法.
注意: 使用寄生式繼承來爲對象添加函數, 會因爲不能作到函數複用而下降效率;這一點與構造函數模式相似.
前面講過,組合繼承是 JavaScript 最經常使用的繼承模式; 不過, 它也有本身的不足. 組合繼承最大的問題就是不管什麼狀況下,都會調用兩次父類構造函數: 一次是在建立子類型原型的時候, 另外一次是在子類型構造函數內部. 寄生組合式繼承就是爲了下降調用父類構造函數的開銷而出現的 .
其背後的基本思路是: 沒必要爲了指定子類型的原型而調用超類型的構造函數
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//建立對象
prototype.constructor = subClass;//加強對象
subClass.prototype = prototype;//指定對象
}
複製代碼
extend的高效率體如今它沒有調用superClass構造函數,所以避免了在subClass.prototype上面建立沒必要要,多餘的屬性. 於此同時,原型鏈還能保持不變; 所以還能正常使用 instanceof 和 isPrototypeOf() 方法.
以上,寄生組合式繼承,集寄生式繼承和組合繼承的優勢於一身,是實現基於類型繼承的最有效方法.
下面咱們來看下extend的另外一種更爲有效的擴展.
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
複製代碼
我一直不太明白的是爲何要 "new F()", 既然extend的目的是將子類型的 prototype 指向超類型的 prototype,爲何不直接作以下操做呢?
subClass.prototype = superClass.prototype;//直接指向超類型prototype
複製代碼
顯然, 基於如上操做, 子類型原型將與超類型原型共用, 根本就沒有繼承關係.
爲了追本溯源, 我順便研究了new運算符具體幹了什麼?發現其實很簡單,就幹了三件事情.
var obj = {};
obj.__proto__ = F.prototype;
F.call(obj);
複製代碼
第一行,咱們建立了一個空對象obj;
第二行,咱們將這個空對象的__proto__成員指向了F函數對象prototype成員對象;
第三行,咱們將F函數對象的this指針替換成obj,而後再調用F函數.
咱們能夠這麼理解: 以 new 操做符調用構造函數的時候,函數內部實際上發生如下變化:
一、建立一個空對象,而且 this 變量引用該對象,同時還繼承了該函數的原型。
二、屬性和方法被加入到 this 引用的對象中。
三、新建立的對象由 this 所引用,而且最後隱式的返回 this.
以上, 經過設置 __proto__ 屬性繼承了父類, 若是去掉new 操做, 直接參考以下寫法
subClass.prototype = superClass.prototype;//直接指向超類型prototype
複製代碼
那麼, 使用 instanceof 方法判斷對象是不是構造器的實例時, 將會出現紊亂.
假如參考如上寫法, 那麼extend代碼應該爲
function extend(subClass, superClass) {
subClass.prototype = superClass.prototype;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
複製代碼
此時, 請看以下測試:
function a(){}
function b(){}
extend(b,a);
var c = new a(){};
console.log(c instanceof a);//true
console.log(c instanceof b);//true
複製代碼
c被認爲是a的實例能夠理解, 也是對的; 但c卻被認爲也是b的實例, 這就不對了. 究其緣由, instanceof 操做符比較的應該是 c.__proto__ 與 構造器.prototype(即 b.prototype 或 a.prototype) 這二者是否相等, 又extend(b,a); 則b.prototype === a.prototype, 故這纔打印出上述不合理的輸出.
那麼最終,原型鏈繼承能夠這麼實現,例如:
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//繼承實例屬性,第一次調用Father()
this.age = age;
}
extend(Son,Father)//繼承父類方法,此處並不會第二次調用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
複製代碼
使用了原型鏈後, 當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性爲止,到查找到達原型鏈的頂部 - 也就是 Object.prototype - 可是仍然沒有找到指定的屬性,就會返回 undefined. 此時若想避免原型鏈查找, 建議使用 hasOwnProperty 方法. 由於 hasOwnProperty 是 JavaScript 中惟一一個處理屬性可是不查找原型鏈的函數. 如:
console.log(instance1.hasOwnProperty('age'));//true
複製代碼
對比: isPrototypeOf 則是用來判斷該方法所屬的對象是否是參數的原型對象,是則返回true,不然返回false。如:
console.log(Father.prototype.isPrototypeOf(instance1));//true
複製代碼
上面提到幾回提到 instanceof 運算符. 那麼到底它是怎麼玩的呢? 下面讓咱們來趴一趴它的使用場景.
instanceof 運算符是用來在運行時指出對象是不是構造器的一個實例, 例如漏寫了new運算符去調用某個構造器, 此時構造器內部能夠經過 instanceof 來判斷.(java中功能相似)
function f(){
if(this instanceof arguments.callee)
console.log('此處做爲構造函數被調用');
else
console.log('此處做爲普通函數被調用');
}
f();//此處做爲普通函數被調用
new f();//此處做爲構造函數被調用
複製代碼
以上, this instanceof arguments
.callee 的值若是爲 true 表示是做爲構造函數被調用的,若是爲 false 則表示是做爲普通函數被調用的。
對比: typeof 則用以獲取一個變量或者表達式的類型, 通常只能返回以下幾個結果:
number,boolean,string,function(函數),object(NULL,數組,對象),undefined。
此處引用 艾倫的 JS 對象機制深剖——new 運算符
接着上述對new運算符的研究, 咱們來考察 ECMAScript 語言規範中 new 運算符的定義:
The new Operator
The production NewExpression : new NewExpression is evaluated as follows:Evaluate NewExpression.Call GetValue(Result(1)).If Type(Result(2)) is not Object, throw a TypeError exception.If Result(2) does not implement the internal [[Construc]] method, throw a TypeError exception.Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).Return Result(5).
其大意是,new 後必須跟一個對象而且此對象必須有一個名爲 [[Construct]] 的內部方法(其實這種對象就是構造器),不然會拋出異常
根據這些內容,咱們徹底能夠構造一個僞 [[Construct]] 方法來模擬此流程
function MyObject(age) {
this.age = age;
}
MyObject.construct = function() {
var o = {}, Constructor = MyObject;
o.__proto__ = Constructor.prototype;
// FF 支持用戶引用內部屬性 [[Prototype]]
Constructor.apply(o, arguments);
return o;
};
var obj1 = new MyObject(10);
var obj2 = MyObject.construct(10);
alert(obj2 instanceof MyObject);// true
複製代碼
不知不覺本文已經寫了3天, 其實還有不少引伸的東西沒有講出來, 你們有什麼問題或好的想法歡迎在下方參與留言和評論.
本文做者: louis
本文連接: louiszhai.github.io/2015/12/15/…
參考: