js原生语法之继承及实现方式

931 阅读4分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

1 前言

前面写了一篇js 原生语法之 prototype,__proto__和 constructor把它三者之间的关系搞清楚了, 也理明白了js是怎样通过prototype实现继承的,这篇文章就讲一讲继承的具体实现方式. 其实就是怎样将想要继承的对象放置到需要继承对象的原型链上.

2 原型链继承

就是直接修改Function存放的共享数据prototype

const Animal = function () {}
Animal.prototype.eat = function () {
  console.log('i eat')
}

const Dog = function () {}
// Dog.prototype = new Animal()
// Object.setPrototypeOf(Dog.prototype, new Animal())
Dog.prototype = Object.create(new Animal())
Dog.prototype.constructor = Dog // 最好修复一下
const dog = new Dog()
dog.eat()

子类构造出来的实例, 在自身上找不到的属性 eat 会到它的原型对象的 Dog.prototype 上去找, 就是 new Animal(), Animal 实例身上也没有 eat 属性, 会到它的原型对象 Animal.prototype 上找到 eat

3 其它六种继承方式

  • 借用构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • 混入继承多个对象

都是垃圾, 都是糟粕, 其实就是我不想记了...

4 class 继承

主要要学习 class 的继承, 它也是通过 prototype 实现的继承, 是一个语法糖.

如果可以, 我还是希望代码中不要直接修改 prototype 的属性, 例如prototype.XXX = XXX, 这样的代码真的很难阅读, 并且实现继承的代码做好在最前面.

// 父类
class Animal {
  constructor(options) {
    typeof options === 'object' && Object.assign(this, options)
  }
  say() {
    console.log('i ...')
  }
}
// 子类 , 继承父类
class Dog extends Animal {
  constructor(options) {
    // 在子类中的构造函数中, 在使用 this 之前, 需要 super(), 一下, 去先将父类中的构造函数调用一下, 得到父类中的实例属性
    super(options)
    this.options = options
  }
  eat() {
    console.log('i eat')
  }
}

tsctsjs 之后, 简化代码的结果

/**
 * 继承方法
 * @param {*} child  子类
 * @param {*} b 父类
 */
var __extends = function (child, parent) {
  if (typeof b !== 'function' && parent !== null)
    throw new TypeError(
      'Class extends value ' + String(parent) + ' is not a constructor or null'
    )
  // 这里是相当于复制父类的静态方法到子类上, 因为这样会在子类 child 上读取不到的属性, 会到 父类 parent 上去读取
  Object.setPrototypeOf(child, parent)

  if (parent === null) {
    // 如果父类是 null, 则原型 设置为 null
    child.prototype = Object.create(parent)
  } else {
    // 如果父类是一个函数,
    // 直接使用 原型链继承
    child.prototype = new parent()
    child.prototype.constructor = child
  }
}
// 为啥是一个立即执行函数, 因为 class 不会提升, 可以使用这种方式做到.
// 但是 const 就模拟不出来了
var Animal = /** @class */ (function () {
  // class 实际上也就是一个 function,
  // constructor 就是 函数本体
  function Animal(options) {
    typeof options === 'object' && Object.assign(this, options)
  }
  // 方法通过 原型实现
  Animal.prototype.say = function () {
    console.log('i ...')
  }
  return Animal
})()
var Dog = /** @class */ (function (_super) {
  // 继承父类
  __extends(Dog, _super)
  function Dog(options) {
    // 实例化父类, 得到父类的实例属性
    var _this = _super.call(this, options) || this
    _this.options = options
    return _this
  }
  Dog.prototype.eat = function () {
    console.log('i eat')
  }
  return Dog
})(Animal)

5 diy 继承

纯属 diy

const protoArr = Symbol('protoArr')
const addProto = (child, parent) => {
  if (!child[protoArr]) {
    // 因为不能修改 child.prototype 的指向, 所以变通了一下, 去修改 child.prototype 的原型对象的指向
    const originProto = Object.getPrototypeOf(child.prototype)
    // 将它原先的原型对象, 和要添加的原型对象保存起来
    child[protoArr] = new Set([originProto, parent.prototype])
    const newProp = new Proxy(
      {},
      {
        get(target, key) {
          // 到保存的各个原型对象上去读取需要获取的属性
          const value = [...child[protoArr]].find((i) => i?.[key] !== void 0)?.[
            key
          ]
          return value
        },
      }
    )
    // 如果在 child.prototype 上没有读取到的属性, 回到它的原型对象上去读取, 然后就读取到了这个代理的对象, 然后就可以被拦截到
    Object.setPrototypeOf(child.prototype, newProp)
  } else {
    // 直接将需要添加的原型链加到保存的地方
    child[protoArr].add(parent.prototype)
  }
}

const delProto = (child, parent) => {
  if (!child[protoArr]) {
    // 没有新增过这个原型对象
    return
  } else {
    // 删除即可
    child[protoArr].delete(parent.prototype)
  }
}

class Style1 {
  width() {
    return 100
  }
}
class Style2 {
  height() {
    return 100
  }
}
class Style3 {
  color() {
    return '#ccc'
  }
}
class Style4 {}
Style4.prototype.fontSize = '12px'
class Style5 {
  backgroundColor() {
    return 'red'
  }
}
const getStyle = (style) => {
  const keys = ['width', 'height', 'color', 'fontSize', 'backgroundColor']
  return keys.reduce((total, cur) => {
    if (style[cur]) {
      total[cur] = typeof style[cur] === 'function' ? style[cur]() : style[cur]
    }
    return total
  }, {})
}
class Style {}

const style = new Style()
console.log(getStyle(style))
// {} 此时, 它和它的原型链上读取不到任何属性
addProto(Style, Style1)
addProto(Style, Style2)
addProto(Style, Style3)
addProto(Style, Style4)
addProto(Style, Style5)
console.log(getStyle(style))
// {
//   width: 100,
//   height: 100,
//   color: '#ccc',
//   fontSize: '12px',
//   backgroundColor: 'red',
// }
delProto(Style, Style2)
console.log(getStyle(style))
// {
//   width: 100,
//   color: '#ccc',
//   fontSize: '12px',
//   backgroundColor: 'red',
// }

6 总结

继承就是通过修改原型对象实现的.

7 最后

继承就记这么多, 如果有给你带来一些启示, 欢迎点赞, 评论, 关注, 收藏...

按照惯例, 附上之前写的几篇文章

  1. vue2 源码解析之 nextTick
  2. 代码片段之 js 限流调度器
  3. 数据结构与算法之链表(1)
  4. vue2 源码解析之事件系统$on
  5. vue2-全局 api 源码分析
  6. vue2class 写法源码分析 vue-class-component(1)
  7. js 原生语法之 prototype,__proto__和 constructor