ES5、ES6常见的7种继承
继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
构造函数
一个函数可以作为创建对象实例的构造函数,也可以当做普通函数使用,区别就是在于是否使用了 new
关键字。
构造函数在创建对象的时候,会执行如下的操作:
- 在内存中创建一个对象
- 在新对象内部的
[[Prototype]]
属性指向构造函数的prototype
属性 - 构造函数内部的
this
指向这个创建的新对象 - 执行构造函数内部的代码,比如添加一些属性、方法等
- 如果构造函数有返回值,该返回值为非空对象,则返回该对象,否则返回创建的新对象。
继承方式(ES5)
下面是一些 ES5
中常见的继承。
1、原型链继承
核心:
将父类的实例作为子类的原型。
SubType.prototype = new SuperType();
// 要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubType.prototype.constructor = SubType;
// 不能通过对象字面量的方式添加新方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
优点:
- 简单。
- 父类方法可以复用。
缺点:
- 子类之间会共享引用类型属性。
- 创建子类时,无法向父类构造函数传参, 这样就会使子类实例没法自定义自己的属性。
举例🌰:
// 创建子类Child,使用原型和构造函数的方式继承父类People的方法,并调用say函数说出姓名和年龄。
// 父类:
function People(name, age) {
this.name = name;
this.age = age;
this.arr = [1];
this.say = function () {
console.log('我的名字是:' + this.name + ',我今年' + this.age + '岁。');
};
}
People.prototype.sayHi = function () {
console.log('Hi');
};
// 子类:
function Child(name, age) {
this.name = name;
this.age = age;
}
// 原型链继承:
Child.prototype = new People();
// 所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向People
Child.prototype.constructor = Child;
let child = new Child('Rainy', 20);
child.say(); // 我的名字是:Rainy,我今年20岁。
child.arr.push(2);
console.log(child.arr); // [ 1, 2 ]
let child1 = new Child('Jack', 23);
child1.say(); // 我的名字是:Jack,我今年23岁。
console.log(child1.arr); // [ 1, 2 ]
child1.sayHi(); // Hi
// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
// instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型
console.log(child instanceof Child); // true
console.log(child instanceof People); // true
2、构造函数(经典继承)
核心:
将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到
prototype
的继承。
优点:
- 避免了引用类型的属性被所有实例共享。
- 子类可以向父类传参。
缺点:
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
- 无法复用父类的公共函数。
- 每次子类构造实例都得执行一次父类函数。
举例🌰:
// 父类:
function People(name, age) {
this.name = name;
this.age = age;
this.arr = [1];
this.say = function () {
console.log('我的名字是:' + this.name + ',我今年' + this.age + '岁。');
};
}
People.prototype.sayName = function () {
console.log(this.name);
};
// 子类:
function Child(name, age) {
// 构造函数继承(经典继承):
People.call(this, name, age);
}
let child = new Child('Rainy', 20);
child.say(); // 我的名字是:Rainy,我今年20岁。
child.arr.push(2);
console.log(child.arr); // [ 1, 2 ]
let child1 = new Child('Jack', 23);
child1.say(); // 我的名字是:Jack,我今年23岁。
console.log(child1.arr); // [ 1 ]
// 父类的方法(构造函数外定义的方法)不能复用
child.sayName(); // Error: child.sayName is not a function
3、组合继承(伪经典继承)
核心:
原型式继承和构造函数继承的组合,兼具了二者的优点。
基本的思路是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
优点:
- 可以在创建子类实例时向父类构造函数传参。
- 父类的方法可以被复用。
- 父类的引用属性不会被共享。
缺点:
- 调用了两次父类的构造函数,第一次是
Child.prototype = new Parent()
,第二次是Parent.call(this, name)
,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。
举例🌰:
// 父类:
function People(name, age) {
this.name = name;
this.age = age;
this.arr = [1];
this.say = function () {
console.log('我的名字是:' + this.name + ',我今年' + this.age + '岁。');
};
}
People.prototype.sayName = function () {
console.log(this.name);
};
// 组合继承(伪经典继承):
// 子类:
function Child(name, age) {
// 第二次调用父构造函数People
People.call(this, name, age);
}
// 第一次调用父构造函数People
Child.prototype = new People();
Child.prototype.constructor = Child;
let child = new Child('Rainy', 20);
child.say(); // 我的名字是:Rainy,我今年20岁。
child.arr.push(2);
console.log(child.arr); // [ 1, 2 ]
let child1 = new Child('Jack', 23);
child1.say(); // 我的名字是:Jack,我今年23岁。
console.log(child1.arr); // [ 1 ]
delete child.arr;
console.log(child.arr); // [ 1 ]
// 子类可以调用父构造函数外定义的方法
child.sayName(); // Rainy
4、原型式继承:
核心:
原型式继承的
object
方法本质上是对参数对象的一个浅复制。
原型式继承并没有使用严格意义上的构造函数,是通过借助原型基于已有的对象创建新对象,同时还不必创建自定义类型。本质上就是对传入的对象进行了一次浅复制。
优点:
- 父类方法可以复用。
缺点:
- 父类的引用属性会被所有子类实例共享。
- 子类构建实例时不能向父类传递参数。
举例🌰:
// 核心代码
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
name: 'Tom',
list: [1, 2, 3]
};
let p1 = object(person);
// ES5通过新增 Object.create() 方法规范化了原型式继承
// let p1 = Object.create(person);
p1.name = 'Jack';
p1.list.push(4);
console.log(person.name); // Tom
console.log(p1.name); // Jack
let p2 = Object.create(person, {
name: { value: 'Helen' }
});
p2.list.push(5);
console.log(p2.name); // Helen
console.log(person.list); // [ 1, 2, 3, 4, 5 ]
delete p2.name;
console.log(p2.name); // Helen
ECMAScript 5
通过新增Object.create()
方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()
与object()
方法的行为相同。即
var p1 = object(person);
等同于var p1 = Object.create(person);
5、寄生式继承:
核心:
使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
优点:
- 寄生式继承在主要考虑对象而不是创建自定义类型和构造函数时,是十分有用的。
缺点:
- 跟构造函数继承模式一样,每次创建对象都会创建一遍方法。(使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。)
- 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。
举例🌰:
// 寄生式继承
function createPerson(o) {
// 核心代码
var clone = Object.create(o);
clone.sayHi = function () {
console.log('Hi');
};
return clone;
}
var person = {
name: 'Tom',
age: [1, 2, 3]
};
var p1 = createPerson(person);
p1.sayHi(); // Hi
p1.name = 'Jack';
console.log(p1.name); // Jack
console.log(person.name); // Tom
p1.age.push(4);
console.log(person.age); // [ 1, 2, 3, 4 ]
6、寄生组合式继承
核心:
最佳的的继承方式,组合继承会调用两次父类构造函数,存在效率问题。其实本质上子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
优点:
- 不必为了指定子类型的原型而调用父类型的构造函数。
- 这种方式的高效率体现它只调用了一次
Parent
构造函数,并且因此避免了在Parent.prototype
上面创建不必要的、多余的属性。 - 与此同时,原型链还能保持不变;因此,还能够正常使用
instanceof
和isPrototypeOf
。 - 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
缺点:
- 太复杂了。
举例🌰:
// 寄生组合式继承
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype); // 创建了父类原型的浅复制
prototype.constructor = subType; // 修正原型的构造函数
subType.prototype = prototype; // 将子类的原型替换为这个原型
}
// 父类:
function Animal(name) {
this.name = name;
this.arr = [1, 2, 3];
}
Animal.prototype.sayName = function () {
console.log(this.name);
};
// 子类:
function Cat(name, age) {
Animal.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(Cat, Animal);
Cat.prototype.sayAge = function () {
console.log(this.age);
};
Cat.prototype.sayName = function () {
console.log('catName');
};
var cats = new Cat('Tom', 3);
var animals = new Animal('Boss');
console.log(typeof animals.sayName); // function
console.log(typeof animals.sayAge); // undefined(Cat独有的方法)
animals.sayName(); // Boss
cats.sayName(); // catName
cats.sayAge(); // 3
console.log(cats.__proto__.prototype); // undefined
继承方式(ES6)
ES6
中的继承。
ES6 Class 继承
核心:
ES6
继承的结果和寄生组合继承相似,本质上,ES6
继承是一种语法糖。但是,寄生组合继承是先创建子类实例this
对象,然后再对其增强;而ES6
先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
class A {}
class B extends A {
constructor() {
super();
}
}
// 核心代码:
class A {}
class B {}
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
};
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
总结:
ES6 Class extends
是ES5
继承的语法糖。JS
的继承除了构造函数继承之外都基于原型链构建的。- 可以用寄生组合继承实现
ES6 Class extends
,但是还是会有细微的差别。
ES6 继承与 ES5 继承的异同:
-
相同点:本质上
ES6
继承是ES5
继承的语法糖。 -
不同点:
-
ES6
继承中子类的构造函数的原型链指向父类的构造函数,ES5
中使用的是构造函数复制,没有原型链指向。 -
ES6
子类实例的构建,基于父类实例,ES5
中不是。
-
举例🌰:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return this.x + '-' + this.y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的 constructor(x, y)
this.color = color;
}
// 调用父类的 toString()
toString() {
return this.color + ' ' + super.toString();
}
}
let point = new Point(100, 100);
let colorPoint = new ColorPoint('1', '2', 'red');
console.log(point); // Point { x: 100, y: 100 }
console.log(colorPoint); // ColorPoint { x: '1', y: '2', color: 'red' }
point.toString(); // 100-100
colorPoint.toString(); // red 1-2
扩展
new()做了什么:
- 创建一个新的空对象。
- 将该对象的原型链链接到构造函数的原型对象上,使其继承构造函数的属性和方法。
- 将构造函数中的
this
指向新创建的对象。 - 执行构造函数内部的代码,给新对象添加属性和方法。
- 如果构造函数没有返回其他对象,则返回新创建的对象;如果构造函数返回了一个非基本类型的值(对象),则返回这个对象,否则还是返回新创建的对象。
function myNew(fn, ...args) {
// 创建一个新的空对象
let target = {};
// 将这个空对象的__proto__指向构造函数的原型
target.__proto__ = fn.prototype;
// 将this指向空对象
let res = fn.apply(target, args);
// 对构造函数返回值做判断,然后返回对应的值
return res instanceof Object ? res : target;
}
super关键字:
- 子类中存在
constructor
方法的时候,需要调用super
方法,并且需要在使用this
关键字之前调用 super
关键字可以用来调用父对象上的方法- 可以使用
super
来调用父对象上的静态方法 - 不可以使用
delete
来删除super
上的属性 - 不可以复写
super
对象上的只读属性
转载自:https://juejin.cn/post/7273014178930131000