2

js实现继承的五种方法及原型的继承关系

 2 years ago
source link: https://segmentfault.com/a/1190000040957850
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

继承是javascript中实现代码复用的一种方式,也能绑定对象或者函数之间的关系

为什么要继承

比如以下代码,Person、Student和Teacher构造函数,可以发现他们有一些特征

  • Person和Student都有姓名、年龄的属性和吃的方法,但Student还有学号、分数的属性和学习的方法
  • Person和Teacher都有姓名、年龄的属性和吃的方法,但Teacher还有教学的方法
function Person(name, age) {
  this.name = name
  this.age = age
  this.eating = function () {
    console.log('eating')
  }
}

function Student(name, age, sno, score) {
  this.name = name
  this.age = age
  this.sno = sno
  this.score = score
  this.eating = function () {
    console.log('eating')
  }
  this.studing = function () {
    console.log('studing')
  }
}

function Teacher(name, age) {
  this.name = name
  this.age = age
  this.eating = function () {
    console.log('eating')
  }
  this.teaching = function () {
    console.log('teaching')
  }
}

可以发现在定义函数的时候有很多的重复代码,而且Person和Student、Teacher是包含关系,继承就是通过这种包含关系为突破口来减少的重复代码

父类原型赋值给子类

这种方式是直接把子类和父类原型指向同一个对象

Student.prototype = Person.prototype

这样会将所有添加到子元素原型上的属性和方法都添加到父元素身上,再增加一个子元素,也会拥有其它子元素的所有方法,这样实现继承的方式并不推荐

原型式继承

通过函数的prototype属性就可以实现继承关系,原型式继承就是将Student.prototype赋值为Person构造函数实例对象

function Person() {
  this.name = 'alice'
}
Person.prototype.eating = function () {
  console.log('eating')
}

function Student(score) {
  this.score = score
}
var person = new Person()
Student.prototype = person
var student = new Student(88)

console.log(student)
console.log(student.name)
console.log(student.__proto__)
student.eating()

以上代码的执行结果为

这样Student的实例对象可以访问Person的属性和方法,但是存在一些问题

  • 通过打印,无法获取Student继承的Person属性和方法
  • 不能自定义Person中属性的值

借用构造函数继承

基于以上问题,在原型式继承的基础上来借用构造函数来解决,子函数内通过call/apply来调用父函数

function Person(name) {
  this.name = name
}
Person.prototype.eating = function () {
  console.log('eating')
}

function Student(name, score) {
  Person.call(this, name)
  this.score = score
}
var person = new Person()
Student.prototype = person
var student = new Student('kiki', 88)

console.log(student)
console.log(student.name)
console.log(student.__proto__)
student.eating()

执行结果如下

通过这样的方式,可以解决原型式继承存在的两个问题,一是可以从Student的实例对象上找到父元素Person的属性和方法,二是可以自定义Person属性的值。

但这样一种定义方式也存在问题

  • Person构造函数至少被执行了两次,一次是创建Person的实例对象,一次是Student构造函数中通过call调用
  • 创建的person对象也保存了一份Person的数据,但这份数据是不必要的

寄生式继承

寄生式继承结合了原型式继承和工厂函数,创建一个实现继承的函数,在函数内部操作对象并返回

var user = {
  flying: function(){
    console.log('flying')
  }
}

function createObj(name, age){
  var obj = Object.create(user)
  obj.name = name
  obj.age = age
  obj.eating = function(){
    console.log('eating')
  }
  return obj
}

var kiki = createObj('kiki', 18)
console.log(kiki)
console.log(kiki.__proto__)

执行结果如下

通过寄生式继承,解决了借用构造函数重复调用父类函数和保存不必要数据的问题,但它又产生了新的问题

  • 无法知道对象的类型,比如Person或者Student
  • 每个对象都保存了一份eating的方法,其实是没有必要的

寄生组合式继承

通过一个对象来链接父子函数之间的继承关系

首先定义一个原型继承函数,提供三种定义的方式,可以根据项目兼容性选择

var obj = {
  name: 'alice'
}

// 方式一:setPrototypeOf
function createObject1(o) {
  var obj = {}
  Object.setPrototypeOf(obj, o)
  return obj
}

// 方式二:构造函数
function createObject2(o) {
  function Fn() { }
  Fn.prototype = o
  var obj = new Fn()
  return obj
}

var obj1 = createObject1(obj)
var obj2 = createObject2(obj)

// 方式三:Object.create
var obj3 = Object.create(obj)

console.log(Object.getOwnPropertyDescriptors(obj1.__proto__))
console.log(Object.getOwnPropertyDescriptors(obj2.__proto__))
console.log(Object.getOwnPropertyDescriptors(obj3.__proto__))

执行结果为

然后再将原型式继承函数添加到构造函数的继承关系中

function createObj(o) {
  function Fn() { }
  Fn.prototype = o
  return new Fn()
}
// 封装继承的函数
function inheritPrototype(subType, superType){
  subType.prototype = createObj(superType.prototype)
  Object.defineProperty(subType.prototype, 'constructor', {
    value: subType,
    configurable: true
  })
}

function Person(name, age) {
  this.name = name
  this.age = age
  this.eating = function () {
    console.log('eating')
  }
}
function Student(name, age, score) {
  Person.call(this, name, age)
  this.score = score
}

inheritPrototype(Student, Person)
Student.prototype.running = function () {
  console.log('running')
}
var student = new Student('alice', 18, 100)
console.log(student)
student.running()

执行结果如下

寄生组合式继承就能比较好的解决以上继承方式存在的问题

原型的继承关系

实例对象、构造函数、原型对象之间存在继承的关系

var obj = {}
function Foo() { }
var foo = new Foo()

console.log(obj.__proto__ === Object.prototype)
console.log(foo.__proto__ === Foo.prototype)

console.log(Foo.__proto__ === Function.prototype)
console.log(Function.__proto__ === Function.prototype)
console.log(Object.__proto__ === Function.prototype)

console.log(Foo.prototype.__proto__ === Object.prototype)
console.log(Function.prototype.__proto__ === Object.prototype)
console.log(Object.prototype.__proto__)

执行结果如下

继承关系如下

    • 定义obj字面量相当于创建Function Object的实例,所以obj的隐式原型等于Object的显式原型
    • foo对象是构造函数Foo的实例,所以foo的隐式等于Foo的显式原型
    • 函数Foo也是对象,是构造函数Function的实例,所以函数Foo的隐式原型等于Function的显式原型
    • 函数Function也是对象,是构造函数Function的实例,所以函数Function的隐式原型等于Function的显式原型,即Function的隐式原型与显式原型相等
    • 函数Object也是对象,是构造函数Function的实例,所以函数Object的隐式原型等于Function的显式原型
    • Foo的显式原型也是对象,是构造函数Object的实例,所以Foo的显式原型对象的隐式原型等于Object的显式原型
    • Function的显式原型也是对象,是构造函数Object的实例,所以Function的显式原型对象的隐式原型等于Object的显式原型
    • Object的显式原型也是对象,它的隐式原型指向null

上述原型关系更为详细的图解如下

以上就是js实现继承的五种方法及原型的继承关系,关于js高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK