js继承的方式
方式一:使用原型链继承
例1、基本模式
// 父类构造函数
function Super() {
// 父类的属性
this.SuperName = '影子';
}
// 父类原型上的方法
Super.prototype.getSuperName = function () {
return this.SuperName;
}
// 子类的属性
function Sub() {
this.subName = '小古';
}
// Sub继承Super
Sub.prototype = new Super();
let instance1 = new Sub();
console.log(instance1.getSuperName()); //影子
console.log(instance1.constructor); //指向Super
- 通过创建Super的实例并将其赋值给Sub原型Sub.prototype,实现了对Super的继承;
- 重写了Sub最初的原型,将其替换为Super的实例,这意味着Super实例可以访问的所有属性和方法都会存在于Sub.prototype上。
例2、子类需要覆盖父类的方法或者增加父类没有的方法
// 父类的属性
function Super() {
this.SuperName = '影子';
}
// 父类原型上的方法
Super.prototype.getSuperName = function () {
return this.SuperName;
}
// 子类的属性
function Sub() {
this.subName = '小古';
}
// Sub继承Super
Sub.prototype = new Super();
// 当子类需要覆盖父类的方法或者增加父类没有的方法时需要在原型赋值之后再添加到子类原型上
// 子类覆盖父类身上的方法
Sub.prototype.getSuperName = function () {
return this.SuperName = '敏宝';
};
// 子类原型上增加新方法
Sub.prototype.getSubName = function () {
return this.subName;
}
let instance1 = new Sub();
let instance2 = new Super();
console.log(instance1.getSuperName()); //敏宝
console.log(instance2.getSuperName()); //影子
- 当子类覆盖父类的方法时,子类的实例会调用覆盖后的方法,所以instance1实例会输出“敏宝”;
- 而父类的实例仍然会调用最初的方法,所以instance2实例会输出“影子”;
例3、当以对象字面量方式创建原型方法会破坏之前的原型链,因为这样相当于重写了原型链
// 父类的属性
function Super() {
this.SuperName = '影子';
}
// 父类原型上的方法
Super.prototype.getSuperName = function () {
return this.SuperName;
}
// 子类的属性
function Sub() {
this.subName = '小古';
}
// Sub继承Super
Sub.prototype = new Super();
// 通过对象字面量添加新方法,这会导致上一行的继承无效
Sub.prototype = {
getSubName() {
return this.subName
}
}
let instance1 = new Sub();
console.log(instance1.getSuperName()); //报错
在例3中,子类Sub的原型被赋值为Super实例之后,又被一个对象字面量覆盖了,覆盖后的原型是一个Object的实例,而不再是Super的实例,因此之前的原型链就断了,Sub与Super就没有关系了。
例4、原型链存在的问题
// 父类的属性
function Super() {
this.nums = [1,2,3];
}
function Sub() { };
// Sub继承Super
Sub.prototype = new Super();
let instance1 = new Sub();
instance1.nums.push(4);
console.log(instance1.nums);//[1,2,3,4]
let instance2 = new Sub();
console.log(instance2.nums);//[1,2,3,4]
console.log(instance1.nums === instance2.nums);//true
原型链的问题:
(1)原型中包含的引用值会在所有实例中共享,每个Super实例都会有nums这个属性,而Sub.prototype是Super的实例,因而获得了自己的nums属性;最终所有的实例都会共享nums这个属性,因此instance1.nums的修改会影响到instance2.nums;
(2)子类在实例化时不能给父类的构造函数传参,因此我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数中,因此原型链继承方式不会被单独使用。
方式二:盗用构造函数--解决原型包含引用值导致的继承问题
例1、基本模式
function Super() {
this.nums = [1, 2, 3];
}
function Sub() {
// 子类Sub继承父类Super
Super.call(this);
}
let instance1 = new Sub();
instance1.nums.push(4);
console.log(instance1.nums);//[1,2,3,4]
let instance2 = new Sub();
console.log(instance2.nums);//[1,2,3]
通过使用call()或者apply()方法,Super构造函数在为Sub的实例创建的新对象的上下文中执行了,相当于在新的Sub对象上运行了Super函数中的所有初始化代码,结果就是每个实例都会有自己的nums属性。
例2、在子类构造函数中向父类构造函数传参
// 父类构造函数
function Super(name) {
this.name = name;
}
// 子类构造函数
function Sub() {
// 子类Sub继承父类Super并传递参数
Super.call(this, '影子');
// 实例自身的属性
this.age = 18;
}
let instance1 = new Sub();
console.log(instance1.name);//影子
console.log(instance1.age);//18
- 相比于原型链继承,盗用构造函数可以在子类构造函数中向父类构造函数传递参数;
- 为了确保Super构造函数不会覆盖子类Sub定义的属性,需要在调用父类Super构造函数之后再给子类Sub实例添加额外的属性。
- 盗用构造函数的问题: (1)必须要在构造函数中定义方法,因此函数不能重用;
(2)子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式,因此盗用函数继承方式不会被单独使用;
方式三:组合继承(原型链继承+构造函数继承)
function Super(name) {
this.nums = [1, 2, 3];
this.name = name;
}
Super.prototype.sayName = function () {
console.log(this.name);
}
function Sub(name,age) {
// 继承属性
Super.call(this, name); //第二次调用Super构造函数
this.age = age;
}
// 继承方法
Sub.prototype = new Super(); //第一次调用Super构造函数
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function () {
console.log(this.age)
}
let instance1 = new Sub('影子',18);
instance1.nums.push(4);
console.log(instance1.nums); //[1,2,3,4]
instance1.sayName(); //影子
instance1.sayAge(); //18
let instance2 = new Sub('小古',19);
console.log(instance2.nums); //[1,2,3]
instance2.sayName(); //小古
instance2.sayAge(); //19
- Super构造函数定义了两个属性nums和name,在原型上定义了一个方法sayName;子类Sub构造函数在调用父类Super构造函数的时候传递了参数name,通过盗用构造函数的方式实现对父类Super上属性的继承;此后,Sub的原型被赋值为Super实例,通过原型链的方式实现对父类身上方法的继承;
- 优点:组合继承方式弥补了原型链继承和盗用构造函数继承的不足,使用最为广泛。
- 缺点:存在效率问题,父类构造函数始终会被调用两次,一次是在创建子类原型时调用,另一次是在子类构造函数中调用。
方式四:寄生组合式继承
寄生组合继承的核心逻辑:
//该函数接收两个参数:子类构造函数和父类构造函数
function inheritPrototype(Sub, Super) {
let pro = object(Super.prototype); //创建对象
pro.constructor = Sub; //增强对象
Sub.prototype = pro; //赋值对象
}
在这个函数内部,第一步是创建父类原型的一个副本;然后给返回的pro对象设置constructor属性,解决由于重写原型导致默认的constructor丢失的问题;最后将新创建的对象pro赋值给子类型的原型。
寄生组合继承方式:
function Super(name) {
this.name = name;
this.nums = [1, 2, 3];
}
Super.prototype.sayName = function () {
console.log(this.name);
}
function Sub(name, age) {
Super.call(this, name);//调用一次Super构造函数
this.age = age;
}
inheritPrototype(Sub, Super);
Sub.prototype.sayAge = function () {
console.log(this.age)
}
这里只调用了一次Super构造函数,避免了Super.prototype上不必要也用不到的属性,效率更高,而且原型链仍保持不变。