公有屬性繼承的7種方式總結(繼承系列-4)

繼承的幾種方式分析
用自己的理解,系統(tǒng)整理下以上幾種繼承,從簡至更簡更高級??偨Y如下:
前言:
1、共享原型的繼承方式
2、原型鏈繼承的方式
3、圣杯前身
4、圣杯模式的繼承
5、高級模式:變廢為寶
6、es6中的繼承 - setPrototypeOf
仿寫實現(xiàn):
擴展:Object.getPrototypeOf(b.prototype)
7、Object.create()
公有屬性繼承的7種方式 - 關鍵代碼總結
總結一下
最后驗證代碼
思考
前言:
接下來簡寫字母表示的含義如下:
-
A表示下例代碼中的Person構造函數(shù), -
A實例化對象 a= new Person() = person, -
B表示下例代碼中的Man構造函數(shù) -
B實例化對象 b= new Man() = man, -
中轉 Temp函數(shù)不變
/* 構造函數(shù) */
function Person(name) {
this.name = name; //私有屬性
return this;
}
Person.prototype.eat = function () { //公有屬性/方法
console.log('I can eat');
return this;
}
var person = new Person('構造函數(shù)');
console.log(person);
function Man(sex) {
Person.call(this, '繼承私有屬性'); //借用構造函數(shù)的方式實現(xiàn)其他對象私有屬性的繼承。
this.sex = sex;
}
1、共享原型的繼承方式
比如讓B.prototype = A.prototype。
這么做的缺點是大家成了綁在一條繩上的螞蚱,一損俱損、一改都改。
且B構造出來的實例化對象的constructor成了A,而不是B了。這就亂套了,瞎認祖宗了。
2、原型鏈繼承的方式
比如讓B.prototype = new A()。
實現(xiàn)原理是原型指向構造函數(shù)的實例化對象。通過實例化對象的__proto__去得到構造函數(shù)的原型。
缺點也是B構造出來的實例化對象的constructor成了A,而不是B了。這就亂套了,認賊作父了。
另外缺點還有A構造函數(shù)的實例化對象身上的私有屬性也被繼承了。
所謂“認賊作父”:
A的實例化對象的__proto__指向的就是A的原型,A原型上的constructor指向A本身這本沒有錯。
但因為你把人家B的原型修改了,B的原型成了A的實例化對象,B構造出來的實例化對象b的__proto__就是A構造出來的實例化對象a,因此導致順著原型鏈的關系,A成了B實例化對象b的構造類。但實際b的構造類應該是B。
3、圣杯前身
為了解決以上問題,新方案是建一個第三方Temp函數(shù)中轉一下。
讓 Temp.prototype = A.prototype;
讓 B.prototype = new Temp();
弊端:這樣還是改變不了B的實例化對象得到的constructor指向A這種認賊作父的局面。
為什么指向不對?
原因很簡單:B的實例化對象(b)沿著__proto__查找constructor。
因b.__proto__ == B.prototype = new Temp(),而new Temp得到的是個實例化對象,普通對象身上沒有constructor,只有原型對象身上有。
所以還得沿著原型鏈找到Temp實例化對象的__proto__,即Temp.prototype,而Temp.prototype == A.prototype,這次終于找到了prototype原型對象、也找到了constructor。但是遺憾的是,這個prototype是A的,A.prototype.constructor === A;,所以最后結果還得是A。
4、圣杯模式的繼承
沒辦法了,我們知道constructor是誰,但是JS引擎搞混了。
所以,只能在上一個基礎上,人為的去修改constructor,并將這塊代碼封裝起來,得到終極的封裝代碼“圣杯模式”:
(function () {
var Inherit = function () {};
Inherit.prototype = Person.prototype;
/* 上一句可以優(yōu)化,可以利用Object.create簡化這一步。改成:
var protoType = Object.create(Person.prototype);
Man.prototype = protoType;
這樣寫, 也就是下邊第7條的意思
*/
Man.prototype = new Inherit();
Man.prototype.constructor = Man; //在prototype添加固定屬性,中途攔截一下constructor
Man.prototype.uber = Person.prototype; // 設置他的超類
}());
5、高級模式:變廢為寶
prototype.__proto__指向的是Object.prototype,但Object原型上的內容我們基本不會用、且每個對象原型鏈的最后都是Object的原型,即使被改了也能最終找到Object原型。那既然這樣,為何不從Object的原型下手改一下?
讓B原型上實例化對象指向A原型。Man.prototype.__proto__ = Person.prototype;這堪比將黃河改道?。『喼辈灰呒?。直呼內行!
冷靜一下啊,缺點還是有的,就是__proto__作為隱式屬性,是系統(tǒng)自己的屬性,不建議我們去修改,如果理解不透徹容易改錯,所以不推薦使用。
這么好的思路不讓用豈不是可惜?
不必悲傷,因為官方內部利用這個原理幫我們實現(xiàn)了:
6、es6中的繼承 - setPrototypeOf
該方法就是讓B的原型指向A的原型。
代碼如下
Object.setPrototypeOf(B.prototype, A.prototype)
其原理同第五條,只不過是es6給我們新增的官方用法。更安全、更可靠。
仿寫實現(xiàn):
Object.setPrototypeOf = function(_pro, proto){
_pro.__proto__ = proto;
return _pro;
}
擴展:Object.getPrototypeOf(b.prototype)
MDN:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
7、Object.create()
有了上一個Object的方法,聯(lián)想到另一個es5的Object.create()。
本意用來創(chuàng)建一個新對象,第一個參數(shù)是一個對象,用來當做新對象的__proto__,第二個參數(shù)是一個配置對象,和Object.defineProperties()的第二個參數(shù)一致。
因此,我們修改原型指向也可以用這個方法創(chuàng)建出來的對象:
Man.prototype = Object.create(Person.prototype, {
constructor: {
value: Man
}
});
公有屬性繼承的7種方式 - 關鍵代碼總結
1、原型直接指向原型,簡單粗暴
Man.prototype = Person.prototype;
2、原型指向實例化對象
Man.prototype = new Person('繼承第二種');
3、中轉函數(shù),粗糙圣杯
function Temp(){};
Temp.prototype = Person.prototype;
Man.prototype = new Temp();
4、精美圣杯模式
(function(){
var Inherit = function(){};
Inherit.prototype = Person.prototype;
Man.prototype = new Inherit();
Man.prototype.constructor = Man;// 在prototype添加固定屬性,中途攔截一下constructor
Man.prototype.uber = Person.prototype; // 設置他的超類
}());
5、高級模式:直接改變原型上的隱式原型
Man.prototype.__proto__ = Person.prototype;
6、Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(Man.prototype,Person.prototype); // 第五條原理的官方實現(xiàn)
7、Object.create(A.prototype,{…})
Man.prototype = Object.create(Person.prototype, { // class中的繼承原理寫法
constructor: {
value: Man
}
});
總結一下
從第五條開始,這個思路高明的地方所在:
這么做也很合理,因為man.__proto__指向Man.prototype,還有自己的使命不能被直接替換,但是Man.prototype.__proto__作為Object.Prototype貌似除了要用Object定義的方法外沒啥作用,所以從這一骨節(jié)上嫁接一下,把Person.prototype按到這里,幸運的是,Person.prototype.__proto__也有Object.prototype,所以我們不僅賺了夫人,也沒賠了兵。
最后驗證代碼
分別用上邊的方案實現(xiàn)繼承后,可以用下邊的代碼驗證下效果。
var man = new Man('male');
console.log(man);
console.log(man.constructor);
思考
這些方法其實都很麻煩,為什么還要人為的去搞一下繼承?
好像要科學方法改細胞似的。就不能天生繼承嗎?
這就是class出現(xiàn)的原因了。
class寫法更簡單,目的更明確。
具體寫法和做法,可以看后續(xù)es6 - 《class》篇章

讓我們一起攜手同走前端路!
關注公眾號回復【加群】即可
