继承
-
定义:继承是一个类从另一个类获取方法和属性的过程。
-
js的继承:复制父类的属性和方法来重写子类原型对象。
记住这个概念,你会发现JS中的继承都是在实现这个目的,差异是它们的实现方式不同。
实现方式
准备
既然要继承,首先需要一个父类,定义一个Person
父类:
function Person (name) {
this.name = name || 'xuxu' // 实例基本属性 (该属性,强调私有,不共享)
this.like = ['eat'] // 私有,不共享
this.say = function() { // 实例引用属性 (该属性,强调复用,需要共享)
console.log('hello')
}
}
Person.prototype.getInfo = function () { // 将需要复用、共享的方法定义在父类原型上
console.log(this.name + this.sex)
}
1. 原型链继承
-
核心:将父类的实例作为子类的原型
-
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
-
代码:
function p (sex) { this.sex = sex } p.prototype = new Person() let p1 = new p('man') let p2 = new p('woman') // 优点:父类的方法可以被复用,p1和p2共用同一个方法 console.log(p1.say === p2.say) // true // 原型上的方法也可以 console.log(p1.getInfo === p2.getInfo) // true console.log(p1.getInfo()) // xuxuman console.log(p2.getInfo()) // xuxuwoman // 缺点1:父类的引用数据类型被所有子类实例共享 p1.link.push('sleep') console.log(p1.link) // ['eat', 'sleep'] console.log(p2.link) // ['eat', 'sleep']
-
优点:父类的方法可以被复用,原型上的方法也可以。
-
缺点:
- 父类的引用数据类型被所有子类实例共享。
- 子类在实例化的时候不能给父类构造函数传参。
2. 借用构造函数继承
-
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)。
-
借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。 但原型链继承的优点也变成它的缺点。
-
代码:
function p(name, sex) { Person.call(this, name) // 核心 this.sex = sex } let p1 = new p('小红', 'man') let p2 = new p('小明', 'woman ') // 优点1:可以向父类构造函数传参数 console.log(p.name, p.name) // 小红, 小明 // 优点2:子类实例不共享父类构造函数的引用属性 p1.like.push('sleep') console.log(p1.like,p2.like)// ['eat', 'sleep'] ['eat'] // 缺点1:方法不能复用,每次创建子类实例都要创建一遍方法 console.log(p1.say === p2.say) // false (说明,p1和p2 的say方法是独立,不是共享的) // 缺点2:不能继承父类原型上的方法 Person.prototype.walk = function () { // 在父类的原型对象上定义一个walk方法。 console.log('我会走路') } p1.walk // undefined (说明实例,不能获得父类原型上的方法)
-
优点:和原型链继承完全反过来。
- 可以向父类构造函数传参数。
- 子类实例不共享父类构造函数的引用属性。
-
缺点:
- 方法不能复用;由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。
- 不能继承父类原型上的方法,只能继承父类的实例属性和方法。
3. 组合继承
-
通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用。
-
代码:
function p(name, sex) { Person.call(this, name) // 核心 第二次 this.sex = sex } p.prototype = new Person() // 核心 第一次 // 修复构造函数的指向 p.prototype.constructor = p let p1 = new p('小红', 'man') let p2 = new p('小明', 'woman ') // 优点1:可以向父类构造函数传参数 console.log(p.name, p.name) // 小红, 小明 // 优点2:子类实例不共享父类构造函数的引用属性 p1.like.push('sleep') console.log(p1.like,p2.like)// ['eat', 'sleep'] ['eat'] // 优点3:父类原型上的方法可以被复用 console.log(p1.getInfo === p2.getInfo) // true
-
注意:p.prototype 指向的是Person 的实例,p.prototype 上面是没有constructor属性的,就会往上找,这样就找到了Person.prototype上面的constructor属性,导致
p.prototype.constructor === Person
。这就需要将constructor 指向子类的构造函数p。原因:不能判断子类实例的直接构造函数,到底是子类构造函数还是父类构造函数。
-
优点:
- 保留构造函数的优点:1. 可以向父类构造函数传参数。2. 子类实例不共享父类构造函数的引用属性。
- 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用。
-
缺点:两次调用父类的构造函数,存在多余的一份父类的实例属性。
- 第一次执行:
Person.call(this, name)
p 调用了Person 拷贝一份父类实例属性,作为子类的实例属性。 - 第二次执行:
p.prototype = new Person()
创建父类实例作为子类原型。 - 第二次执行完后,父类实例就又有了一份实例属性,但这份会被第一次拷贝来的实例属性屏蔽掉,所以多余。
- 第一次执行:
组合继承优化1
-
核心:砍掉父类的实例属性,这样在调用父类的构造函数的时候,就不会初始化两次实例,避免组合继承的缺点。
-
代码:
function p(name, sex) { Person.call(this, name) // 核心 this.sex = sex } p.prototype = Person.prototype // 核心 子类原型和父类原型,实质上是同一个 // 修复构造函数的指向 p.prototype.constructor = p let p1 = new p('小红', 'man') let p2 = new p('小明', 'woman ') let p3 = new Person() // 缺点:当修复子类构造函数的指向后,父类实例的构造函数指向也会跟着变了。 console.log(p1.constructor) // p console.log(p3.constructor) // p 这里就是存在的问题(我们希望是Person)
-
优点:
- 只调用一次父类构造函数。
- 保留组合继承的优点:
- 保留构造函数的优点:1. 可以向父类构造函数传参数。2. 子类实例不共享父类构造函数的引用属性。
- 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用。
-
缺点:
-
修正构造函数的指向之后,父类实例的构造函数指向,同时也发生变化(这是我们不希望的)
原因:
p.prototype = Person.prototype
子类的原型和父类的原型是一个,所以修改了子类实例的construtor属性,所有的constructor的指向都会发生变化。
-
4. 原型式继承
-
核心:原型式继承的
createObj
方法本质上是对参数对象的一个浅复制。 -
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
function createObj(o) { function F() {} F.prototype = o // 将被继承的对象作为空函数的prototype return new F() // 返回new期间创建的新对象,此对象的原型为被继承的对象, 通过原型链查找可以拿到被继承对象的属性 }
就是 ES5
Object.create
的模拟实现,将传入的对象作为创建的对象的原型。现在都是使用
Object.create()
来做对象的原型继承。 -
代码:
let p1 = createObj(Person) let p2 = createObj(Person) // 优点:父类的方法可以被复用,p1和p2共用同一个方法 console.log(p1.say === p2.say) // true // 原型上的方法也可以 console.log(p1.getInfo === p2.getInfo) // true console.log(p1.getInfo()) // xuxuman console.log(p2.getInfo()) // xuxuwoman // 缺点1:父类的引用数据类型被所有子类实例共享 p1.link.push('sleep') console.log(p1.link) // ['eat', 'sleep'] console.log(p2.link) // ['eat', 'sleep'] p1.name = 'p1'; console.log(p2.name); // xuxu
-
与原型链继承一样,修改
p1.name
的值,p2.name
的值并未发生改变,并不是因为p1 和p2 有独立的name
值,而是因为p1.name = 'p1'
,给p1 添加了name
值,并非修改了原型上的name
值。 -
优点:与原型链一样
- 父类的方法可以被复用,原型上的方法也可以。
-
缺点:与原型链一样
- 父类的引用数据类型被所有子类实例共享。
- 子类在实例化的时候不能给父类构造函数传参。
5. 寄生式继承
-
使用原型式继承的浅复制,创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
-
代码:
function createObj (o) { let clone = Object.create(o) clone.sayName = function () { console.log('hi') } return clone }
-
使用场景:专门为对象来做某种固定方式的增强。
6. 寄生组合继承(组合继承优化2)--- 完美方式
-
解决组合继承 调用两次父构造函数 的缺点。
-
通过寄生式封装函数设置父类prototype为子类prototype的原型来继承父类的prototype声明的属性/方法。
-
代码:
function p(name, sex) { Person.call(this, name) // 核心 this.sex = sex } --------------------------------------------------- // p.prototype = new Person() // 核心 第一次 // 1. 最简单的方式,修改上面这一行 p.prototype = Object.create(Person.prototype) // 核心 不使用 p.prototype = new Person() ,而是间接的让 p.prototype 访问到 Person.prototype // 修复构造函数的指向 p.prototype.constructor = p --------------------------------------------------- // 2. 寄生式继承:封装了p.prototype对象原型式继承Person.prototype的过程,并且增强了传入的对象。 function inheritPrototype(son, father) { const fatherFnPrototype = Object.create(father.prototype) // 原型式继承:浅拷贝father.prototype对象 father.prototype为新对象的原型 son.prototype = fatherFnPrototype // 设置father.prototype为son.prototype的原型 son.prototype.constructor = son // 修正constructor 指向 } inheritPrototype(p, Person) --------------------------------------------------- let p1 = new p('小红', 'man') let p2 = new p('小明', 'woman ') // 优点1:可以向父类构造函数传参数 console.log(p.name, p.name) // 小红, 小明 // 优点2:子类实例不共享父类构造函数的引用属性 p1.like.push('sleep') console.log(p1.like,p2.like)// ['eat', 'sleep'] ['eat'] // 优点3:父类原型上的方法可以被复用 console.log(p1.getInfo === p2.getInfo) // true
-
最成熟的继承方法, 也是现在最常用的继承方法,众多JS库采用的继承方案也是它。
-
引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:
这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
7. ES6 Class extends
-
ES6继承的原理跟寄生组合式继承是一样的。
-
extends
关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor
表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError
错误,如果没有显式指定构造方法,则会添加默认的constructor
方法,使用例子如下。 -
继承代码:
class Person { // constructor constructor(name) { this.name = name this.like = ['eat'] } // Getter get say() { return this.getInfo() } // Method getInfo() { return this.name + this.like } } const pp = new Person('xuxu') console.log(pp.say) // xuxueat ----------------------------------------------------------------- // 继承 class p extends Person { constructor(name, sex) { super(name) // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。 this.sex = sex; } get say() { return this.name + this.like + this.sex } } const p1 = new p('xx', 'man') console.log(p1.say) // xxeatman
-
extends继承的核心代码(通过babel在线编译成es5):
// 寄生式继承 封装继承过程 function _inherits(son, father) { // 原型式继承: 设置father.prototype为son.prototype的原型 用于继承father.prototype的属性/方法 son.prototype = Object.create(father && father.prototype) son.prototype.constructor = son // 修正constructor 指向 // 将父类设置为子类的原型 用于继承父类的静态属性/方法(father.some) if (father) { Object.setPrototypeOf ? Object.setPrototypeOf(son, father) : son.__proto__ = father } }
-
另外子类是通过借用构造函数继承(call)来继承父类通过this声明的属性/方法,也跟寄生组合式继承一样。
-
Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null。
-
ES5继承与ES6继承的区别:
-
ES5的继承实质上是先创建子类的实例对象,再将父类的方法添加到this上。
-
ES6的继承是先创建父类的实例对象this,再用子类的构造函数修改this。
因为子类没有自己的this对象,所以必须先调用父类的
super()
方法。
总结:
-
ES6 Class extends是ES5继承的语法糖。
-
JS的继承除了构造函数继承之外都基于原型链构建的。
-
可以用寄生组合继承实现ES6 Class extends,但是还是会有细微的差别。