本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
js早期没有类的概念,直到后来es6引入了class,在这之前关于js如何实现继承,主要靠的就是原型链。
本文循序渐进的介绍关于原型链的各种继承方式的演变,这些继承方式的特点。
首先学习继承之前,要对原型链有一定程度的了解。
不了解可以去先阅读我另一篇文章,里面对原型链有一个较为详细的说明:js 原型链详解。
不了解call方法的用法的话,提前阅读:js apply、call、bind一篇掌握。
如果已经了解请继续。
原型链继承
父类实例作为子类的原型。
子类创造的两个实例的隐式原型__proto__指向父类的那个实例。
而父类的实例的隐式原型__proto__又指向父类的原型father.prototype
根据原型链的特性,将这张图理解清晰,后续我就不再重复解释了。
//父类
function father() {
this.fatherAttr = ["fatherAttr"];
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
//子类
function child() {}
// 将father实例作为child这个构造函数的原型
child.prototype = new father();
child.prototype.constructor = child;
//两个子类实例
const test1 = new child();
const test2 = new child();
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
特点:
-
两个实例对象都没有fatherAttr属性,但是因为父类的实例会拥有fatherAttr属性,且现在父类的实例作为child的原型,根据原型链,他们可以共享到自己的构造函数child的原型上的属性。(测试1)
-
因为只有一个父类的实例作为他们的原型,所以所有实例共享了一个原型上的属性fatherAttr,当原型上的属性作为引用类型时,此处是数组,test1添加一个新内容会导致test2上的fatherAttr也改变了。(测试2)(
缺点
) -
child构造函数不能传递参数到父类原型上。(
缺点
) -
实例可以访问到父类的原型上的属性,因此可以把可复用方法定义在父类原型上。(测试3)
构造函数继承
将父类上的this绑定到子类,也就是父类上创建的属性会拷贝到子类这个构造函数上,所以实例会继承这些属性。
//父类
function father(params) {
this.fatherAttr = ["fatherAttr"];
this.params = params;
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
//子类
function child(params) {
father.call(this, params);
}
//两个子类实例
const test1 = new child("params1");
const test2 = new child("params2");
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
特点:
-
两个实例对象都拥有了拷贝来的fatherAttr属性,所以没有共享属性,创造一个实例就得拷贝一次父类的所有属性,且因为不能继承父类原型,所以方法不能复用,被迫拷贝方法。(测试1)(
缺点
) -
test1添加一个新内容只是改变了test1自己的属性,不会影响到test2。(测试2)
-
child构造函数可以传递参数,定制自己的属性。(测试1)
-
实例不能继承父类的原型上的属性。(测试3)(
缺点
)
组合继承
结合原型链继承和构造函数继承,可以根据两种继承特点进行使用。
//父类
function father(params) {
this.fatherAttr = ["fatherAttr"];
this.params = params;
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
//子类
function child(params) {
//第二次调用了父类构造函数
father.call(this, params);
}
// 将father实例作为child构造函数的原型
child.prototype = new father();//第一次调用了父类构造函数
child.prototype.constructor = child;
//两个实例
const test1 = new child("params1");//从这里跳转去子类构造函数第二次调用了父类构造函数
const test2 = new child("params2");
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
console.log("测试4:");
delete test1.fatherAttr
console.log("test1:", test1);
console.log("test1.fatherAttr:", test1.fatherAttr);
特点:
-
两个实例对象都拥有了拷贝来的fatherAttr属性,创造一个实例就得拷贝一次父类的所有属性(构造函数继承特点,测试1),但是能访问父类原型,可以把复用方法定义在父类原型上。(原型链继承特点,测试1)
-
test1添加一个新内容只是改变了test1自己的属性,不会影响到test2。(构造函数继承特点,测试2)
-
child构造函数可以传递参数,定制自己的属性。(构造函数继承特点,测试1)
-
实例能继承父类的原型上的属性。(原型链继承特点,测试3)
-
调用了两次父类的构造函数,生成两份实例,创建子类原型链一次,用子类创建实例时,子类内部里面一次,第二次覆盖了第一次。(
缺点
) -
因为调用两次父类构造函数,如果用delete删除实例上拷贝来的fatherAttr属性,实例仍然拥有隐式原型指向的父类实例上的fatherAttr属性。(原型链继承特点,测试4)(
缺点
)
原型式继承
就是封装一个方法,在方法中创建一个临时的构造函数,入参为父类实例,将临时构造函数的原型绑定到传入的实例上并返回。
//父类
function father() {
this.fatherAttr = ["fatherAttr"];
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
function create(obj) {
function child() {}
child.prototype = obj;
return new child();
}
const fatherObj = new father();
//两个实例
const test1 = create(fatherObj);
const test2 = create(fatherObj);
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
ECMAScript5新增了Object.create()方法规范化了原型式继承,这里的create函数中的代码就是Object.create()的核心部分,Object.create()还提供第二个参数,这里就不展开介绍了。
用Object.create()进行原型式继承:
//父类
function father() {
this.fatherAttr = ["fatherAttr"];
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
// function create(obj) {
// function child() {}
// child.prototype = obj;
// return new child();
// }
const fatherObj = new father();
//两个实例
const test1 = Object.create(fatherObj);
const test2 = Object.create(fatherObj);
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
结果一致。
特点:
- 看测试结果,貌似与原型链方法特点一致。
寄生式继承
在原型式继承的基础上,再多封装一个函数,用于增强对象。
//父类
function father() {
this.fatherAttr = ["fatherAttr"];
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
function create(obj) {
function child() {}
child.prototype = obj;
return new child();
}
function afterCreate(obj) {
const clone = create(obj);
clone.createAttr = "createAttr";
return clone;
}
const fatherObj = new father();
//两个实例
const test1 = afterCreate(fatherObj);
const test2 = afterCreate(fatherObj);
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
特点:
- 在原型式继承的基础上,可以拥有afterCreate构造方法内部自带的属性,创建实例时,afterCreate方法自带的属性就自动寄生在实例身上,也就是能让所有调用afterCreate继承于任何不同对象的实例都会拥有这么一个属性。
寄生组合继承
结合寄生式继承和组合式继承,与寄生式继承的区别是:寄生式继承继承的是父类的实例,目的无非是需要父类的原型,那么我们去掉创建父类实例的那一步,直接把父类原型拿去继承,用增强对象的方式去实现将子类的原型指向父类的原型,不再用父类的实例作为子类的原型。
//父类
function father(params) {
this.fatherAttr = ["fatherAttr"];
this.params = params;
}
//父类的原型上的属性
father.prototype.checkProto = "checkProto";
function child(params) {
father.call(this, params);
}
function create(obj) {
function child() {}
child.prototype = obj;
return new child();
}
function afterCreate(ch, fa) {
const clone = create(fa.prototype);
clone.constructor = ch;
ch.prototype = clone;
}
afterCreate(child, father);
//两个实例
const test1 = new child("params1");
const test2 = new child("params2");
console.log("测试1:");
console.log("test1:", test1);
console.log("test2:", test2);
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试2:");
test1.fatherAttr.push("newAttr");
console.log("test1.fatherAttr:", test1.fatherAttr);
console.log("test2.fatherAttr:", test2.fatherAttr);
console.log("测试3:");
console.log("test1.checkProto:", test1.checkProto);
console.log("测试4:");
delete test1.fatherAttr;
console.log("test1:", test1);
console.log("test1.fatherAttr:", test1.fatherAttr);
特点:
- 拥有组合式继承的所有优点,且用寄生的方式修复了组合继承会调用两次父类构造函数的问题,也因为修复的这个问题,解决了删除对应实例上的属性,仍然能访问到原型链上属性的问题。