Skip to content

JS 继承

原型链继承

  • 原理: 通过原型继承多个引用类型的属性和方法。
  • 构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个指针指向原型。原型对 象是另一个类型的实例时,原实例即可继承该实例的属性和方法。
js
//父对象构造函数
function Parent() {
  this.name = 'parentName'
}
//父对象原型对象
Parent.prototype.getName = function () {
  console.log(this.name)
}

//子对象构造函数
function Child() {}
//子对象原型对象指向父对象实例,完成继承
Child.prototype = new Parent()
//子对象构造函数更新为自身
Child.prototype.constructor = Child

//测试
var child1 = new Child()
child1.getName() // parentName

// 1、解析:Child.prototype = new Parent();
// Parent的实例同时包含实例属性方法和原型属性方法,所以把new Parent()赋值给Child.prototype。
// 如果仅仅Child.prototype = Parent.prototype,那么Child只能调用getName,无法调用.name
// 当Child.prototype = new Parent()后, 如果new Child()得到一个实例对象child,那么
// child.__proto__ === Child.prototype;
// Child.prototype.__proto__ === Parent.prototype
// 也就意味着在访问child对象的属性时,如果在child上找不到,就会去Child.prototype去找,如果还找不到,就会去Parent.prototype中去找,从而实现了继承。

// 2、解析:Child.prototype.constructor = Child;
// 因为constructor属性是包含在prototype里的,上面重新赋值了prototype,所以会导致Child的constructor指向[Function: Parent],有的时候使用child1.constructor判断类型的时候就会出问题
// 为了保证类型正确,我们需要将Child.prototype.constructor 指向他原本的构造函数Child

问题

  • 如果有属性是引用类型的,一旦某个实例修改了这个属性,所有实例都会受到影响。
  • 创建子对象 Child 实例的时候,不能传参

构造函数继承

实现

盗用构造函数技巧:在子类构造函数中调用父类构造函数,使用 apply 和 call 方法以新创建的对象为上下文执行构造函数。

  • 属性公用问题解决:执行子类的构造函数时,相当于运行了父类构造函数的所有初始化代码,结果是子类的每个实例都会有自己的属性
  • 传递参数:可以在子类构造函数中向父类构造函数传参。
js
function Parent() {
  this.actions = ['eat', 'run']
  this.name = 'parentName'
}

function Child() {
  Parent.call(this) //子类构造函数调用父类构造函数
}

const child1 = new Child()
const child2 = new Child()

child1.actions.pop()

console.log(child1.actions) // ['eat']
console.log(child1.actions) // ['eat', 'run']
js
function Parent() {
  this.actions = ['eat', 'run']
  this.name = 'parentName'
}

function Child(id, name, actions) {
  Parent.call(this, name) // 如果想直接传多个参数, 可以Parent.apply(this, Array.from(arguments).slice(1));
  this.id = id
}

const child1 = new Child(1, 'c1', ['eat'])
const child2 = new Child(2, 'c2', ['sing', 'jump', 'rap'])

console.log(child1.name) // { actions: [ 'eat' ], name: 'c1', id: 1 }
console.log(child2.name) // { actions: [ 'sing', 'jump', 'rap' ], name: 'c2', id: 2 }

问题

  • 属性或者方法想被继承的话,只能在构造函数中定义。
  • 方法在构造函数内定义了,那么每次创建实例都会创建一遍方法,多占一块内存。
js
function Parent(name, actions) {
  this.actions = actions
  this.name = name
  this.eat = function () {
    console.log(`${name} - eat`)
  }
}

function Child(id) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1))
  this.id = id
}

const child1 = new Child(1, 'c1', ['eat'])
const child2 = new Child(2, 'c2', ['sing', 'jump', 'rap'])

console.log(child1.eat === child2.eat) // false

组合继承

实现

综合了原型链继承和构造函数继承,基本思路:使用原型链继承原型上的属性和方法,通过构造函数继承继承实例属性。这些既可以把方 法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

js
function Parent(name, actions) {
  this.name = name
  this.actions = actions
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`)
}

//使用构造函数继承实例属性
function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1))
  this.id = id
}
//使用原型链继承继承原型上的属性和方法
Child.prototype = new Parent()
Child.prototype.constructor = Child

const child1 = new Child(1, 'c1', ['hahahahahhah'])
const child2 = new Child(2, 'c2', ['xixixixixixx'])

child1.eat() // c1 - eat
child2.eat() // c2 - eat
console.log(child1.name === child2.name) // false
console.log(child1.eat === child2.eat) // true

问题

调用了两次构造函数,做了重复的操作,一次是在创建子类原型时调用,另一次是在子类构造函数中调用

Parent.apply(this, Array.from(arguments).slice(1));

Child.prototype = new Parent();

寄生组合式继承

实现

组合继承的优化,通过 Child.prototype 间接访问到 Parent.prototype,减少一次构造函数执行。

js
function Parent(name, actions) {
  this.name = name
  this.actions = actions
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`)
}

function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1))
  this.id = id
}

// 模拟Object.create的效果
let TempFunction = function () {}
TempFunction.prototype = Parent.prototype
Child.prototype = new TempFunction()
Child.prototype.constructor = Child
//封装写法
function inheritPrototype(child, parent) {
  let prototype = Object.create(parent.prototype) //object(parent.prototype)
  prototype.constructor = child
  child.prototype = prototype
}
inheritPrototype(Child, Parent)

const child1 = new Child(1, 'c1', ['hahahahahhah'])
const child2 = new Child(2, 'c2', ['xixixixixixx'])

为什么一定要通过桥梁的方式让 Child.prototype 访问到 Parent.prototype? 直接 Child.prototype = Parent.prototype 不行吗 ?答:不行!!在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变。

js
function Parent(name, actions) {
  this.name = name
  this.actions = actions
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`)
}

function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1))
  this.id = id
}

Child.prototype = Parent.prototype

Child.prototype.constructor = Child

console.log(Parent.prototype) // Child { eat: [Function], childEat: [Function] }

Child.prototype.childEat = function () {
  console.log(`childEat - ${this.name}`)
}

const child1 = new Child(1, 'c1', ['hahahahahhah'])

console.log(Parent.prototype) // Child { eat: [Function], childEat: [Function] }

class 类继承

两种定义方式

  • 类申明:class Person{}
  • 类表达式:const Person = class{}

构成:

  • 构造函数方法:类构造函数与普通构造函数的主要区别是,调用类的构造函数必须使用 new 操作符,而普通函数如果不使用 new 调用 ,那么就会以全局 this(通常是 window)作为内部对象。
  • 实例方法
  • 获取函数、设置函数
  • 静态类方法
js
class Parent {
  name_ = null
  constructor() {
    this.name = 'aaa'
  }
  getName() {
    return this.name
  }
  static get() {
    return 'good'
  }
  //支持获取和设置访问器
  //使用get和set关键字对某个属性设置存值函数和取值函数,拦截该函数的存取行为
  set name(value) {
    this.name_ = value
  }
  get name() {
    return this.name_
  }
}
Parent.get() //good
//通过类继承
class Child extends Parent {
  constructor() {
    super()
  }
}
const p1 = new Child()
p1.getName()