📝个人主页:爱吃炫迈
💌系列专栏:JavaScript
🧑💻座右铭:道阻且长,行则将至💗
文章目录
- 继承
- JavaScript如何实现继承?
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
- ES6类继承-extends
- 总结
继承
🍇继承是什么?
继承(inheritance)是面向对象软件技术当中的一个概念。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类”也可以称“A是B的超类”
🍇关于继承,我们举个例子理解一下
- 几何形状只有两种,即椭圆形和多边形。
- 圆是椭圆形的一种,它只有一个焦点。
- 三角形、矩形和五边形都是多边形的一种,具有不同数量的边。
- 这就构成了一种完美的继承关系。
在这个例子中:
- 形状是椭圆形和多边形的
基类
(所有类都由它继承而来)。 - 圆形继承了椭圆形,因此圆形是椭圆形的
子类
,椭圆形是圆形的超类
。 - 三角形、矩形和五边形都继承了多边形,因此它们是多边形的
子类
,多边形是它们的超类
。 - 正方形继承了矩形,因此正方形是矩形的
子类
,矩形是正方形的超类
。
🍇继承的优点
-
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
-
在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能
-
虽然
JavaScript
并不是真正的面向对象语言,但它天生的灵活性,使应用场景更加丰富
JavaScript如何实现继承?
- 原型链继承
- 构造函数继承(借助 call)
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链继承
🥝学习原型链继承的前提是我们要知道JavaScript原型链的机制
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。
🥝举个例子实现原型链继承
// 1.定义父类构造函数
function Person() {
this.name = "jack";
}
// 2.父类原型上添加内容
Person.prototype.running = function () {
console.log(this.name + "running");
};
// 3.定义子类构造函数
function Student() {
this.sno = 111;
}
// 4.创建父类对象,并且作为子类的原型对象
let p = new Person();
Student.prototype = p;
// 5.在子类原型上添加内容
Student.prototype.studying = function () {
console.log(this.name + "studying");
};
// 6.创建子类对象
let stu = new Student();
// 测试
console.log(stu.name); //jack
stu.running(); //jack running
🥝继承创建对象的内存图
- 第4步
let p = new Person();
、Student.prototype = p;
使Student类的原型指向p对象。 - 第5步创建子类对象stu对象
let stu = new Student();
使stu的隐式原型(_proto_
)指向Student的显式原型对象,即p对象。 - 测试
stu.name
、stu.running
中的name
属性和running
属性在stu对象中找不到,就沿着原型链寻找,最终在p对象中找到。
🥝原型链继承的弊端
-
继承的属性其实是保存在p对象上的
-
我们通过直接打印对象是看不到这个属性的
console.log(stu)
- 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
function Person() {
this.name = "jack";
this.friends = []
}
let stu = new Student();
let stu1 = new Student();
// *获取引用,修改引用中的值,会相互影响
stu1.friends.push("kobe")
// *补充:直接修改对象上的属性,是给对象添加了一个新属性
stu.name = "kobe";
console.log(stu1.name); //jack
console.log(stu.friends); //kobe
console.log(stu1.friends); //kobe
- 不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化)
构造函数继承
- 在子类型构造函数的内部调用父类型构造函数。
- 因为函数可以在任意的时刻被调用,
- 因此通过
apply()
和call()
方法也可以在新创建的对象上执行构造函数;
🍌举例实现构造函数继承
function Person(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends
}
function Student(name, age, friends, sno) {
// 继承自Person
Person.call(this, name, age, friends);
this.sno = sno;
}
let stu1 = new Student("why", 18, ["lilei"], 111);
stu1.friends.push("lucy");
console.log(stu1.friends); //['lilei', 'lucy']
let stu2 = new Student("kobe", 18, ["james"], 112);
console.log(stu2.friends); //['james']
核心代码是Person.call(this)
,创建子类实例时调用Person
构造函数,于是Student
的每个实例都会将Person中的属性复制一份。
构造函数优点:解决了原型链继承的缺点
🍌构造函数继承缺点
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
组合继承
组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。
🍉举例实现组合继承
function Person(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends
}
function Student(name, age, friends, sno) {
// 继承属性
// 第二次调用Person()
Person.call(this, name, age, friends);
this.sno = sno;
}
// 继承方法
// 构建原型链
// 第一次调用Person()
let p = new Person();
Student.prototype = p;
let stu1 = new Student("why", 18, ["lilei"], 111);
let stu2 = new Student("kobe", 18, ["james"], 112);
console.log(stu1); //Student {name: 'why', age: 18, friends: Array(1), sno: 111}
stu1.friends.push("lucy");
console.log(stu1); //Student {name: 'why', age: 18, friends: Array(2), sno: 111}
console.log(stu1.friends); //['lilei', 'lucy']
console.log(stu2.friends); //['james']
🍉组合继承内存图
🍉组合继承缺点
- 第一次调用
Person.prototype
写入三个属性name,friends,age。 - 第二次调用
Person()
:给stu1
写入两个属性name,friends,age。
🍉实例对象stu1
上的两个属性就屏蔽了其原型对象Person.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
原型式继承
🥑利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
var person = {
name: "jack",
friends: ["kobe", "lucy"],
};
🥑object()对传入的对象执行了一次浅拷贝,将构造函数F的原型直接指向传入的对象
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
// 等价于
// var newObj = new F();
// return newObj;
// 即newObj._proto_=F.prototype=obj
}
var student1 = object(person);
console.log(student1.name); //jack
student1.friends.push("Bob");
🥑*Object.create()
方法实现原型式继承,Object.create()
可替代上面的object方法*
var student2 = Object.create(person);
student2.name = "Linda";
console.log(student2.name); //Linda
student2.friends.push("Greg");
🥑测试
console.log(person.friends); //['kobe', 'lucy', 'Bob', 'Greg']
🥑原型式继承缺点
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
寄生式继承
思路:寄生式继承的思路是结合原型类继承和工厂模式的一种方式
核心:创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回
🌽举例实现寄生式继承
var person = {
name: "jack",
friends: ["kobe", "lucy"],
};
function createStudent(person) {
var newObj = Object.create(person);
newObj.studying = function () {
console.log(this.name + " studying");
};
return newObj;
}
var student1 = createStudent(person);
student1.friends.push("Bob");
console.log(person.friends); //['kobe', 'lucy', 'Bob']
console.log(student1.friends); //['kobe', 'lucy', 'Bob']
console.log(student1.name); //jack
student1.studying(); //jack studying
🌽寄生式继承缺点(和原型式继承一样)
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
寄生组合式继承
借助解决普通对象的继承问题的
Object.create
方法,在全面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式。
🥕举例实现寄生组合式继承
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function person(name) {
this.name = name;
this.colors = ["red"];
}
person.prototype.sayName = function () {
console.log(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function student(name, age) {
person.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(student, person);
// 新增子类原型属性
student.prototype.sayAge = function () {
console.log(this.age);
};
var student1 = new student("jack", 23);
var student2 = new student("kobe", 25);
student1.colors.push("blue");
student1.colors.push("green");
console.log(student1.colors); //['red', 'blue', 'green']
console.log(student2.colors); //['red']
student1.sayName(); //jack
student2.sayAge(); //25
🎉🎉综上,我们花了很大的篇幅讨论了在ES5中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的。
在ES6中新增了使用extends关键字,可以方便的帮助我们实现继承🎉🎉🎉
ES6类继承-extends
extends
关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。- 其中
constructor
表示构造函数,一个类只能有一个构造函数,有多个会报出SyntaxError
错误。 - 如果没有显式指定构造方法,则会添加默认的
constructor
方法。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(this.name + " running");
}
eating() {
console.log("eating1");
console.log("eating2");
}
static personMethod() {
console.log("personMethod");
}
}
class Student extends Person {
constructor(name, age, sno) {
// 1.super用法1
super(name, age);
this.sno = sno;
}
// 子类对父类方法的重写
running() {
console.log(stu + " running");
}
eating() {
// 2.super用法2:复用父类中的处理逻辑
super.eating();
console.log("eating3");
console.log("eating4");
}
static studentMethod() {
// 3.super用法3
super.personMethod();
console.log("studentMethod");
}
}
var stu = new Student("kobe", 18, 111);
stu.running();
// 1.若没有重写,则输出:kobe running
// 2.若重写了,则优先找自己的方法,则输出:stu running
stu.eating(); //eating1 eating2 eating3 eating4
Student.studentMethod(); //personMethod studentMethod
console.log(stu); //{name: 'kobe', age: 18, sno: 111}
🥕我们会发现在上面的代码中我使用了一个super关键字,这个super关键字有不同的使用方式:
- 注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数!
- super的使用位置有三个:子类的构造函数、实例方法、静态方法;
总结
以一张图总结:
通过Object.create
来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends
的语法糖和寄生组合继承的方式基本类似。