写在前面:
本文记录的几种继承方式,努力向基于 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); // true
console.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 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
优点:
- 解决原型链继承中无法向父类传参的问题
缺点
- 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); // 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:封装、继承与多态