理解JS中如何实现继承
理解JS中如何实现继承
很多面向对象语言都支持两种继承:接口继承和实现继承。 前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
TLDR: JS中继承终极方法即寄生组合式,组合式继承模式的核心就是产生一个__proto__指向父类原型的对象,两种方法1.组合式:使用父类型构造函数,但性能差,2.寄生组合式:生成一个prototype指向父类原型的轻量构造函数,使用该构造函数生成对象
1.基本概念
1.1 为什么需要继承
使用继承可以复用并扩展原有代码,减少重复性工作。
1.2 JS中的原型
核心三点:
- 每个构造函数有prototype指针指向原型对象:F.prototype = 原型对象
- 原型对象有指针constructor指回构造函数: 原型对象.constructor = F
- 由构造函数生成的实例,有指针__proto__指向原型对象: f.__ proto__ = 原型对象
1.3 JS中创建对象的模式
构造对象是JS实现继承的基石,使用Object 构造函数(如Object.defineProperty)或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。下面介绍几种在继承中常用的构造对象模式。
1.3.1 工厂模式
console.log('1.工厂模式');
let createPerson = function(name,age){
let obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function(){
console.log('- I am',this.name);
};
return obj;
}
let factoryPerson = createPerson('factory',1);
factoryPerson.sayName();
1.3.2 构造函数实现工厂模式
console.log('2.构造函数实现工厂模式');
// function PersonFactory(name,age){
let PersonFactory = function(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log('- I am',this.name);
}
//默认return this
}
let factoryPerson = new PersonFactory('factoryByNew',2)
factoryPerson.sayName();
let bool = factoryPerson.__proto__ === PersonFactory.prototype;
console.log('- factoryPerson.__proto__ === PersonFactory.prototype',bool);
console.log('- 问题: 函数会重复声明,无法复用');
let factoryPerson1 = new PersonFactory('factoryByNew1',2);
bool = factoryPerson.sayName === factoryPerson1.sayName;
console.log('- factoryPerson.sayName === factoryPerson1.sayName',bool);
1.3.3 解决构造函数模式,无法复用函数;
console.log('3.解决构造函数模式,无法复用函数');
let sayName = function(){
console.log('- I am',this.name);
}
let ShareFactory = function(name,age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
let shareFactory1 = new ShareFactory('shareFactory1',3)
let shareFactory2 = new ShareFactory('shareFactory2',3)
let bool = shareFactory1.sayName === shareFactory2.sayName;
console.log('- shareFactory1.sayName === shareFactory2.sayName',bool);
1.3.4 原型模式
function proto(){}
proto.prototype.name = 'proto';
proto.prototype.age = 4;
proto.prototype.sayName = sayName;
let protoPerson1 = new proto();
let protoPerson2 = new proto();
let bool = protoPerson1.sayName === protoPerson2.sayName;
console.log('- protoPerson1.sayName === protoPerson2.sayName',bool);
console.log('- 问题: 属性也会复用,没有独立的实例属性');
let bool = protoPerson1.friend === protoPerson2.friend;
console.log('- protoPerson1.friend === protoPerson2.friend',bool);
1.3.5 最佳实践
//将私有属性放到构造函数上
let Person = function(name,age){
this.name = name;
this.age = age;
}
//将共有属性放到原型上
Person.prototype.sayName = sayName;
Person.prototype.description = 'person';
const person = new Person('howard',24)
person.sayName();
下面结合示例代码,JS中实现继承的6种模式。
2. JS中实现继承的6种模式
JS中实现继承的具体目标就是:实现子类型实例能够访问父类原型上的方法和属性,同时有希望具有自己独享的实例属性。
2.1 原型链
原型链是实现继承的一种方式,具体思路如下:A和B是两个构造函数,通过让一个对象同时拥有两个身份,既是B的原型对象,又是A的实例对象,连接两个构造函数,实现B的实例继承A原型上的属性和方法。
let Father = function(){
this.name = "father";
this.friends = [1,2];
this.other = arguments;
};
let sayName = function(){
console.log('- I am',this.name);
}
let sayHi = function(){
console.log('- Hi, my name is:',this.name);
}
Father.prototype.sayName = sayName;
let Son = function(){
this.age = 18;
};
Son.prototype = new Father();
let son1 = new Son();
son1.sayName();
let son2 = new Son()
但该方式存在两个问题
- 问题1: 实例属性变成了原型属性,产生错误的共享数据
- 问题2: 无法向父类型传参生成独享的属性,传参都会使子类型所有实例发生改变
2.2 盗用构造函数——经典继承
为了独享数据,可以在子类型构造函数中,调用父类型构造函数,实现继承并独享父类型的实例对象属性。
console.log('2.盗用构造函数——经典继承');
console.log('- 在子类型构造函数中,调用父类型构造函数,实现继承并独享父类型的实例对象属性');
Grandpa = function(){
this.money = 10;
}
Father = function(name){
Grandpa.call(this);
this.friends = [1,2];
this.name = name;
}
Father.prototype.sayName = sayName;
Son = function(name,age){
Father.call(this,name);
this.age = age;
this.sayHi = sayHi;
}
son1 = new Son('son1');
son2 = new Son('son2');
father1 = new Father('father');
son1.friends.push(3);
console.log('- son1.friends === son2.friends',son1.friends === son2.friends);
console.log('- 并且可以传递参数');
son1.sayHi();
son2.sayHi();
但该方式存在两个问题
- 问题1: 无法在保持代码紧凑的同时重用函数(和构造函数模式问题相同)
- 问题2: 无法访问父类原型上的方法,只能逐层使用构造函数模式
console.log('问题1: 无法在保持代码紧凑的同时重用函数(和构造函数模式问题相同)');
console.log('问题2: 无法访问父类原型上的方法,只能逐层使用构造函数模式');
console.log('- sayName in father1','sayName' in father1);
console.log('- sayName in son1','sayName' in son1);
console.log('- money in son1 ','money' in son1);
2.3 组合式继承——原型链+盗用构造函数
融合前两种方式的优点,组合式继承通过原型链继承原型上的方法和属性,通过构造函数继承实例属性
console.log('3.组合继承——原型链+盗用构造函数');
console.log('通过原型链继承原型上的方法和属性,通过构造函数继承实例属性(可独享)');
Father = function(name){
this.friends = [1,2];
this.name = name;
}
Father.prototype.sayName = sayName;
Father.prototype.lastName = 'Yang';
Son = function(name,age){
Father.call(this,name);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.sayHi = sayHi;
son1 = new Son('mixin-son');
father1 = new Father('father');
console.log('- sayName in father1','sayName' in father1);
console.log('- sayName in son1','sayName' in son1);
son1.sayHi();
然而组合式继承仍有两个问题
- 问题1: 两次调用父类构造函数会产生的效率问题
- 问题2: 第一滴调用时,子类原型对象上增加了不必要的,由父类型构造函数产生的属性
console.log('- 第1次Son.prototype = new Father()');
console.log('- 第2次Father.call(this)')
组合式继承在功能上已经满足了大部分要求,为了解决其性能问题和不必要的副作用,通过“原型式继承”->“寄生式继承”->最终得到“寄生组合式继承”解决了该问题。
2.4 原型式继承
console.log('4.原型式继承');
console.log('不自定义类型,即不通过构造函数生成原型对象,而是直接将需要共享信息的对象作为原型对象,实现继承');
console.log('实际上是一种创建对象的方式,与原型模式相同');
function object(o){
function F(){};
F.prototype = o;
return new F();
}
let father = {
fisrtname : 'Yang',
name : 'father',
friends : [1,2]
};
son1 = object(father);
son2 = object(father);
son3 = Object.create(father);
console.log('- objectson1.friends === son2.friends',son1.friends === son2.friends);
console.log('- Object.create() son3.friends === son1.friends',son3.friends === son1.friends);
2.5 寄生式继承
console.log('5.寄生式继承');
console.log('同样直接将需要被继承/共享信息的对象作为原型对象,融合工厂模式,直接返回一个增加了方法的对象');
function createSon(father){
let clone = object(father);//原型式继承
clone.sayHi = function(){
console.log('- I am ',this.name);
}
return clone;
}
son1 = createSon(father);
son2 = createSon(father);
son1.sayHi();
console.log('问题: 无法在保持代码紧凑的同时重用函数(和构造函数模式问题相同)');
2.6 寄生组合式继承
寄生组合式继承 vs 组合式继承: 组合式继承模式的核心就是产生一个__proto__指向父类原型的对象 两种方法
- 组合式:使用父类型构造函数,但性能差
- 寄生组合式:生成一个prototype指向父类原型的轻量构造函数,使用该构造函数生成对象
console.log('6.寄生组合式继承——组合式继承优化');
console.log('避免组合式继承,两次调用父类构造函数产生的效率问题');
console.log('避免子类原型对象上不必要的,由父类型构造函数产生的属性');
console.log('- 第1次Son.prototype = new Father()');
console.log('- 第2次Father.call(this)');
console.log('避免第一次,通过调用父类构造函数给子类原型赋值,而是使用寄生式继承,使用父类原型对象产生赋值给子类原,实现继承父类原型');
function inheritPrototype(Son, Father){
let prototype = object(Father.prototype);
prototype.constructor = Son;
Son.prototype = prototype;
}
Father = function(name){
this.name = name;
}
Father.prototype.sayName = sayName;
Son = function(name,age){
Father.call(this,name);
this.age = age;
}
inheritPrototype(Son,Father);
Son.prototype.sayHi = sayHi;
son1 = new Son('son1',1);
son2 = new Son('son2',2);
son1.sayName();
son2.sayName();
son2.sayHi();