阅读 937

从原型链出发,看看 Class 被编译成了什么样子

本文从 JavaScript 继承中,最核心的原型链出发,介绍了在 ES5 和 ES6 中一些实现继承的方式,及其背后的原理。

原型链

基于原型的语言

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。
在 JavaScript 中是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

理解对象的原型(可以通过Object.getPrototypeOf(obj)或者已被弃用的__proto__属性获得)与构造函数的prototype属性之间的区别是很重要的。**前者是每个实例上都有的属性,后者是构造函数的属性。**也就是说,Object.getPrototypeOf(new Foobar()) === Foobar.prototype

和基于类的语言的区别

在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。但是在 JavaScript 是如上文说的通过引用的方式在原型链上寻找对应的方法或属性,所以这种可能更应该被称为委托

核心图解(基础)


我们先祭出简单版本的图,并以几个关键问题阐述

  • 函数的 prototype 属性指向了一个对象,这个对象是调用该 构造函数 创建的实例的 原型(所以叫实例原型)
  • 实例对象有一个 __proto__ 属性,指向他的原型
  • 每一个实例原型都有一个 constructor 属性指向关联的 构造函数

为什么有的对象有 prototype 有的没有

JavaScript 分为函数对象普通对象,每个对象都有 __proto__ 属性,但是只有函数对象才有 prototype 属性

JavaScript 是如何访问原型链上的属性或方法的

在调用继承自 ObjectPersonperson 上的 valueOf 方法时(person.valueOf()),会发生以下过程:

  • 浏览器首先检查,person 对象是否具有可用的 valueOf() 方法。
  • 如果没有,则浏览器检查 person 对象的原型对象(即 Person构造函数的 prototype 属性所指向的对象)是否具有可用的 valueof() 方法。
  • 如果也没有,则浏览器检查 Person() 构造函数的prototype属性所指向的对象的原型对象(即 Object构造函数的prototype属性所指向的对象)是否具有可用的 valueOf() 方法。这里有这个方法,于是该方法被调用。

为什么 person.constructor === Person

首先,我们需要知道 实例原型(即 Person.prototype )的 constructor 指向构造函数本身
从图里我们会发现, person 上并没有 constructor 属性,那为什么问题里的判等成立呢?——因为访问 person.``constructor 时,发现属性不存在, 则会从 person 的原型也就是 Person.prototype 中读取,这个时候能访问到,因此:
person.constructor === Person.prototype.constructor

核心图解(进阶)



接下来我们换一张复杂一些的,加上了 Function ,变得复杂了起来。

原型链的顶端是什么

  • Object.prototype 是原型链的 root ,它指向了 nullObject.prototype === null

Object 和 Function

  • 对于 Function 而言
    • Function.prototype.__proto__ === Object.prototype 这里 Function 和 Object 的联系建立了起来 —— 函数是一个对象
    • Object.__proto__-> Function.prototype Object 是一个函数(构造函数)
    • Function.__proto__ === Function.prototype 这里是 Function 和其他人都不一样的地方。而查找的时候,则会从 Function.prototype 查到 Object.prototype 走完原型链
  • 对于 Object
    • 这里解释了为什么 Object.prototype.valueOf === Object.valueOf ,为什么 Object 上可以直接访问到 Object.prototype 上的属性,而当 Person 却不能直接访问 Person.prototype 上的属性 —— Object.__proto__-> Function.prototype

Function.prototype.__proto__->Object.prototype

鸡蛋问题

  • 到底是先有鸡还是先有蛋?现有 Object 还是先有 Function
    • Object instanceof Function === true
    • Function instanceof Object === true
  • 核心:Object.prototype.__proto__ === null,人为规定了最开始的 rootPrototype,所以可以认为是先有 Object

ES5 中的继承

上文简单阐述了 JavaScript 中原型链的机制,下面我们来说说基于这种机制,如何实现继承

原型链继承

// 父
function Parent () {
  this.name = 'foo';
}
// 在原型链上添加方法
Parent.prototype.getName = function () {
  console.log(this.name);
  return this.name
}
// 子
function Child () {

}
Child.prototype = new Parent();

const child1 = new Child();
console.log(child1.getName()) // foo
复制代码

最简单的继承

  • 缺点:
    • 引用类型的属性被所有实例共享
      • 即,修改原型链上的引用类型属性时,会影响到所有 Child
    • 在创建 Child 的实例时,不能向 Parent 传参

借用构造函数继承

// 父
function Parent (names) {
  this.names = names;
}
// 子
function Child (names) {
  Parent.call(this, names);
}


const child1 = new Child([]);
child1.names.push('test');
console.log(child1.names); // ["test"]

const child2 = new Child([]);
console.log(child2.names); // []
复制代码
  • 优点
    • 避免了引用类型的属性被所有实例共享
    • 可以在 Child 中向 Parent 传参
  • 缺点:
    • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。

组合继承【常用】

// 父
function Parent (name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
// 子
function Child (name, age) {
  // 执行 Parent 的构造函数
  Parent.call(this, name);
  this.age = age;
}
// 修改 prototype 到父元素实例。以便让子元素实例通过 __proto__ 使用父元素属性
// 但是多执行了一次 Parent 的构造函数
Child.prototype = new Parent();
// 修正 constructor 以保证 child.constructor === Child
Child.prototype.constructor = Child;

const child1 = new Child('foo', '18');
child1.colors.push('black');
console.log(child1.name); // foo
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

const child2 = new Child('bar', '20');
console.log(child2.name); // bar
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
复制代码
  • 缺点
    • 会调用两次父构造函数,导致 prototype 和 实例上有重复的属性

原型式继承

function createObj(o) {
  function F(){}
  F.prototype = o;
  return new F();
}
复制代码

就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

  • 缺点
    • 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function createObj (o) {
  const clone = Object.create(o);
  clone.sayName = function () {
    console.log('hi');
  }
  return clone;
}
复制代码
  • 缺点
    • 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

寄生组合式继承【常用】

核心:不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype ,少一次调用 Child 的构造函数
之前是通过构造函数,来创建 __proto__ 的指向,而现在我们改进后,直接修改 Child 的 prototype 指向 Parent 的 prototype

// 代码封装
function object(o) {
  // 使用一个“干净”的函数,没有执行 Parent 的构造函数
  function F() {}
  F.prototype = o;
  return new F();
}
function prototype(child, parent) {
  const prototype = object(parent.prototype);
  // 修正 consturctor
  prototype.constructor = child;
  // 本质上约等于 child.prototype.__proto__ = parent.prototype
  child.prototype = prototype;
}

// 业务代码
function Parent (name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 当我们使用的时候:
prototype(Child, Parent);
const child1 = new Child('foo', '18');
console.log(child1);
复制代码
  • 优点
    • 只调用了一次 Parent 构造函数
    • prototype 上没有有重复的属性
    • 原型链还能保持不变,能够正常使用 instanceof 和 isPrototypeOf

image.png
上述代码关系如图

继承静态方法

我们前面讲的所有的继承方法,都没有实现构造函数上的静态方法继承,然而在ES6的 class 中,子类是可以继承父类的静态方法的。

我们可以通过 Object.setPrototypeOf() 方法实现静态方法的继承。

function prototype(child, parent) {
  const prototype = Object.create(parent.prototype);
  // 修正 consturctor
  prototype.constructor = child;
  // 即 child.prototype.__proto__ = parent.prototype
  child.prototype = prototype;
}

// 业务代码
function Parent (name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
// 静态方法
Parent.staticFn = function () {
  return "Parent";
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name, age) {
  Parent.call(this, name);
  this.age = age;
}
// 继承静态方法
Object.setPrototypeOf(Child, Parent);

// 当我们使用的时候:
prototype(Child, Parent);
const child1 = new Child('foo', '18');
console.log(child1);
Child.staticFn()
复制代码

扩展 - Object.create()

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__。 也就是说,新创建一个对象,这个对象的 __proto__ 指向了一个现有对象

参数

Object.create(proto,[propertiesObject])

  • proto 新创建对象的原型对象。
  • propertiesObject 【可选】需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数(即 数据描述符 - configurableenumerablevaluewritable 和 访问器描述符 - getset)。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

使用场景

  • 在需要实例原型,但不需要执行构造函数的时候(比如寄生组合继承)
    • o = new Constructor()o = Object.create(Constructor.prototype) 的区别就在于,后者没有执行构造函数
  • 创建一个纯净的对象
    • Object.create(null)

Polyfill

Object.create = (proto, propertiesObject) => {
  if (typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object: ' + proto);
  }
  if (propertiesObject === null) throw 'TypeError'
  
  function F () {}
  F.prototype = proto
  const o = new F()
  
  // 添加属性
  if (typeof propertiesObject !== 'undefined') {
    Object.defineProperties(o, propertiesObject)
  }
  // 如果 proto 为 null,需要去除 __proto__
  if (proto === null) o.__proto__ = null
  // 返回新的对象
  return o
}
复制代码

扩展 - Object.setPrototypeOf()

设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性,也就是 __proto__)到另一个对象或  null

Object.create 的微小区别

  • Object.create 是新创建一个对象,这个对象的 __proto__ 指向了一个现有对象
  • Object.setPrototypeOf() 是 设置一个指定的对象的原型

也就是说,Object.create 会多一层对象

参数

Object.setPrototypeOf(obj, prototype)

  • obj 要设置其原型的对象。
  • prototype 该对象的新原型(一个对象 或 null).

使用场景

  1. 继承静态方法
    1. 如上文

Polyfill

Object.prototype.setPrototypeOf = function(obj, proto) {
  if(obj.__proto__) {
    obj.__proto__ = proto;
    return obj;
  } else {
    // 如果你想返回 prototype of Object.create(null):
    const Fn = function() {
      for (const key in obj) {
        Object.defineProperty(this, key, {
          value: obj[key],
        });
      }
    };
    Fn.prototype = proto;
    return new Fn();
  }
}
复制代码

ES6 中的继承 - Class 语法糖

ES6 中新增了 Class,使得实现继承得到了简化。我们先来看简单看基础

类表达式 和 类声明

实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式类声明

  • 类声明:定义类的一种方法是使用类声明。要声明一个类,你可以使用带有class关键字的类名
  • 类表达式:类表达式可以命名或不命名。命名类表达式的名称是该类体的局部名称。
// 类声明
class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}
// 类表达式 - 匿名类
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
// 类表达式 - 命名类
let Rectangle = class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
复制代码

类体和方法定义

类元素拥有以下几种属性

  • 构造函数:constructor 方法是一个特殊的方法,这种方法用于创建和初始化一个由 class 创建的对象
    • 注意:类的内部所有定义的方法,都是不可枚举的,也就是说
// class
class Person {
  constructor(name) {
    this.name = name;
  }
}
// ES5
function Person(name) {
  this.name = name;
}
复制代码
  • 原型方法
    • 注意:类的内部所有定义的方法,都是不可枚举的
// class
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return 'hello, I am ' + this.name;
  }
}
Object.keys(Person.prototype); // []
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]

// ES5
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function () {
  return 'hello, I am ' + this.name;
};
Object.keys(Person.prototype); // ['sayHello']
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]
复制代码
  • 静态方法:static 关键字用来定义一个类的一个静态方法。调用静态方法不需要实例化该类,但不能通过一个类实例调用静态方法。类比 ES5 中定义在构造函数对象上的方法。
// class
class Person {
  static sayHello() {
    return 'hello';
  }
}
Person.sayHello() // 'hello'
const foo = new Person();
foo.sayHello(); // TypeError: foo.sayHello is not a function

// ES5
function Person() {}
// 不在原型链上
Person.sayHello = function() {
    return 'hello';
};
Person.sayHello(); // 'hello'
var foo = new Person();
kevin.sayHello(); // TypeError: foo.sayHello is not a function
复制代码
  • 实例属性
    • 实例的属性必须定义在类的方法里。(但是有一个 Stag 3的提案 可以直接写在类里面)
    • 静态的或原型的数据属性必须定义在类定义的外面。
// class
class Person {
  constructor() {
    this.state = {
      count: 0
    };
  }
}
// 新的 Field declarations
class Person {
  state = {
    count: 0
  };
}
// ES5
function Person() {
  this.state = {
    count: 0
  };
}
复制代码
  • 静态属性
    • 静态公有字段在你想要创建一个只在每个类里面只存在一份,而不会存在于你创建的每个类的实例中的属性时可以用到。静态公有字段不会在子类里重复初始化
// class
class Person {
  static name = 'foo';
}

// ES5
function Person() {};
Person.name = 'foo';
复制代码
  • gettersetter
    • 与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
// class
class Person {
  get name() {
    return 'bar';
  }
  set name(newName) {
    console.log('new name 为:' + newName)
  }
}
let person = new Person();
person.name = 'foo';
// new name 为:foo
console.log(person.name);
// bar

// ES5
function Person(name) {}
Person.prototype = {
  get name() {
    return 'bar';
  },
  set name(newName) {
    console.log('new name 为:' + newName)
  }
}
let person = new Person();
person.name = 'foo';
// new name 为:foo
console.log(person.name);
// bar
复制代码
  • 私有属性/方法
    • 在属性/方法前加上 # 即可让其成为私有的,#是名称的一部分,也用于访问和声明。
    • 私有字段仅能在字段声明中预先定义,不能通过在之后赋值来创建它们
class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
	static #privateStaticMethod() {
    return 42;
  }
}
// 这个在自己实现起来很恶心,可以使用 闭包/Symbol/WeakMap 实现,暂且不详细叙述
复制代码

Babel是如何编译你的 Class 的

下文编译均来自 babel try it out

只有构造函数

// Input
class Person {
  constructor(name) {
    this.name = name;
  }
}
// Output
"use strict";
// 在环境支持与不支持Symbol的情况做了区分
// 在 Symbol 存在的环境下,left instanceof right 实际上是 right[Symbol.hasInstance](left),同时可以在类的内部去覆写这个方法
function _instanceof(left, right) {
  if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}
// _classCallCheck 的作用是检查 Person 是否是通过 new 的方式调用,类必须使用 new 调用,否则会报错。
// 当我们使用 var person = Person() 的形式调用的时候,this 指向 window,所以 instance instanceof Constructor 就会为 false,与 ES6 的要求一致。
function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
var Person = function Person(name) {
  _classCallCheck(this, Person);
  this.name = name;
};
复制代码

实例属性,静态属性,私有属性

// Input
class Person {
  // 实例属性
  foo = 'foo';
	// 静态属性
	static bar = 'bar';
	// 私有属性
	#test = 'test';
	constructor(name) {
    this.name = name;
  }
}
// Output
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// defineProperty 修改/设置属性
function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
// 通过 weakMap 实现私有变量,详见下文扩展
var _test = new WeakMap();
var Person = function Person(name) {
  _classCallCheck(this, Person);
  _defineProperty(this, "foo", 'foo');
  _test.set(this, {
    writable: true,
    value: 'test'
  });
  this.name = name;
};
_defineProperty(Person, "bar", 'bar');
复制代码

我们可以清楚的发现:

  • 实例属性通过 defineProperty 在构造函数中设置到了实例上
  • 静态属性 defineProperty 在构造函数外设置到了构造函数上
  • 私有属性 和 实例属性/静态属性 是独立的,通过 weakMap 实现

实例方法,静态方法,私有方法,getter/setter

// Input
class Person {
  #hello = 'hello'
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return 'hello, I am ' + this.#privateSayHello();
  }
  static onlySayHello() {
    return 'hello';
  }
  #privateSayHello() {
  	return this.#hello;
  }
  get name() {
    return 'foo';
  }
  set name(newName) {
    console.log('new name 为:' + newName);
  }
}
// Output
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// 1. 会把设置的值变成不可枚举的,符合上文规范中要求的【类的内部所有定义的方法,都是不可枚举的】
// 2. 对于 getter setter,需要设置成不可写的,进来就是没有 value 字段
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}
// 将实例方法添加到原型链上,将静态方法添加到构造函数上
function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

// 以下三个为获取私有属性用
function _classPrivateFieldGet(receiver, privateMap) {
    var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get");
    return _classApplyDescriptorGet(receiver, descriptor);
}
// 保护调用,获得 weakMap 里通过构造函数绑定的方法
function _classExtractFieldDescriptor(receiver, privateMap, action) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to " + action + " private field on non-instance");
    }
    return privateMap.get(receiver);
}
// 调用方法,获得私有属性的值
function _classApplyDescriptorGet(receiver, descriptor) {
    if (descriptor.get) {
        return descriptor.get.call(receiver);
    }
    return descriptor.value;
}

// 调用私有方法用
// 调用时,检查是否有这个 weakmap 在实例上,有则执行对应方法
function _classPrivateMethodGet(receiver, privateSet, fn) {
    if (!privateSet.has(receiver)) {
        throw new TypeError("attempted to get private field on non-instance");
    }
    return fn;
}

var _hello = new WeakMap();
var _privateSayHello = new WeakSet();
var Person = /*#__PURE__*/function () {
  function Person(name) {
    _classCallCheck(this, Person);
    _privateSayHello.add(this);
    // 设置私有属性的值
    _hello.set(this, {
      writable: true,
      value: 'hello'
    });
    this.name = name;
  }
  // 第一个参数是构造函数,二个参数是实例方法,第三个参数是静态方法
  _createClass(Person, [{
    key: "sayHello",
    value: function sayHello() {
      return 'hello, I am ' + _classPrivateMethodGet(this, _privateSayHello, _privateSayHello2).call(this);
    }
  }, {
    key: "name",
    get: function get() {
      return 'foo';
    },
    set: function set(newName) {
      console.log('new name 为:' + newName);
    }
  }], [{
    key: "onlySayHello",
    value: function onlySayHello() {
      return 'hello';
    }
  }]);
  return Person;
}();

function _privateSayHello2() {
  return _classPrivateFieldGet(this, _hello);
}
复制代码

这里需要注意的几点

  • 在设置方法时,会把设置的值变成不可枚举的,符合上文规范中要求的【类的内部所有定义的方法,都是不可枚举的】
  • 对于 getter/setter ,是不可写的
  • 私有方法也是靠 weakMap 实现,不过方法被定义到了构造函数外,通过构造函数和 weakMap 链接,同时需要注意私有方法和私有变量取值不一样之处

Babel是如何让你通过 Class 继承的

上面我们看了一下 Class 中各种各样的属性、方法会被编译成什么样子,接下来我们看看是如何用他们实现继承的

只有构造函数

注意,在子类的 constructor 里要使用 this 的话,必须调用一次父类的构造函数,也就是 super() (类似 ES5 中的 Parent.call(this) )。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。

// Input - 寄生组合式继承
class Parent {
  constructor(name) {
    this.name = name;
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}
const child1 = new Child('foo', '18');

// Output
"use strict";
function _typeof(obj) {
  "@babel/helpers - typeof";
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    _typeof = function _typeof(obj) {
      return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
    };
  }
  return _typeof(obj);
}
// 重点,继承的核心
function _inherits(subClass, superClass) {
  // extend 的继承目标必须是函数或者是 null
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  // 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      writable: true,
      configurable: true
    }
  });
  // 设置子类的 __proto__ 属性指向父类,这样能让子类访问到父类上的静态方法
  if (superClass) _setPrototypeOf(subClass, superClass);
}
// 工具方法,设置一个对象的 __proto__
function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
    o.__proto__ = p;
    return o;
  };
  return _setPrototypeOf(o, p);
}
// 创建一个 super,来调用父元素构造函数,主要是处理其返回类型
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    // 获取原型,也就是Parent(在之前已经完成了继承)
    var Super = _getPrototypeOf(Derived),
        result;
    if (hasNativeReflectConstruct) {
      // 有 Reflect 就走高端方案
      // 作为新创建对象的原型对象的 constructor 属性
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      // 没有 Reflect 的环境就当成构造函数调用一下
      result = Super.apply(this, arguments);
    }
    // 检查返回值
    return _possibleConstructorReturn(this, result);
  };
}
// 用于处理构造函数返回值——规范允许在构造函数内主动返回一个 对象、方法,否则则返回构造函数内的 this
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}
// 工具方法,判断 this 是不是存在
function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return self;
}
// 工具方法,是否支持原生的 Reflect
function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {}));
    return true;
  } catch (e) {
    return false;
  }
}
// 工具方法,返回指定对象的原型
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
    return o.__proto__ || Object.getPrototypeOf(o);
  };
  return _getPrototypeOf(o);
}
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Parent = function Parent(name) {
  _classCallCheck(this, Parent);
  this.name = name;
};

var Child = /*#__PURE__*/function (_Parent) {
  // 继承原型链
  _inherits(Child, _Parent);
  // 创建一个super
  var _super = _createSuper(Child);
  function Child(name, age) {
    var _this;
    _classCallCheck(this, Child);
    // 调用 super,拿到 this
    _this = _super.call(this, name);
    _this.age = age;
    return _this;
  }
  return Child;
}(Parent);

var child1 = new Child('foo', '18');
复制代码

这块最核心的其实就两个:如何处理 super 和如何处理继承:

  • 如何处理继承
    • 防御性处理,extend 的继承目标必须是函数或者是 null
    • 继承的核心和 ES5 的寄生组合式继承类似,使用 create 完成
    • ES6中基于 Class 的继承,子类能继承父类的静态方法,所以需要让子类的 __proto__ 指向父类
// 重点,继承的核心
function _inherits(subClass, superClass) {
  // extend 的继承目标必须是函数或者是 null
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  // 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      writable: true,
      configurable: true
    }
  });
  // 设置子类的 __proto__ 属性指向父类,这样能让子类访问到父类上的静态方法
  if (superClass) _setPrototypeOf(subClass, superClass);
}
复制代码
  • 如何处理 super
    • 本质上,super 类似于 Parent.call(this)
    • 需要注意的是额外处理了返回值
// 创建一个 super,来调用父元素构造函数,主要是处理其返回类型
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    // 获取原型,也就是Parent(在之前已经完成了继承)
    var Super = _getPrototypeOf(Derived),
        result;
    if (hasNativeReflectConstruct) {
      // 有 Reflect 就走高端方案
      // 作为新创建对象的原型对象的 constructor 属性
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      // 没有 Reflect 的环境就当成构造函数调用一下
      result = Super.apply(this, arguments);
    }
    // 检查返回值
    return _possibleConstructorReturn(this, result);
  };
}
// 用于处理构造函数返回值——规范允许在构造函数内主动返回一个 对象、方法,否则则返回构造函数内的 this
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}
复制代码

继承自内建对象

// Input
class Child extends Array {
  constructor(value) {
    super(value);
  }
}
const child1 = new Child(1);
// Output
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _wrapNativeSuper(Class) {
  // 基于 Map 的缓存
  var _cache = typeof Map === "function" ? new Map() : undefined;
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    // 保护,如果没有,或者不是原生的 function 则直接返回
    if (Class === null || !_isNativeFunction(Class)) return Class;
    if (typeof Class !== "function") {
      throw new TypeError("Super expression must either be null or a function");
    }
    if (typeof _cache !== "undefined") {
      if (_cache.has(Class)) return _cache.get(Class);
      _cache.set(Class, Wrapper);
    }

    function Wrapper() {
      // 用父类构造函数生成了新的实例
      return _construct(Class, arguments, _getPrototypeOf(this).constructor);
    }
    // 把父类的原型方法挂上去
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    // 修正 __proto__ 指向
    return _setPrototypeOf(Wrapper, Class);
  };
  // 所以这里返回的是父类的一个实例
  return _wrapNativeSuper(Class);
}
// 工具函数,Reflect.construct 的 polyfill
function _construct(Parent, args, Class) {
    if (_isNativeReflectConstruct()) {
        _construct = Reflect.construct;
    } else {
        _construct = function _construct(Parent, args, Class) {
            var a = [null];
            a.push.apply(a, args);
            var Constructor = Function.bind.apply(Parent, a);
            var instance = new Constructor();
            if (Class) _setPrototypeOf(instance, Class.prototype);
            return instance;
        };
    }
    return _construct.apply(null, arguments);
}
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _isNativeFunction(fn) {
    return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

var Child = /*#__PURE__*/function (_Array) {
  _inherits(Child, _Array);
  var _super = _createSuper(Child);
  function Child(value) {
    _classCallCheck(this, Child);
    return _super.call(this, value);
  }
  return Child;
}( /*#__PURE__*/_wrapNativeSuper(Array));

var child1 = new Child('foo');
复制代码

这里很好的解决了一个问题,你会发现在之前 ES5 的处理中,我们没有处理继承内建对象,比如:ArrayDate等。这是因为之前都是调用父类构造函数并使用 call 改变 this 指向子类实例实现的(Parent.call(this, foo))。但是对于原生构造函数来说,会有几种情况:

  • 一些会忽略 apply/call 方法传入的 this,也就是说原生构造函数 this 无法绑定,导致子类实例拿不到内部属性。
  • 一些在底层有限制,如 Date ,如果调用对象的 [[Class]] 不是 Date,则抛出错误


而现在则通过包装,调用时会返回 new 父类出来的实例,从而借助其获得内建对象上的方法:

function _wrapNativeSuper(Class) {
  // 基于 Map 的缓存
  var _cache = typeof Map === "function" ? new Map() : undefined;
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    // 保护,如果没有,或者不是原生的 function 则直接返回
    if (Class === null || !_isNativeFunction(Class)) return Class;
    if (typeof Class !== "function") {
      throw new TypeError("Super expression must either be null or a function");
    }
    if (typeof _cache !== "undefined") {
      if (_cache.has(Class)) return _cache.get(Class);
      _cache.set(Class, Wrapper);
    }

    function Wrapper() {
      // 用父类构造函数生成了新的实例
      return _construct(Class, arguments, _getPrototypeOf(this).constructor);
    }
    // 把父类的原型方法挂上去
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    // 修正 __proto__ 指向
    return _setPrototypeOf(Wrapper, Class);
  };
  // 所以这里返回的是父类的一个实例
  return _wrapNativeSuper(Class);
}
复制代码

小结

本文从原型链讲起,主要针对 JavaScript 中基于原型链继承的原理,ES5中各种继承的优缺点,以及 Class 语法糖本身的样子 进行了一定介绍。主要还是偏向于基础知识层面,后续会结合设计模式、TypeScript 以及有关 polyfill 进行学习

文章分类
前端
文章标签