面向?qū)ο蟮恼Z(yǔ)言支持兩種繼承方式,接口繼承和實(shí)現(xiàn)繼承
js無(wú)法實(shí)現(xiàn)接口繼承,只支持實(shí)現(xiàn)繼承,主要通過(guò)原型鏈來(lái)實(shí)現(xiàn)。
具體實(shí)現(xiàn)方式有以下幾種:
- 原型鏈
- 借用構(gòu)造函數(shù)
- 組合繼承
- 原型式繼承
- 寄生式繼承
- 寄生組合式繼承
逐一看下:
原型鏈
每個(gè)函數(shù)都有prototype(原型)屬性,指向一個(gè)原型對(duì)象
原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針(constructor)
通過(guò)函數(shù)實(shí)例化的對(duì)象中,都有一個(gè)指向原型對(duì)象的內(nèi)部指針
通過(guò)這種特性,可以讓一個(gè)對(duì)象A的原型對(duì)象等于另一個(gè)實(shí)例化的對(duì)象B,對(duì)象B的原型對(duì)象等于另一個(gè)實(shí)例化的對(duì)象C,這樣層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條,也就是原型鏈
function Person() {
this.name = 'wang'
}
Person.prototype.sayName = function () {
console.log(this.name);
}
function Man() {
this.age = 18;
}
//繼承:子類的原型等于父類的實(shí)例對(duì)象
Man.prototype = new Person();
Man.prototype.sayAge = function () {
console.log(this.age);
}
var p1 = new Man();
p1.sayName(); //wang
p1.sayAge(); //18
代碼中子類的方法都可以正常使用,這就涉及到了對(duì)象的原型搜索機(jī)制
當(dāng)訪問(wèn)一個(gè)實(shí)例屬性時(shí),首先在實(shí)例中搜索該屬性,如果沒(méi)有找到,則會(huì)繼續(xù)搜索實(shí)例的原型,如果沒(méi)有找到并且原型對(duì)象還有原型,則繼續(xù)向上搜索,直到搜索到?jīng)]有原型對(duì)象為止,也就是截止到Object,因?yàn)閖s中所有對(duì)象都繼承自O(shè)bject。
這也就能解釋為什么所有實(shí)例都有toString()等公用方法。
對(duì)象識(shí)別
利用原型鏈繼承實(shí)現(xiàn)的子類可以識(shí)別屬于子類、父類、Object
console.log(p1 instanceof Object); //true
console.log(p1 instanceof Person); //true
console.log(p1 instanceof Man); //true
派生判斷
console.log(Object.prototype.isPrototypeOf(p1)); //true
console.log(Person.prototype.isPrototypeOf(p1)); //true
console.log(Man.prototype.isPrototypeOf(p1)); //true
原型鏈的問(wèn)題:
- 屬性為引用類型時(shí),由于共享機(jī)制,操作實(shí)例屬性會(huì)影響其他實(shí)例
- 無(wú)法在不影響其他實(shí)例的情況下,給父類的構(gòu)造函數(shù)傳參
function Person() {
this.name = 'wang'
this.friends = ['zhang', 'liu']
}
function Man() {
this.age = 18;
}
//繼承:子類的原型等于父類的實(shí)例對(duì)象
Man.prototype = new Person();
var p1 = new Man();
var p2 = new Man();
console.log(p1.friends); //["zhang", "liu"]
p2.friends.push('li');
console.log(p1.friends); //["zhang", "liu", "li"]
可以看到數(shù)組屬性受到影響,所以實(shí)踐中很少單獨(dú)使用原型鏈。
借用構(gòu)造函數(shù)
也叫偽造對(duì)象或經(jīng)典繼承,為解決原型鏈不足而生
基本思想:在子類構(gòu)造函數(shù)內(nèi)部調(diào)用父類構(gòu)造函數(shù)
function Person(name) {
this.name = name;
this.friends = ['zhang', 'liu']
}
function Man(name) {
Person.call(this, name)
}
var p1 = new Man('wang');
var p2 = new Man('zhang');
console.log(p1.name); //wang
console.log(p2.name); //zhang
console.log(p1.friends); //["zhang", "liu"]
p2.friends.push('li');
console.log(p2.friends); //["zhang", "liu", "li"]
console.log(p1.friends); //["zhang", "liu"]
可以看出即可以給父類的構(gòu)造函數(shù)傳參,引用類型的屬性又互不影響
問(wèn)題
如果僅用借用構(gòu)造函數(shù),就會(huì)碰到構(gòu)造函數(shù)的共性問(wèn)題,無(wú)法將函數(shù)復(fù)用造成資源浪費(fèi),所以借用構(gòu)造函數(shù)的技術(shù)也很少單獨(dú)使用。
組合繼承
也叫偽經(jīng)典繼承,將原型鏈和借用構(gòu)造函數(shù)組合來(lái)用,發(fā)揮二者之長(zhǎng)
使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承
使用借用構(gòu)造函數(shù)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承
function Person(name) {
this.name = name;
this.friends = ['zhang', 'liu']
}
Person.prototype.sayName = function () {
console.log(this.name);
}
function Man(name, age) {
//用構(gòu)造函數(shù)繼承屬性
Person.call(this, name);
this.age = age;
}
//用原型鏈繼承實(shí)例
Man.prototype = new Person();
Man.prototype.sayAge = function () {
console.log(this.age);
}
var p1 = new Man('wang', 18);
console.log(p1.name); //wang
p1.sayName() //wang
p1.sayAge(); //18
console.log(p1.friends); //["zhang", "liu"]
console.log(p1.__proto__.friends); //["zhang", "liu"]
代碼中用到了2種方式一起實(shí)現(xiàn)了繼承
通過(guò)對(duì)象的__proto__屬性,可以訪問(wèn)到對(duì)象的原型
可以看到實(shí)例對(duì)象和實(shí)例的原型對(duì)象同時(shí)存在父類屬性和方法
組合繼承避免了各自的缺陷,融合了優(yōu)點(diǎn),成為最常用的繼承模式
但也有不足之處:
...
Person.call(this, name); //第二次執(zhí)行Person()
...
Man.prototype = new Person(); //第一次執(zhí)行Person()
組合繼承會(huì)指向兩次父類的構(gòu)造函數(shù),在性能上不夠理想
所以可以采用寄生組合式繼承優(yōu)化實(shí)現(xiàn)
在不同場(chǎng)景下,還有其他可選擇的輕量級(jí)繼承方式
原型式繼承
核心思想:原型可以基于已有對(duì)象創(chuàng)建新對(duì)象,同時(shí)不必創(chuàng)建自定義類型
function object(obj) {
function Fn() { }
Fn.prototype = obj;
return new Fn();
}
在函數(shù)內(nèi)部,先創(chuàng)建了一個(gè)臨時(shí)的構(gòu)造函數(shù),再將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回這個(gè)臨時(shí)類型的一個(gè)新實(shí)例。本質(zhì)是執(zhí)行了傳入對(duì)象的淺復(fù)制。
var person = {
name: 'wang',
}
var p1 = object(person);
console.log(p1.name);
在ES5中通過(guò)新增Object.create()方法規(guī)范了原型式繼承
var person = {
name: 'wang',
friends: ['zhang', 'liu']
}
var p1 = Object.create(person);
console.log(p1.friends); //["zhang", "liu"]
var p2 = Object.create(person);
p2.friends.push('li');
console.log(p1.friends); //["zhang", "liu", "li"]
通過(guò)Object.create()可實(shí)現(xiàn)原型式繼承
這種方式適用于讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類似的情況,可不用構(gòu)造函數(shù)
問(wèn)題:可以看出,存在引用類型的屬性的共享問(wèn)題,也就是會(huì)被其它實(shí)例修改
寄生式繼承
實(shí)現(xiàn)思路:創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù),該函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象,最后返回對(duì)象
var person = {
name: 'wang',
friends: ['zhang', 'liu']
}
function createObject(obj){
var copy = Object.create(obj);
copy.sayName = function(){
console.log(this.name);
}
return copy;
}
var p1 = createObject(person);
p1.sayName(); //wang
類似寄生構(gòu)造函數(shù)和工廠模式,
適用于:主要考慮對(duì)象而不是自定義類型和構(gòu)造函數(shù)的情況
問(wèn)題:在函數(shù)體內(nèi)部定義方法,會(huì)無(wú)法使函數(shù)復(fù)用而降低效率
寄生組合式繼承
針對(duì)組合式繼承模式指向兩次父類構(gòu)造函數(shù)的不足,來(lái)優(yōu)化
//實(shí)現(xiàn)父類原型拷貝副本給子類
function inheritPrototype(subObj, superObj) {
var prototype = Object.create(superObj.prototype); //創(chuàng)建對(duì)象
prototype.constructor = subObj; //增強(qiáng)對(duì)象
subObj.prototype = prototype; //指定對(duì)象
}
function Person(name) {
this.name = name;
this.friends = ['zhang', 'liu']
}
Person.prototype.sayName = function () {
console.log(this.name);
}
function Man(name, age) {
//用構(gòu)造函數(shù)繼承屬性,只需執(zhí)行一次Person()
Person.call(this, name);
this.age = age;
}
//Man.prototype = new Person();
inheritPrototype(Man, Person); //拷貝父類原型的副本,無(wú)需執(zhí)行構(gòu)造函數(shù)
Man.prototype.sayAge = function () {
console.log(this.age);
}
var p1 = new Man('wang', 18);
console.log(p1.name); //wang
p1.sayName() //wang
寄生組合式繼承是引用類型最理想的繼承方式。