继承
预备知识
- 构造函数(constructor):能通过 new 运算符生成新的实例,且实例会继承构造函数的属性、方法
- 函数都有一个属性 prototype,即原型。构造函数的实例会继承原型上的方法
- 构造函数的 prototype 上的 constructor 属性,指向的是构造函数本身
- 实例上有
__proto__
属性,指向构造函数的 prototype - 原型链:在尝试读取对象的某个属性/方法时,会先在对象本身寻找,如果没找到,则通过
__proto__
属性到构造函数的原型上找,以此类推,直到 Object 的 prototype 还没找到就会报错
TIP
- 任何函数都可以用做构造函数,即任何函数都可以通过 new 来运行
- 构造函数首字母大写是一个共同的约定,以标示这个函数将被使用 new 来使用
原型链继承
通过将方法挂载在父类的原型链上,子类通过原型链共用父类的方法
function Person() {
this.name = "父类"
this.arr = [1, 2, 3]
}
Person.prototype.skill = function() {
console.log("会说话")
}
function Student() {
this.score = "A"
}
Student.prototype = new Person()
let stu1 = new Student()
let stu2 = new Student()
stu1.arr.push(4)
console.log(stu2.arr) // [1,2,3,4]
console.log(stu1.skill === stu2.skill) // true
- 优点:子类共用了父类的方法
- 缺点:
- 子类不能够传参
- 子类的实例会共享父类构造函数的引用类型,如父类的引用类型数组,在子类的实例中共享了
组合继承
为了解决原型链继承的缺点,我们可以使用构造函数,在子类中调用父类的方法,从而生成每个子类独立的属性。
为了实现方法的继承,我们将子类的原型连接到了父类的实例上。
为了子类属性的继承以及独立性,我们在子类中调用父类的方法。
因为将子类的 prototype 连接到了父类的实例上了,所以 Student.prototype.constructor 就为 Person 了,显然是不合理的,所以我们需要将 Student.prototype.constructor 更改为 Student
function Person(name, gender) {
this.name = name
this.gender = gender
this.arr = [1, 2, 3]
}
Person.prototype.skill = function() {
console.log("会说话")
}
function Student(name, gender, score) {
Person.call(this, name, gender) // 就父类的属性传递给子类,并支持传参
this.score = score
}
Student.prototype = new Person() // 将父类原型上的方法挂载到子类上去
Student.prototype.constructor = Student
- 优点: 子类能够传参,父类的引用属性也不进行共享
- 缺点:调用了两次父类的构造方法,12、16 行各调用了一次
寄生组合继承
为了解决组合继承调用了两次父类的缺点,我们就要从两次调用下手,第 12 行看起来不好下手,那我们将 16 行改为
Student.prototype = Person.prototype
这样一来,似乎是省去了一次父类调用,但是由于 Person.prototype 是一个引用数据类型,我们在手动更改子类的 Student.prototype.constructor = Student 的时候,父类的 constructor 也会被更改,此时我们打印 Person.prototype.constructor 的话,就会发现变为了 Student
那咋办,直接将这两个连接在一起不好使的话,那我们找个媒介把他们从中间隔一下
function Person(name, gender) {
this.name = name
this.gender = gender
this.arr = [1, 2, 3]
}
Person.prototype.skill = function() {
console.log("会说话")
}
function Student(name, gender, score) {
// 继承父类属性
Person.call(this, name, gender)
this.score = score
}
// 继承父类原型的方法
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
TIP
Object.create()方法会创建一个新对象,使用传入的对象来提供新创建的对象的 proto
在这个例子中,Object.create(Person.prototype) 会创建一个对象,并且这个对象的 proto 指向 Person.prototype
Student.prototype 指向的是这个新建的对象,那相当于用这个对象将 Student 的原型和 Person 的原型联系在一起,但又不至于指向同一个引用,在更改 Student.prototype.constructor 的时候不至于影响到 Person。用代码来说就是
let obj = Object.create(Person.prototype)
Student.prototype = obj
- 优点:完美,ES6 之前的最佳实践
- 缺点:没有
class 继承
class 语法
我们将父类的构造函数改成 class 的写法,constructor 方法即为构造函数方法,同时 Person 类上的其他方法都是挂载在 Person 的原型链上
class Person {
constructor(name, gender) {
this.name = name
this.gender = gender
}
skill() {
console.log("会说话")
}
}
let personA = new Person("tom", "male")
class 继承写法
在子类的构造函数中,需要使用 super 方法来调用父类的构造函数,下面代码的第三行相当于
Person.call(this, name, gender)
子类必须在 constructor 函数中调用 super 方法,否则新建实例时会出错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。
同时在父类的基础上,还可以拓展子类独有的属性和方法
class Student extends Person {
constructor(name, gender, score) {
super(name, gender)
this.score = score
}
study() {
console.log("要学习的")
}
}
new 做了什么
function Person(name, gender) {
this.name = name
this.gender = gender
}
var personA = new Person("tom", "male")
等同于下面代码
var personA = {}
personA.__proto__ = Person.prototype // 共享原型链方法
Person.call(personA) // 共享属性
就分为三步:
- 生成新的对象
- 该对象继承父类的原型
- 将父类的 this 绑定为新对象
instanceof
instanceof 内部也采用原型链的方式去判断对象的原型链上能不能找到另一个对象的原型, 下面是 instanceof 的简单手动实现。
function myInstanceof(left, right){
const leftProto = left?.__proto__;
if(!leftProto){
return false;
}
if(leftProto !== right.prototype){
return myInstanceof(leftProto, right);
}
return true;
}
myInstanceof(12, Number) // true