JS继承

117 阅读6分钟

一、预备知识

1、构造函数的属性

function A(name) {
    this.name = name; // 实例基本属性 
    this.arr = [1]; // 实例引⽤属性 
    this.say = function () { // 实例引⽤属性
        console.log('hello')
    }
}

注意:数组和⽅法都属于实例引⽤属性,但是数组强调私有、不共享的。⽅法需要复⽤、共享。

在构造函数中,⼀般很少有数组形式的引⽤属性,⼤部分情况都是:基本属性(定义在构造函数内) + ⽅法(定义在构造函数的原型上)。

2、什么是原型对象

简单来说,每个函数都有prototype属性,它就是原型对象,通过函数实例化出来的对象有个 __proto__属性,指向原型对象。

let a = new A() 
a.__proto__ == A.prototype 

// prototype的结构如下
A.prototype = { 
 constructor: A, 
 ...其他的原型属性和⽅法 
 }

3、原型对象的作用

原型对象的⽤途是为每个实例对象存储共享的⽅法和属性,它仅仅是⼀个普通对象⽽已。并且所有的实例是共享同⼀个原型对象,因此有别于实例⽅法或属性,原型对象仅有⼀份。⽽实例有很多份,且实例属性和⽅法是独⽴的。在构造函数中:为了属性(实例基本属性)的私有性、以及⽅法(实 例引⽤属性)的复⽤、共享。因此提倡:

  • 将属性封装在构造函数中
  • 将⽅法定义在原型对象上
function A(name) {
    this.name = name; // (该属性,强调私有,不共享) 
}
A.prototype.say = function () {
    // 定义在原型对象上的⽅法 (强调复⽤,需要共享) 
    console.log('hello')
}

二、JS继承方式

继承指子类继承父类的属性和方法,目的是让子类的实例可以享有父类的属性和方法

1、原型链继承

核⼼:让父类的属性和方法在子类实例的原型链上

  • child.prototype = new Parent()

  • Child.prototype.constructor = Child (修正constructor指向 )

优点:

  • ⽅法复⽤,由于⽅法定义在⽗类的原型上,复⽤了⽗类构造函数的⽅法。⽐如say⽅法。

缺点:

  • 创建⼦类实例的时候,不能传⽗类的构造函数传参数(⽐如name)。

  • ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性。

function Parent(x) {
    this.x = x || 'parent' //基本属性
    this.arr = [1]; // 引用属性
}

Parent.prototype.getX = function(){
    console.log('hello parent')
}

function Child(y) {
    this.y = y
}

//原型继承,如果new A的时候不传参数,那么newB的时候不能传父类的参数了,对应缺点第一点
Child.prototype = new Parent()
// 修正constructor指向
Child.prototype.constructor = B 
//B原型上添加新方法getY
Child.prototype.getY = function(){
    console.log('hello child')
}

let b1 = new Child(100)
let b2 = new Child(200)

// 优点:共享了⽗类构造函数原型的getX⽅法
 console.log(b1.getX(), b2.getX()); //hello parent
 console.log(b1.getX === b2.getX) //true 
 
// 缺点1:不能向⽗类构造函数传参 
console.log(b1.x, b2.x, b1.x===b2.x); // parent,parent,true

// 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性 
b1.arr.push(2); // 修改了b1的arr属性,b2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr; 所以只要修改了b1.arr,b2.arr的属性也会变化。 
console.log(b2.arr); // [1,2]

//注意:修改b1的x属性(基本属性),是不会影响到b2.x。因为设置b1.x相当于在⼦类实例新增了x属性。

2、通过call()实现继承

优点:实例之间独⽴。

  • 创建⼦类实例,可以向⽗类构造函数传参数。

  • ⼦类实例不共享⽗类构造函数的引⽤属性。如arr属性

  • 可实现多继承(通过多个call或者apply继承多个⽗类)

缺点:

  • ⽗类的⽅法不能复⽤

由于⽅法在⽗构造函数中定义,导致⽅法不能复⽤(因为每次创建⼦类实例都要创建⼀遍⽅法)。

  • ⼦类实例,继承不了⽗类原型上的属性(因为没有⽤到原型)
//2.CALL继承
function Parent(name) {
    this.name = name || 'parent' //基本属性
    this.arr = [1]; // 引用属性
    this.say = function(){
        console.log('父类构造函数上的方法')
    }
}

Parent.prototype.getX = function(){
    console.log('hello parent')
}

function Child(x,y) {
    Parent.call(this,x) //call继承
    this.y = y
}
Child.prototype.getY = function(){
    console.log('hello child')
}

let boy1 = new Child(100,200)
let boy2 = new Child(300,400)

// 优点1:可向⽗类构造函数传参 
console.log(boy1.x, boy2.x); // 100,300
// 优点2:不共享⽗类构造函数的引⽤属性 
boy1.arr.push(2); console.log(boy1.arr,boy2.arr);// [1,2] [1]

// 缺点1:⽅法不能复⽤ 
console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的)
// 缺点2:不能继承⽗类原型上的⽅法
console.log(boy1.getX) //undefined

3、组合继承(CALL继承+原型继承)

核⼼:通过调⽤⽗类构造函数,继承⽗类的属性并保留传参的优点;然后通过将⽗类实例作为

⼦类原型,实现函数复⽤。

优点:

  • 保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。

  • 保留原型链的优点:⽗类的⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。

  • 不共享⽗类的引⽤属性。⽐如arr属性

缺点:

  • 由于调⽤了2次⽗类的构造函数,会存在⼀份多余的⽗类实例属性。

第⼀次Parent.call(this);从⽗类拷⻉⼀份⽗类实例属性,作为⼦类的实例属性

第⼆次 Child.prototype = new Parent();创建⽗类实例作为⼦类原型,Child.prototype中的⽗类属性和⽅法会被第⼀次拷⻉来的实例属性屏蔽掉,所以多余。

注意:组合继承这种⽅式,要记得修复Child.prototype.constructor指向

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享) 
    this.arr = [1]; // 实例的引用属性(该属性,强调私有) 
}
Parent.prototype.say = function () { // 将需要复⽤、共享的⽅法定义在⽗类原型上 
    console.log('hello')
}
function Child(name,like) { 
    Parent.call(this,name) // 核⼼ 第⼆次 
    this.like = like; 
}

Child.prototype = new Parent() // 核⼼ 第⼀次
Child.prototype.constructor = Child // 修正constructor指向

let boy1 = new Child('⼩红','apple') 
let boy2 = new Child('⼩明','orange')

// 优点1:可以向⽗类构造函数传参数
 console.log(boy1.name,boy1.like); // ⼩红,apple

 // 优点2:可复⽤⽗类原型上的⽅法 
 console.log(boy1.say === boy2.say) // true

 // 优点3:不共享⽗类的引⽤属性,如arr属性 
   boy1.arr.push(2) 
   console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。
   
// 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性

4、寄生组合继承(核心:Object.create)

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享) 
    this.arr = [1]; // 实例的引用属性(该属性,强调私有) 
}
Parent.prototype.say = function () { // 将需要复⽤、共享的⽅法定义在⽗类原型上
    console.log('hello')
}

function Child(name,like) { 
    Parent.call(this,name,like) // 核⼼ 
    this.like = like;
}

// 核⼼ 通过创建中间对象,⼦类原型和⽗类原型,就会隔离开,不是同⼀个啦
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

let boy1 = new Child('⼩红','apple') 
let boy2 = new Child('⼩明','orange') 
let p1 = new Parent('⼩爸爸')

5、ES6继承(extends)

//5.ES6的继承
class Parent {
    constructor(name) {
        this.name = name
    }
    say() {
        console.log('parent prototype method')
    }
}

class Child extends Parent {
    constructor(name, like) {
        super(name) //继承Parent,给Parent传参
        this.like = like
    }
    favourite() {
        console.log('child prototype method')
    }
}

let boy = new Child('piano')
console.log(boy)
console.log(boy.say())
console.log(boy.favourite())

三、补充Object.create

1、基本概念

Object.create()可以帮我们生成一个对象,通过传参,可以将生成的对象的原型指向第一个参数,

⽅法还有第⼆个可选参数,是添加到新创建对象的属性,写法如下。

const a = Object.create(Person.prototype, {
    age: {
        value: 12,
        writable: true,
        configurable: true,
    }
})

2、new 与 Object.create() 的区别?

  • new 产⽣的实例,优先获取构造函数上的属性;构造函数上没有对应的属性,才会去原型上查找; 如果构造函数中以及原型中都没有对应的属性,就会报错。

  • Object.create() 产⽣的对象,只会在原型上进⾏查找属性,原型上没有对应的属性,就会报错。