乐趣区

JavaScript面向对象之继承

写在前面:

本文记录的几种继承方式,努力向基于 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, []);

以上代码可以表明我们的目的:

  1. 创建 Person 类,有属性 friends,方法 sayFriends
  2. 创建 Man 类,在子类调用父类构造函数(super),继承 Person 类,有属性 name、age,方法 sayName,sayAge
  3. friends 不能共享
  4. 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); // true
console.log(xialuo instanceof Person); // true

缺点:

  1. 无法向父类构造函数传参
  2. 父类的引用类型 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 function
xialuo.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); // true
console.log(xialuo instanceof Person); // false

优点:

  1. 解决原型链继承中无法向父类传参的问题

缺点

  1. instanceof 无法判断实例是父类的实例
  2. 无法继承父类 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); // false
console.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); // true
console.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); // true
console.log(xialuo instanceof Person); // true

收工!

本文的思想和代码参考:

JS 实现继承的几种方式 – 幻天芒 – 博客园

面向对象的 JavaScript:封装、继承与多态

退出移动版