认识js中的几种继承

202 阅读5分钟

JS中的几种继承

  • 构造函数的高级应用
  • 继承是出现在两个构造函数之间的关系
    • 构造函数A 的实例使用了 构造函数B 内的属性或方法,我们就说 构造函数B 继承自 构造函数A
    • 此时 构造函数A构造函数B 的父类。
    • 构造函数B构造函数A 的子类。
      实例:
构造函数体A
构造函数原型 -> 方法a
构造函数原型 -> 方法b
构造函数A的实例	{方法a,方法b}

构造函数体B
构造函数原型 -> 方法b
构造函数原型 -> 方法c
构造函数B的实例	{方法b,方法c}

A和B的原型都有方法b,我们再用一个构造函数C,往它的原型里写下构造函数A和B共用的方法b。

  • 常见的继承方案
    • 原型继承
    • call继承
    • 组合继承
    • ES6的继承语法

原型继承

  • 让子类构造函数的prototype原型指向父类构造函数的一个实例
    实例:
    先创建两个构造函数

做法

//创建父类构造函数
function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayHi = function(){
    console.log("Hi");
}

//创建子类构造函数
function Student(height){
    this.height = height;
}

我想让Student继承Person,可以访问到name、age以及SayHi方法。
让Student的prototype原型对象指向一个Person的实例,就能继承Person了。

[核心做法]

Student.prototype = new Person("Tom",23);
Student.prototype.constructor = Student;
var s1 = new Student(180);

console.log(s1.age);//out:23
console.log(s1.name);//out:Tom
console.log(s1.sayHi());//out:Hi
console.log(s1);//结果如下图
  • 上面第一行将Student构造函数指向了一个Person的实例。这一步相当于完全删除了prototype原来的值,然后赋予了一个新值。
    第二行:原来每一个prototype原型都有一个constructor属性,指向它的构造函数。如果没有第一行的代码那么Student.prototype.constructor是指向Student的但如果加了第一行,Student.prototype.constructor是指向Person的。也就是说,在第二行之前写以下代码将会得到true。
    继续往下看,后面会解释加上第二行代码的原因。

解析

alert(Student.prototype.constructor == Pe); //true

然后,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。因此在运行第一行代码之后,s1.constructor也指向了Person
可是,这样会使得继承链混乱,因为s1明明是用Student构造函数生成的,现在却指向了Person。所以现在需要我们去手动纠正,将Student.prototype的constructor指向Student
这一点要注意,即如果替换了prototype对象,后面一定要将prototype对象的constructor指回原来的构造函数。

  • 优点:

    • 父类的 构造函数体内 和 原型对象上的内容都能继承下来。

缺点:

    • 继承下来的属性不在自己身上,子类的实例的所有属性分成了两部分。
    • 需要在两个位置传递参数。

组合继承

结合原型继承和call继承的做法。

做法:

    • 先创建父类和子类构造函数
//父类构造函数
function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayHi = function(){
    console.log("Hi!");
}
//子类构造函数
function Student(gender,name,age){
    this.gender = gender;
}
Student.prototype.play = function(){
    console.log("Hello World!");
}
    • 结合原型继承和call继承
//原型继承
//目的:为了把Person的原型上的内容也能继承下来
Student.prototype = new Person();//注意:这里Person()里面就不要再写参数了
Student.prototype.constructor = Student;

//call继承
//目的:为了能把父类构造函数上的内容能直接继承到子类构造函数的实例上去
function Student(gender,name,age){
    this.gender = gender;
    Person.call(this,name,age);
}
//通过Student构造函数创建一个实例对象
const s1 = new Student("男","Jack",20);
    • 结果:console.log(s1);

优点:

    • 结合了两种继承方式,即能继承到父类构造函数原型上的内容,也能使得父类构造函数上的内容直接出现到实例身上。

缺点:

    • 里面还存在两个name,age属性用不上,比较占内存。

call继承(也叫借用构造函数继承)

1、核心:

在子类构造函数中通过call调用父类构造函数。(也可以利用apply方法调用)
//父类构造函数
function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayHi = function(){
    console.log("Hi!");
}
//子类构造函数
function Student(gender,name,age){
    this.gender = gender;
}
Student.prototype.play = function(){
    console.log("Hello World!");
}
//通过Student构造函数创建一个实例对象
const s1 = new Student("男");

2、做法

//在子类Student构造函数中通过call方法调用Person函数
function Student(gender,name,age){
	this.gender = gender;
	Person.call(this,name,age);
}

3、解析

    • 首先,我们明白构造函数也是一个函数,可以不和new连用,可以直接调用,那么它的this指向window。只是不和new连用的时候,就没有自动创建对象的能力了。
    • 然后,Person函数体内的内容就可以添加到window对象上了
//在调用Person的时候,使用call就可以将Person里面的this指向改为obj
//例如:
const obj = {
    message:"我是一个自定义的obj对象"
}
Person.call(obj,"Jack",20);
console.log(obj)//结果在下图
    • 再然后,Student构造函数中的this指向的是构造函数的实例对象。在上面的核心做法里面,因为使用call方法调用构造函数Person的时候,已经把Person里的this改为Student里的this了(即实例对象s1),所以name和age属性都添加到了s1身上
//通过Student构造函数创建一个实例对象
const s1 = new Student("男","Jack",18);
console.log(s1);//结果如下图
      • 这里要注意过程,上面创建实例对象s1的时候,将“Jack”和18先传给Student里面的name和age,然后在Student里传给了Person里面。然后,Person.call()里面的this又指向了s1,所以name和age就都加到s1上面了。过程类似于以下步骤:
  • 4、优点:

    • 继承来的属性将会直接出现在子类构造函数的实例上
    • 一个实例使用的属性可以在同一个位置上传递参数了。
  • 5、缺点:

    • 只能继承构造函数体内的内容,构造函数的原型对象上不能继承。就拿上面的来说,   s1 实例对象中就没有Person构造函数的原型上的 sayHi 方法。
      原因:因为一开始就只是把构造函数当做普通函数来调用,构造函数的原型上的内容是给实例用的,开始就没有new,自然就没有实例。