文章目录
还不了解原型和原型链的小伙伴,请移步“深入理解javascript之原型和原型链”
简介
继承(inheritance)是面向对象软件技术当中的一个概念。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
继承的优点:
-
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
-
在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能
关于继承,我们举个形象的例子:
定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等
class Car{
constructor(color,speed){
this.color = color
this.speed = speed
// ...
}
}
由汽车这个类可以派生出“轿车”和“货车”两个类,在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱
// 货车
class Truck extends Car{
constructor(color,speed){
super(color,speed)
this.Container = true // 货箱
}
}
这样轿车和货车就是不一样的,但是二者都属于汽车这个类,汽车、轿车继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性
在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法
// 货车
class Truck extends Car{
constructor(color,speed){
super(color,speed)
this.color = "black" //覆盖
this.Container = true // 货箱
}
}
从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系
实现方式
首先,定义父类:
function Person(name) {
this.name = name;
this.showName = function () {
return this.name;
}
}
// 原型对象上添加属性
Person.prototype.age = 18;
Person.prototype.friends = ['小明', '小强'];
.
一、构造函数继承(call与apply)
核心代码
function Student(name) {
Person.call(this, name);
// Person.apply(this, [name]);
}
.
1. 构造继承(利用call改变this指向)
优点:
- 创建子类实例时,可以向父类传递参数
- 可以实现多重继承(call多个父类对象,如例2)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
function Student(name) {
Person.call(this, name);
// Person.apply(this, [name]);
}
let s1 = new Student('小红')
console.log(s1.name); // 小红
console.log(s1.showName()); // 小红
// 无法继承原型上的属性和方法
console.log(s1.age); // undefined
console.log(s1.friends); // undefined
2. 多重继承(使用多个 apply、call)
function Class1() {
this.showSub = function (a, b) {
console.log(a - b);
}
}
function Class2() {
this.showAdd = function (a, b) {
console.log(a + b);
}
}
// 使用多个 apply、call 就实现多重继承了
function Class3() {
Class1.apply(this);
Class2.apply(this);
//Class1.call(this);
//Class2.call(this);
}
var c = new Class3();
c.showSub(3, 1); // 2
c.showAdd(3, 1); // 4
3. s2 继承了 s1 上的 showName 方法
function Student(name) {
this.name = name;
}
var s1 = new Person('张三');
var s2 = new Student('李四');
console.log(s1.showName.call(s2)); // 李四
// console.log(s1.showName.apply(s2));
虽然调用的是 s1 上的 showName 方法,但是 this 指针指向的是 s2 ,所以 this.name 应该是 “李四”
二、原型链继承
核心代码
function Student(name) {
this.name = name;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
.
优点:
- Student 实例可继承父类构造函数(
Person
)的属性方法和父类原型对象(Person.prototype
)上的属性方法 - 简单,易于实现
缺点:
- Student 实例无法向父类构造函数(
Person
)传参(组合方式继承中解决) - 继承单一,无法实现多继承(组合方式继承中解决)
- 来自原型对象的所有属性被所有实例共享。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
function Student(name) {
this.name = name;
}
Student.prototype = new Person(); // prototype指向实例对象,可以继承Person的构造函数属性和原型对象上的属性
// Student.prototype = Person.prototype; // 不能继承Person的构造函数属性,只能继承Person原型对象上的属性,还会修改子会影响父
Student.prototype.constructor = Student;
let s1 = new Student('刘一');
console.log(s1.name); // 刘一
console.log(s1.showName()); // 刘一
console.log(s1.age); // 18
console.log(s1.friends); // ["小明", "小强"]
// 父子构造函数的原型对象之间有共享问题(修改子会影响父)
s1.friends.push('小红');
console.log(s1.friends); // ["小明", "小强", "小红"]
let s2 = new Student();
console.log(s2.friends); // ["小明", "小强", "小红"]
三、组合继承(混合了call方式、原型链方式)
核心代码
function Worker(name) {
Person.call(this, name); // 构造函数继承
}
Worker.prototype = new Person(); // 原型链继承
Worker.prototype.constructor = Worker;
.
组合式继承是比较常用的一种继承方法,其背后的思路是:
- 通过使用原型链实现对原型属性和方法的继承(
Child.prototype = new Parent()
) - 通过借用构造函数来实现对实例属性的继承(
Parent.call(this,hello)
)
优点:
- 可以继承父类原型上的属性,可以传参,可复用
- 每个新实例引入的构造函数属性是私有的
缺点:
- 调用了两次父类构造函数 Person(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
- 父子构造函数的原型对象之间有共享问题(问题依旧)
function Worker(name) {
Person.call(this, name); // 构造函数继承
}
Worker.prototype = new Person(); // 原型链继承
Worker.prototype.constructor = Worker;
let w1 = new Worker('张三');
console.log(w1.name); // 张三
console.log(w1.showName()); // 张三
console.log(w1.age); // 18
console.log(w1.friends); // ["小明", "小强"]
//一个实例修改了原型属性,另一个实例的原型属性也会被修改
w1.friends.push('李四')
let p1 = new Person()
console.log(p1.friends); // ["小明", "小强", "李四"]
四、原型式继承
核心代码
let p1 = Object.create(new Person('李四'));
.
优点:
- 类似于复制一个对象,用函数来包装。
缺点:
- 所有实例都会继承原型上的属性。
- 无法实现复用。(新实例属性都是后面添加的)
这里主要借助Object.create
方法实现普通对象的继承,它创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
。换句话说,它允许你指定一个新对象的原型对象。
基本语法:
Object.create(proto, [propertiesObject])
- proto: 一个对象,将成为新创建对象的原型。
- propertiesObject (可选): 一个可选的属性描述符对象,与 Object.defineProperties() 的第二个参数格式相同。
let p1 = Object.create(new Person('李四'));
console.log(p1.name); // 李四
console.log(p1.showName()); // 李四
console.log(p1.age); // 18
console.log(p1.friends); // ["小明", "小强"]
这种继承方式的缺点也很明显,因为Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。
Object.create()
创建的对象与构造函数创建的对象在功能上是等效的,但 Object.create()
提供了一种更直接的方式来设置新对象的原型,而不需要定义构造函数。
在 ES6 之前,JavaScript 并没有直接设置对象原型的方法,所以开发者通常使用构造函数和 new 关键字来创建对象并设置其原型。但 Object.create()
提供了一个更直接和简洁的方式。
五、寄生式继承
核心代码
function subObj(obj) {
let sub = Object.create(obj);
sub.name = "王五";
return sub;
}
let p1 = subObj(new Person());
.
优点:
- 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:
- 没用到原型,无法复用。
// 在原型式继承外面套了个壳子
function subObj(obj) {
let sub = Object.create(obj);
sub.name = "王五";
return sub;
}
let p1 = subObj(new Person());
console.log(p1.name); // 王五
console.log(p1.showName()); // 王五
console.log(p1.age); // 18
console.log(p1.friends); // // ["小明", "小强"]
六、寄生组合式继承( 常用)
优点:
- 堪称完美
// 寄生
function content(obj) {
function Super() {}
Super.prototype = obj; // 继承了传入的参数
return new Super();
}
let con = content(Person.prototype);
// 组合
function Student(name) {
Person.call(this, name);
}
Student.prototype = con;
con.constructor = Student; // S
let s1 = new Student('赵六');
console.log(s1.name); // 赵六
console.log(s1.showName()); // 赵六
console.log(s1.age); // 18
console.log(s1.friends); // // ["小明", "小强"]
将上面的简化下
function Student(name) {
Person.call(this, name);
}
// 创建一个没有实例方法的类
let Super = function () {};
Super.prototype = Person.prototype;
//将实例作为子类的原型
Student.prototype = new Super();
let s1 = new Student('赵六');
console.log(s1.name); // 赵六
console.log(s1.showName()); // 赵六
console.log(s1.age); // 18
console.log(s1.friends); // // ["小明", "小强"]
七、extends继承( 常用)
核心代码
class Gamer extends Person {
constructor(name, age) {
super(name)
this.age = age
}
}
.
使用ES6 中的extends关键字直接实现 JavaScript的继承:
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
// Gamer继承Person
class Gamer extends Person {
constructor(name, age) {
super(name) // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式
总结
下面以一张图作为总结:
通过Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似
对象冒充
function Temp(name) {
this.aaa = Person; // this.aaa是作为一个临时的属性,并且指向 Person 所指向的对象,
this.aaa(name); // 执行this.aaa方法,即执行 Person 所指向的对象函数
delete this.aaa; // 销毁this.aaa属性,即此时 Temp 就已经拥有了 Person 的所有属性和方法
// 以上三行相当于: Person.call(this);
}
var s1 = new Temp('张三');
console.log(s1.name); // 李四
console.log(s1.showName()); // 李四
// 无法继承原型对象上的属性方法
console.log(s1.age); // undefined
console.log(s1.friends); // undefined