原型与继承

147 阅读6分钟

所有的实例对象(包括字面量创建的对象、new 构造函数生成的对象、Object.create 创建的非空对象)都有一个与其对应的原型对象,这个原型对象就是我们常说的原型,它是一个对象,通过这个原型对象我们就能够实现实例对象之间公有属性和方法的共享

上图所反映的就是实例对象、原型对象、构造函数之间的关联关系,它们之间通过原型链构造函数链紧密相连

// 定义一个构造函数
function Foo(){}
// 生成一个实例
let f1 = new Foo()

原型链

1、每一个实例对象都有一个与其对应的构造函数,并且通过一个隐式的 __proto__ 属性指向该构造函数的原型对象 构造函数.prototype

f1.__proto__ === Foo.prototype // true

// __proto__属性虽然在ECMAScript 6语言规范中标准化,但是不推荐被使用
// 推荐使用Object.getPrototypeOf或者Reflect.getPrototypeOf
Object.getPrototypeOf(f1) === Foo.prototype // true

2、所有的函数都是对象,实例对象对应的构造函数也是一个对象,所以也会通过 __proto__ 属性指向其对应的构造函数的原型对象;而所有函数的顶层构造函数都是Function(包括Function构造函数本身和Object构造函数),所以在没有其它继承关系的情况下,它们所对应的原型对象就是 Function.prototype ,如果存在一定的继承关系,那么它们将会一级一级的向上,最终到达Function

例如:f1.proto -> Foo.prototype, Foo.proto -> Goo.prototype, ... ,-> Function.prototype

Foo.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true

3、原型对象也是一个对象,所以也会通过 __proto__ 属性指向其对应的构造函数的原型对象,在不被主动改变的情况下,它所指向的就是 Object.prototype ,而 Object.prototype 是一个顶级原型对象,它的 __proto__ 属性值为null,它是这条链上的终结者

Foo.prototype.__proto__ === Object.prototype // true
Function.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true

构造函数链

1、每一个实例对象都有一个与其对应的构造函数,并且通过 constructor 属性指向这个构造函数

f1.constructor === Foo // true

2、实例对象对应的构造函数也是一个对象,所以它也会通过 constructor 属性指向其对应的构造函数,而Function构造函数作为顶层的构造函数对象,它的 constructor 属性指向其本身,它是这条链上的终结者

Foo.constructor === Function // true
Object.constructor === Function // true
Function.constructor === Function // true

3、原型对象也是一个对象,所以它也会通过 constructor 属性指向其对应的构造函数,在不被主动改变的情况下,指向的就是其自身

Foo.prototype.constructor === Foo // true
Function.prototype.constructor === Function // true
Object.prototype.constructor === Object // true

总结:

  • 所有的对象(包括实例对象、原型对象、构造函数对象),它的 __proto__ 属性都会指向其对应的构造函数的原型对象 构造函数.prototype ;而所有的原型对象,包括构造函数链的终点 Function.prototype,都会最终上溯到 Object.prototype,终结于 null
  • 所有的对象(包括实例对象、原型对象、构造函数对象),它的 constructor 属性都会指向其对应的构造函数;而构造函数也是对象,它们最终会一级一级上溯到 Function 这个构造函数。Function 的构造函数是它自己,也即此链的终结;
  • instanceof运算符用于检查右边构造函数的prototype属性是否出现在左边对象的原型链中的任何位置。其实它表示的是一种原型链继承的关系。
  • 我们平常所说的空对象,其实并不是严格意义上的空对象,它的原型对象指向Object.prototype,还可以继承hasOwnProperty、toString、valueOf等方法。如果想要生成一个不继承任何属性的对象,可以使用Object.create(null);如果想要生成一个平常字面量方法生成的对象,需要将其原型对象指向Object.prototype:obj = Object.create(Object.prototype) 等价于 obj = {}
  • 当我们使用new时,做了些什么?
    • 创建一个全新对象,并将其__proto__属性指向构造函数的prototype属性。
    • 将构造函数调用的this指向这个新对象,并执行构造函数。
    • 如果构造函数返回对象类型Object(包含Functoin, Array, Date, RegExg, Error等),则正常返回,否则返回这个新的对象。
// func 构造函数
// args 参数
function newOperator(func, ...args) {
  if (typeof func !== 'function') {
      console.error('第一个参数必须为函数,您传入的参数为', func)
      return
  }
  // 创建一个全新对象,并将其`__proto__`属性指向构造函数的`prototype`属性
  let newObj = Object.create(func.prototype)
  // 将构造函数调用的this指向这个新对象,并执行构造函数
  let result = func.apply(newObj, args)
  // 如果构造函数返回对象类型Object,则正常返回,否则返回这个新的对象
  return result instanceof Object ? result : newObj
}

通过原型链和构造函数链我们就可以指定方案实现继承了,继承主要是为了实现数据共享

ES5原生继承

原生继承主要分两步,一步继承父类的私有属性(call / apply),一步继承父类的原型对象上的属性(Object.create(parent.prototype))

// ES5的API
function inherit(child, parent) {
  // 子类的原型
  const childPrototype = child.prototype
  // 继承父类的原型
  const parentPrototype = Object.create(parent.prototype, {
    constructor: {
      value: child,
      enumerable: false,
      configurable: true,
      writable: true
    }
  })
  // 将父类原型和子类原型合并,并赋值给子类的原型
  child.prototype = Object.assign(parentPrototype, childPrototype)
  // 重写被污染的子类的constructor
  // parentPrototype.constructor = child
}

/*
ES6的API,写出了这个我所认为的最合理的继承方法:
  用Reflect代替了Object;
  用Reflect.getPrototypeOf来代替ob.__ptoto__;
  用Reflect.ownKeys来读取所有可枚举/不可枚举/Symbol的属性;
  用Reflect.getOwnPropertyDescriptor读取属性描述符;
  用Reflect.setPrototypeOf来设置__ptoto__。
*/

// 不同于object.assign, 该 merge方法会复制所有的源键
// 不管键名是 Symbol 或字符串,也不管是否可枚举
function fancyShadowMerge(target, source) {
  for (const key of Reflect.ownKeys(source)) {
    Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source, key))
  }
  return target
}

// Core
function inherit(child, parent) {
  const objectPrototype = Object.prototype
  // 继承父类的原型
  const parentPrototype = Object.create(parent.prototype)
  let childPrototype = child.prototype
  // 若子类没有继承任何类,直接合并子类原型和父类原型上的所有方法
  // 包含可枚举/不可枚举的方法
  if (Reflect.getPrototypeOf(childPrototype) === objectPrototype) {
    child.prototype = fancyShadowMerge(parentPrototype, childPrototype)
  } else {
    // 若子类已经继承子某个类
    // 父类的原型将在子类原型链的尽头补全
    while (Reflect.getPrototypeOf(childPrototype) !== objectPrototype) {
      childPrototype = Reflect.getPrototypeOf(childPrototype)
    }
    Reflect.setPrototypeOf(childPrototype, parent.prototype)
  }
  // 重写被污染的子类的constructor
  parentPrototype.constructor = child
}

// GithubUser
function GithubUser(username, password) {
  let _password = password
  this.username = username
}

GithubUser.prototype.login = function () {
  console.log(this.username + '要登录Github,密码是' + _password)
}

// JuejinUser
function JuejinUser(username, password) {
  GithubUser.call(this, username, password)
  this.articles = 3
}

JuejinUser.prototype.readArticle = function () {
  console.log('Read article')
}

inherit(JuejinUser, GithubUser) 

ES6类的继承

请参考 阮一峰--Class 的继承

class Person {
  constructor(name) {
    this.name = name;
  }

  printName() {
    console.log(this.name);
  }
}

class Bob extends Person {
  constructor() {
    super("Bob");
    this.hobby = "Histroy";
  }

  printHobby() {
    console.log(this.hobby);
  }
}

感谢 @Logan70 / @Linesh / @ULIVZ 提供的优质文章

深入JavaScript系列(六):原型与原型链

JavaScript 原型精髓 #一篇就够系列

深入JavaScript继承原理