写在前面:
本文记录的几种继承方式,努力向基于class语法糖实现的面向对象靠拢
class Person { constructor(friends) { this.friends = friends == null ? [] : friends; } sayFriends() { console.log(`${this.name}'s friends are ${this.friends}`); }}class Man extends Person { constructor(name, age, friends) { super(friends); this.name = name; this.age = age; } sayName() { console.log(this.name); } sayAge() { console.log(this.age); }}const xialuo = new Man('xialuo', 20, ['qiuya']);const yuanhua = new Man('yuanhua', 21, []);
以上代码可以表明我们的目的:
- 创建Person类,有属性friends,方法sayFriends
- 创建Man类,在子类调用父类构造函数(super),继承Person类,有属性name、age,方法sayName,sayAge
- friends不能共享
- xialuo、yuanhua是Man也是Person的实例
1. 原型链继承
将父类实例作为子类的原型对象
function Person(friends) { this.friends = friends == null ? [] : friends;}Person.prototype.sayFriends = function () { console.log(this.friends);}// 无法向父类构造函数传递参数,第2点未达成function Man(name, age, friends) { this.name = name; this.age = age;}// 原型链继承Man.prototype = new Person();Man.prototype.sayName = function () { console.log(this.name)}Man.prototype.sayAge = function () { console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');// friends被共享,第3点未达成console.log(xialuo.friends); // []console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // ['teacherWang']console.log(yuanhua.friends); // ['teacherWang']// xialuo、yuanhua是Man也是Person的实例,第4点达成console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // true
缺点:
- 无法向父类构造函数传参
- 父类的引用类型friends被共享
2. 借用构造函数继承
借用父类构造函数增强子类
function Person(friends) { this.friends = friends == null ? [] : friends; this.personMethod = () => { console.log('personMethod run') }}Person.prototype.sayFriends = function () { console.log(this.friends);}function Man(name, age, friends) { Person.call(this, friends); this.name = name; this.age = age;}Man.prototype.sayName = function () { console.log(this.name)}Man.prototype.sayAge = function () { console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');// 未继承到父类Person的原型方法,第2点未达成// xialuo.sayFriends(); // TypeError: xialuo.sayFriends is not a functionxialuo.personMethod(); // 'personMethod run'console.log(xialuo.personMethod === yuanhua.personMethod); // false// friends未被共享,第3点达成!console.log(xialuo.friends); // ['qiuya']console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // [''qiuya, 'teacherWang']console.log(yuanhua.friends); // []// xialuo.__proto__和Person.prototype找不到同一个对象,第4点未达成console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // false
优点:
- 解决原型链继承中无法向父类传参的问题
缺点
- instanceof 无法判断实例是父类的实例
- 无法继承父类Person的原型方法,只能继承父类的自有方法,且无法复用(构造方法造成的)
Lint:为什么要区分原型方法和自有方法?
function Man() { // 自有方法sayHello this.sayHello = () => { console.log('hello') }}Man.prototype.sayWorld = () => { console.log('world');}const xialuo = new Man();const yuanhua = new Man();/** * 私有方法在每次调用构造函数new时都会重新创建,无法复用。 * * 解决办法是将方法添加到原型对象prototype,实例会从Man.prototype调用函数,实现函数复用 */console.log(xialuo.sayHello === yuanhua.sayHello); // falseconsole.log(xialuo.sayWorld === yuanhua.sayWorld); // true
3. 组合式继承
原型链模式 + 借用构造函数模式,集二者之长
function Person(friends) { this.friends = friends == null ? [] : friends;}Person.prototype.sayFriends = function () { console.log(this.friends);}// 子类属性处理function Man(name, age, friends) { // 子类继承父类属性 Person.call(this, friends); // 借用构造函数 // 子类生成自有属性 this.name = name; this.age = age;}// 子类方法处理Man.prototype = new Person(); // 原型链模式,继承父类属性Man.prototype.constructor = Man; // lint: 修复因原型链模式改变的子类构造函数(不然会变成Person)// 子类自有方法Man.prototype.sayName = function () { console.log(this.name)}Man.prototype.sayAge = function () { console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');// friends未被共享,第3点达成!console.log(xialuo.friends); // ['qiuya']console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // [''qiuya, 'teacherWang']console.log(yuanhua.friends); // []console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // true
优点:
集原型链模式和借用构造函数的优点缺点:
创建一个xialuo实例,却调用两次构造函数
目前,我们还需要解决组合式继承的缺点,以得到js继承的最佳实践
4. 原型式继承
function object(o) { function Fn() { } Fn.prototype = o; return new Fn();}function Person(name, age, friends) { this.name = name; this.age = age; this.friends = friends == null ? [] : friends;}var p = new Person('xialuo', 20, ['qiuya']);var xialuo = object(p);p = new Person('yuanhua', 21, []);var yuanhua = object(p);
可以看到,object函数接收一个对象obj,作为临时对象Fn的原型对象,最终返回Fn。
也就是说,xialuo,yuanhua的属性,全在xialuo.__proto__,yuanhua.__proto__,而不是实例对象xialuo,yuanhua上。
这通常会导致原型指针混乱而造成this指向不明的问题。—— 维尔希宁
对了,ES5提供一个方法Object.create()替代了上述的object函数,内部实现是一样的。
<br>
5. 寄生组合式继承
寄生组合式继承是对组合式继承的改造,结合原型式继承,目的是解决组合式继承中两次调用父类构造函数的问题。
function object(obj) { function Fn() { } Fn.prototype = obj; return new Fn();}function Person(friends) { this.friends = friends == null ? [] : friends;}Person.prototype.sayFriends = function () { console.log(this.friends);}function Man(name, age, friends) { Person.call(this, friends); this.name = name; this.age = age;}Man.prototype = object(Person.prototype); // 在此处,减少一次父类构造函数调用Man.prototype.constructor = Man;Man.prototype.sayName = function () { console.log(this.name)}Man.prototype.sayAge = function () { console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');console.log(xialuo.friends); // ['qiuya']console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // [''qiuya, 'teacherWang']console.log(yuanhua.friends); // []console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // true
收工!
本文的思想和代码参考:
JS实现继承的几种方式 - 幻天芒 - 博客园
面向对象的 JavaScript:封装、继承与多态