js实现继承的六种方式
对红宝书中继承的几种方式的笔记总结。
js中实现继承主要是依靠原型链来完成。
类方式继承后续在补充。
一、原型链继承
// 定义了父类
function Parent() {
console.log('Parent');
}
// 父类的原型上添加一个方法
Parent.prototype.parentFun = function() {
console.log('this is parentFun')
}
// 定义子类
function Child() {
console.log('Child');
}
// 继承了 Parent
Child.prototype = new Parent();
// 子类的原型是添加一个方法
Child.prototype.childFun = function() {
console.log('this is childFun');
}
// 创建一个实例
var instance = new Child();
instance.parentFun(); // this is parentFun
原型链继承的本质其实就是用父类的构造函数创建的实例重写了子类的原型对象
优点:实现了基本的继承,子类共享了父类的方法和属性
缺点:
- 所以子类生成的实例都会共享父类实例中的属性,一旦在实例A中修改父类实例属性,那么所有实例中的那个属性都会生效。下例代码可以用来说明这个问题
- 没有办法再不影响所有对象实例的情况下,给父类的构造函数传递参数
// 定义父类
function Parent() {
this.colors = ['red', 'blue'];
}
// 定义子类
function Child() {
}
// 继承了 Parent
Child.prototype = new Parent();
// 创建实例A
var instanceA = new Child();
instanceA.colors.push('black');
console.log(instanceA.colors); // [ 'red', 'blue', 'black' ]
// 创建实例B
var instanceB = new Child();
console.log(instanceB.colors); // [ 'red', 'blue', 'black' ]
上述代码中Child继承了Parent,当 instanceA修改colors属性时,instanceB中的colors也会生效。
因此实践中很少会单独使用原型链方式继承。
二、借用构造函数(constructor stealing)
借用构造函数又名伪造对象或经典继承
基本思想:在子类型的构造函数的内部调用父类的构造函数
// 定义父类
function Parent() {
this.colors = ['red', 'blue'];
}
// 定义子类
function Child() {
// 继承了 Parent
Parent.call(this);
}
// 创建实例 A
var instanceA = new Child();
instanceA.colors.push('black');
console.log(instanceA.colors); // [ 'red', 'blue', 'black' ]
// 创建实例 B
var instanceB = new Child();
console.log(instanceB.colors); // [ 'red', 'blue']
借用构造函数的本质其实就是 利用call()或apply()在新创建Child实例的环境中调用了Parent构造函数。就会在新Child对象上执行Parent()函数,并且初始化所有对象,使得Child的每个实例都会有自己的colors属性副本了
借用构造函数可以传递参数
// 定义父类
function Parent(name) {
this.name = name
}
// 定义子类
function Child(name, age) {
// 继承了 Parent
Parent.call(this, name);
// 实例属性
this.age = age;
}
// 创建实例A
var instanceA = new Child('jack', 18);
console.log(instanceA.name); // jack
console.log(instanceA.age); // 18
// 创建实例B
var instanceB = new Child('rose', 20);
console.log(instanceB.name); // rose
console.log(instanceB.age); // 20
优点: 可以像父类传递参数
缺点:
- 方法都在构造函数中定义,函数无法复用。
- 父类中原型定义的方法,对子类来说也是不可见的,下列代码可以用来说明这个问题。
// 定义父类
function Parent(name) {
this.name = name
}
Parent.prototype.parentFun = function() {
console.log('parentFun');
}
// 定义子类
function Child(name, age) {
// 继承了 Parent
Parent.call(this, name);
// 实例属性
this.age = age;
}
// 创建实例A
var instanceA = new Child('jack', 18);
console.log(instanceA.name); // jack
console.log(instanceA.age); // 18
instanceA.parentFun(); // TypeError: instanceA.parentFun is not a function
上述代码在执行instanceA.parentFun()时会报TypeError的错误。
因此借用构造函数的技术也很少单独使用。
三、组合继承
又名伪经典继承,其实就是 原型链+借用构造函数
原型链实现原型的属性和方法
构造函数实现对实例的属性继承
// 定义父类
function Parent(name) {
this.name = name;
this.colors = ['red'];
}
Parent.prototype.getName = function() {
console.log(this.name);
}
function Child(name, age) {
// 继承属性
Parent.call(this, name); // 第二次调用 Parent()
this.age = age;
}
// 继承方法
Child.prototype = new Parent(); // 第一次调用Parent()
Child.prototype.getAge = function() {
console.log(this.age);
}
var instanceA = new Child('jack', 20);
instanceA.colors.push('black');
console.log(instanceA.colors); // [ 'red', 'black' ]
instanceA.getName(); // jack
instanceA.getAge(); // 20
var instanceB = new Child('rose', 18);
console.log(instanceB.colors); // [ 'red' ]
instanceB.getName(); // rose
instanceB.getAge(); // 18
console.log(instanceA)
组合继承避免了原型链和构造函数的缺陷,融合了他们的优点。也是js中最常见的继承模式。
组合继承也有缺点:会调用两次父类的构造函数,并且在子类中最终会包含父类对象的全部实例属性,所以第二次调用时会重写这些属性,并且覆盖原型中的属性。
在上例代码的最后一行console.log(instanceA)可以在浏览器中看到如下结构
我们可以看到
实例instanceA中的__proto__这个属性是指向Child.prototype里面有和实例一样的name和colors属性。
四、原型式继承
借用原型可以基于已有的对象创建新对象,但同时不必因此创建自定义类型
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name: 'jack',
color: ['red', 'black']
}
var anotherPerson = object(person);
anotherPerson.name = "rose";
anotherPerson.color.push('green');
console.log(anotherPerson.color); // [ 'red', 'black', 'green' ]
var yetAnotherPerson = object(person);
anotherPerson.color.push('pink');
console.log(yetAnotherPerson.name); // jack
console.log(yetAnotherPerson.color); // [ 'red', 'black', 'green', 'pink' ]
console.log(person.color); // [ 'red', 'black', 'green', 'pink' ]
原型式继承是你必须有一个对象来作为另一个对象的基础,在上列代码中就是person,把person传递给object()函数,在返回的新对象中,将person作为了它的原型,可以看到返回新对象的引用类型值属性是共享的。
ECMAScript5新增了Object.create()方法规范了原型式继承,这个方法接受两个参数:一个用作新对象原型的对象,一个为新对象定义额外属性的对象(可选)。
var person = {
name: 'jack',
color: ['red', 'black']
}
var anotherPerson = Object.create(person); // 调用Object.create()
anotherPerson.name = "rose";
anotherPerson.color.push('green');
console.log(anotherPerson.color); // [ 'red', 'black', 'green' ]
var yetAnotherPerson = Object.create(person); // 调用Object.create()
anotherPerson.color.push('pink');
console.log(yetAnotherPerson.name); // jack
console.log(yetAnotherPerson.color); // [ 'red', 'black', 'green', 'pink' ]
console.log(person.color); // [ 'red', 'black', 'green', 'pink' ]
可以看到传一个参数时Object.create()和我们自己写的object()方法的行为是相同的。
var person = {
name: 'jack',
age: 18
};
var anotherPerson = Object.create(person, {
name: {
value: 'rose'
},
age: {
value: 20
}
});
console.log(anotherPerson) // {name: "rose", age: 20}
Object.create()传递第二参数时如果与原型对象上的属性同名,则会覆盖。、
五、寄生式继承(parasitic)
寄生式继承的思路是创建一个封装继承过程的函数,来增强对象,与我们常用的高阶组件思想类似。
function createAnother(original) {
var clone = Object.create(original);
clone.sayHello = function() {
console.log('hello');
};
return clone;
}
var person = {
name: 'jack',
color: ['red', 'black']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHello(); // hello
寄生式继承无法做到函数复用。
六、寄生组合式继承
function inheritPrototype(child, parent) {
var prototype = Object.create(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
child.prototype = prototype; // 指定对象
}
function Parent(name) {
this.name = name;
this.color = ['red', 'black'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log(this.age);
}
var instanceA = new Child('jack', 18);
instanceA.color.push('pink');
console.log(instanceA.name); // jack
console.log(instanceA.color); // [ 'red', 'black', 'pink' ]
var instanceB = new Child('rose', 20);
console.log(instanceB.color); // [ 'red', 'black' ]
console.log(instanceB.name); // rose
寄生式组合继承的最大优点就是避免了在Child.prototype上面创建不必要、多余的属性。