[JavaScript之原型和原型链]Class

类定义

ECMAScript 中没有正式的类这个类型。类(class)是 ECMAScript 中新的基础性语法糖结构,背后使用的仍然是原型和构造函数的概念。

// 类声明
class Person {}
// 类表达式
const Animal = class {} 
//-------------------------------------需要注意的细节-------------------------------------

//1.类没有变量提升
console.log(Person)
class Person {}// Uncaught ReferenceError: Cannot access 'Person' before initialization

//2.类受块作用域限制
{
    class ClassDeclaration {}
}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined

//3.类不可配置,不可枚举,不可重写
class Person {}
console.log(Object.getOwnPropertyDescriptor(Person, 'prototype'))//{value: {…}, writable: false, enumerable: false, configurable: false}

//4.Class 默认就是严格模式
复制代码

从各方面来看,Class就是一种特殊函数。

class Person {}
// 使用 typeof 操作符检测 Person 是 function 类型。
console.log(Person) // class Person {}
console.log(typeof Person) // function
// Person 有 prototype 属性,原型对象也有 constructor 属性指向类自身。
console.log(Person.prototype) // { constructor: f() }
console.log(Person === Person.prototype.constructor) // true
// 使用 instanceof 操作符检查 Person 的原型在实例 p 的原型链上。  
let p = new Person()
console.log(p instanceof Person) // true
复制代码

class 是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递。

let classList = [
    class {
        constructor(id) {
            this.id_ = id
            console.log(`instance ${this.id_}`)
        }
    }
]

function createInstance(classDefinition, id) {
    return new classDefinition(id)
}
let foo = createInstance(classList[0], 1024) // instance 1024
复制代码

与立即调用函数表达式相似,类也可以立即实例化:

// 因为是一个类表达式,所以类名是可选的
let p = new class Foo {
    constructor(x) {
        console.log(x)
    }
}('bar') // bar
console.log(p) // Foo {}
复制代码

类的构成

类构造函数

constructor 关键字用于在class定义块内部创建class的构造函数。不定义constructor相当于将构造函数定义为空函数。

class Person {} 
//等价于
class Person {
    constructor() {}
}
复制代码

使用 new 操作符实例化 Person 的操作意味着应该使用 constructor 函数进行实例化。在constructor 内部,可以为新创建的实例(this)添加属性。

class Person {
    constructor(age, job) {
        // 这个例子先使用对象包装类型定义一个字符串,为的是在下面测试两个对象的相等性。
        this.name = new String("小刘")
        this.age = age
        this.sayName = () => console.log(this.name)
        this.job = job
    }
}
// 类实例化时传入的参数会用作构造函数的参数。
let p1 = new Person(18, "前端工程师")
console.log(p1.name, p1.age, p1.job) //小刘,18
p1.sayName() //小刘

let p2 = new Person(18, "前端工程师")
// 每个实例都对应一个唯一的成员对象,不会在原型上共享。
console.log(p1.name === p2.name) // false
复制代码

默认情况下,类构造函数会在执行之后返回 this 对象,在这里是undefined(由于 class 内部是严格模式,所以 this 实际指向的是undefined)。

QQ图片20210527164653

原型方法

为了在实例间共享方法,Class定义语法把在Class块中定义的方法作为原型方法。

class Person {
    constructor() {
        // 添加到 this 的所有内容都会存在于不同的实例上
        this.locate = () => console.log('instance')
    }
    // 在类块中定义的所有内容都会定义在类的原型上
    locate() {
        console.log('prototype')
    }
    // name: "小刘" //Uncaught SyntaxError: Unexpected identifier,不能在类块中给原型添加原始值或对象作为成员数据。
}

let p = new Person()
p.locate() // instance
Person.prototype.locate() // prototype
复制代码

类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键。

const symbolKey = Symbol('symbolKey')
class Person {
    stringKey() {
            console.log('invoked stringKey')
    }
    [symbolKey]() {
            console.log('invoked symbolKey')
    }
    ['computed' + 'Key']() {
            console.log('invoked computedKey')
    }
}
let p = new Person()
p.stringKey() // invoked stringKey
p[symbolKey]() // invoked symbolKey
p.computedKey() // invoked computedKey
复制代码

访问器

类定义也支持获取和设置访问器。

class Person {
    set name(newName) {
        this.name_ = newName
    }
    get name() {
        return this.name_
    }
}

let p = new Person
p.name = "小刘"
console.log(p.name) // 小刘
复制代码

静态方法

如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

  • 静态成员每个类上只能有一个,多个同名静态成员会被覆盖,生效的只有最后一个。
  • 静态方法的this指向Class
  • 父类的静态方法子类可以继承。
  • super方法可以调用父类上的静态方法。
class Person {
    constructor() {
        // 添加到 this 的所有内容都会存在于不同的实例上
        this.locate = () => console.log('instance', this)
    }
    // 定义在类的原型对象上
    locate() {
        console.log('prototype', this)
    }
    // 定义在类本身上
    static locate() {
        console.log('class', this)
    }
}
let p = new Person
p.locate() // instance, Person {}
Person.prototype.locate() // prototype, {constructor: ƒ, locate: ƒ}
Person.locate() // class, class Person {...},静态方法直接通过类标识符调用
复制代码

静态类方法非常适合作为实例工厂。

class Person {
    constructor(age) {
        this.age_ = age
    }
    sayAge() {
        console.log(this.age_)
    }
    static create() {
        // 使用随机年龄创建并返回一个 Person 实例
        return new Person(Math.floor(Math.random() * 100))
    }
}
console.log(Person.create()) // Person { age_: ... }
复制代码

迭代器与生成器方法

class Person {
    // 在原型上定义生成器方法
    * createNicknameIterator() {
        yield "小刘"
        yield "小孙"
        yield "小李"
    }
    // 在类上定义生成器方法
    static * createJobIterator() {
        yield "前端工程师"
        yield "后端工程师"
        yield "全栈工程师"
    }
}
let jobIter = Person.createJobIterator()
console.log(jobIter.next().value) // 前端工程师
console.log(jobIter.next().value) // 后端工程师
console.log(jobIter.next().value) // 全栈工程师
let p = new Person()
let nicknameIter = p.createNicknameIterator()
console.log(nicknameIter.next().value) // 小刘
console.log(nicknameIter.next().value) // 小孙
console.log(nicknameIter.next().value) // 小李
复制代码

因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象。

class Person {
    constructor() {
            this.nicknames = ["小刘", "小孙", "小李"]
        }
        *[Symbol.iterator]() {
            yield* this.nicknames.entries()
        }
}
let p = new Person()
for (let [idx, nickname] of p) {
    console.log(nickname)
}
//小刘 
//小孙
//小李
复制代码

也可以只返回迭代器实例。

class Person {
    constructor() {
            this.nicknames = ["小刘", "小孙", "小李"]
        }
        [Symbol.iterator]() {
            return this.nicknames.entries()
        }
}
let p = new Person()
for (let [idx, nickname] of p) {
    console.log(nickname)
}
复制代码

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

以前只能在Class外部定义,现在通过前缀static关键字就可以实现。

class Person {
    sayName() {
        console.log(`${Person.greeting} ${this.name}`)
    }
}
// 在类上定义数据成员
Person.greeting = "我的名字叫" // 在原型上定义数据成员
Person.prototype.name = "小刘"
let p = new Person()
p.sayName() // 我的名字叫 小刘
//--------------------------------------------------------------------
class Person {
    sayName() {
        console.log(`${Person.greeting} ${this.name}`)
    }
    static greeting = "我的名字叫"
}
// 在类上定义数据成员
Person.prototype.name = "小刘"
let p = new Person()
p.sayName() // 我的名字叫 小刘

复制代码

类的继承

ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有 [[Construct]] 和原型的对象。

class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer()
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true
复制代码

派生类都会通过原型链访问到类和原型上定义的方法。this 的值会反映调用相应方法的实例或者类。

class Vehicle {
    identifyPrototype(id) {
        console.log(id, this)
    }
    static identifyClass(id) {
        console.log(id, this)
    }
}
class Bus extends Vehicle {
    constructor(){
        super()
        this.name = "宇通"
    }
    
}
let v = new Vehicle()
let b = new Bus()
b.identifyPrototype('bus') // bus, Bus {name: "宇通"}
v.identifyPrototype('vehicle') // vehicle, Vehicle {}
Bus.identifyClass('bus') // bus, class Bus extends Vehicle {...}
Vehicle.identifyClass('vehicle') // vehicle, class Vehicle {...}
复制代码

extends 关键字也可以在类表达式中使用,因此 let Bar = class extends Foo {} 是有效的语法。

super()

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。

class Vehicle {
    constructor() {
        this.hasEngine = true
    }
    static identify() {
        console.log('vehicle')
    }
}
class Bus extends Vehicle {
    constructor() {
        /**
         * 不要在调用 super()之前引用 this,否则会抛出 ReferenceError。
         * 这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,
         * 然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
         */
        super() // 相当于 super.constructor(),在类构造函数中使用 super 可以调用父类构造函数。
        console.log(this instanceof Vehicle) 
        console.log(this) 
    }
    static identify() {
        super.identify() // 在静态方法中可以通过 super 调用父类上定义的静态方法。
    }
}
new Bus()
// true
// Bus { hasEngine: true }
Bus.identify() 
//vehicle
复制代码

注意:ES6 给类构造函数和静态方法添加了内部特性 [[HomeObject]] ,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为 [[HomeObject]] 的原型。

在使用super时要注意几个问题。

super 只能在派生类构造函数静态方法中使用。

//
class Vehicle {
    constructor() {
        super()
        // Uncaught SyntaxError: 'super' keyword unexpected here
    }
}
复制代码

不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法

class Vehicle {}
class Bus extends Vehicle {
    constructor() {
        console.log(super)// Uncaught SyntaxError: 'super' keyword unexpected here
    }
}
复制代码

调用 super()调用父类构造函数,并将返回的实例赋值给 this

class Vehicle {}
class Bus extends Vehicle {
    constructor() {
        super()
        console.log(this instanceof Vehicle)
    }
}
new Bus() // true
复制代码

super() 的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。

class Vehicle {
    constructor(licensePlate) {
        this.licensePlate = licensePlate
    }
}
class Bus extends Vehicle {
    constructor(licensePlate) {
        super(licensePlate)
    }
}
console.log(new Bus('CODE999')) // Bus {licensePlate: "CODE999"}
复制代码

如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的参数

class Vehicle {
    constructor(licensePlate) {
        this.licensePlate = licensePlate
    }
}
class Bus extends Vehicle {}
console.log(new Bus('CODE999')) // Bus {licensePlate: "CODE999"}
复制代码

在类构造函数中,不能在调用 super() 之前引用 this

class Vehicle {}
class Bus extends Vehicle {
    constructor() {
        console.log(this)
    }
}
new Bus()
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
复制代码

如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象

class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
    constructor() {
        super()
    }
}
class Van extends Vehicle {
    constructor() {
        return {}
    }
}
console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}
复制代码

抽象基类

new.target 保存通过 new 关键字调用的类或函数。通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化。

// 抽象基类
class Vehicle {
    constructor() {
        console.log(new.target)
        if (new.target === Vehicle) {
            throw new Error('Vehicle cannot be directly instantiated')
        }
    }
}
// 派生类
class Bus extends Vehicle {}
new Bus() // class Bus {...},new.target 指向的是初始化类的类定义。
new Vehicle() // class Vehicle {...},new.target 指向的是初始化类的类定义。
// Error: Vehicle cannot be directly instantiated
复制代码

通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法。

// 抽象基类
class Vehicle {
    constructor() {
        if (new.target === Vehicle) {
            throw new Error('Vehicle cannot be directly instantiated')
        }
        if (!this.foo) {
            throw new Error('Inheriting class must define foo()')
        }
        console.log('success!')
    }
}
// 派生类
class Bus extends Vehicle {
    foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus() // success!
new Van() // Error: Inheriting class must define foo()
复制代码

继承内置类型

ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型。

class SuperArray extends Array {
    shuffle() {
        // 洗牌算法
        for (let i = this.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1))[this[i], this[j]] = [this[j], this[i]]
        }
    }
}
let a = new SuperArray(1, 2, 3, 4, 5)
console.log(a instanceof Array) // true
console.log(a instanceof SuperArray) // true
console.log(a) // [1, 2, 3, 4, 5]
a.shuffle()
console.log(a) // [3, 1, 4, 5, 2]
复制代码

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的。

class SuperArray extends Array {}
let a1 = new SuperArray(1, 2, 3, 4, 5)
let a2 = a1.filter(x => !!(x % 2)) //!!就是将其他类型都转换成boolean型
console.log(a1) // [1, 2, 3, 4, 5]
console.log(a2) // [1, 3, 5]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // true
复制代码

如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类。

class SuperArray extends Array {
    static get[Symbol.species]() {
        return Array
    }
}
let a1 = new SuperArray(1, 2, 3, 4, 5)
let a2 = a1.filter(x => !!(x % 2))
console.log(a1) // [1, 2, 3, 4, 5]
console.log(a2) // [1, 3, 5]
console.log(a1 instanceof SuperArray) // true
console.log(a2 instanceof SuperArray) // false
复制代码

类混入

把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。

注意 Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。

extends 关键字后可以跟表达式(可以解析为一个类或者一个构造函数)。这个表达式会在求值类定义时被求值。

class Vehicle {}

function getParentClass() {
    console.log('evaluated expression')
    return Vehicle
}
class Bus extends getParentClass() {}// evaluated expression(可求值的表达式)
复制代码

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。

如果 Person 类需要组合 A、B、C,则需要某种机制实现 B 继承 A,C 继承 B,而 Person再继承 C,从而把 A、B、C 组合到这个超类中。实现这种模式有不同的策略。
一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:

class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
    foo() {
        console.log('foo')
    }
}
let BarMixin = (Superclass) => class extends Superclass {
    bar() {
        console.log('bar')
    }
}
let BazMixin = (Superclass) => class extends Superclass {
    baz() {
        console.log('baz')
    }
}
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus()
b.foo() // foo
b.bar() // bar
b.baz() // baz
复制代码

通过写一个辅助函数,可以把嵌套调用展开:

class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
    foo() {
        console.log('foo')
    }
}
let BarMixin = (Superclass) => class extends Superclass {
    bar() {
        console.log('bar')
    }
}
let BazMixin = (Superclass) => class extends Superclass {
    baz() {
        console.log('baz')
    }
}

function mix(BaseClass, ...Mixins) {
    return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass)
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus()
b.foo() // foo
b.bar() // bar
b.baz() // baz
复制代码

Class实现原理

Class的底层实现原理可以通过Babel来观察一下。

类的实现

类构造函数

//转换前
class Person {
  constructor(name) {
        this.name = name
        this.sayName = () => {
            console.log("我的名字叫" + this.name)
        }
    }
}
//转换后
function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Person = function Person(name) {
    "use strict";

    var _this = this;

    _classCallCheck(this, Person);

    this.name = name;

    this.sayName = function () {
        console.log("我的名字叫" + _this.name);
    };
};
复制代码
  1. 先声明一个_classCallCheck方法,这个方法的作用是为了检查类是不是通过new关键字调用的。如果不是(也就是直接调用Person()),就抛出Cannot call a class as a function错误。

    image-20210529094445793

  2. 类是严格模式(use strict)。

  3. constructor的属性和方法都赋值给this(实例)。

  4. 执行constructor内部的逻辑。

  5. return this(默认情况下,类构造函数会在执行之后返回 this 对象)。

原型方法和静态方法

//转换前
class Person {
    //原型方法
    sayAge() {
        console.log("我今年 " + this.age)
    }
    //静态属性
    static job = "前端工程师"
    //静态方法
    static sayJob() {
        console.log("我的工作是 " + Person.job)
    }
}
//原型属性
Person.prototype.age = 18

//转换后
function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
        var descriptor = props[i];
        // enumerable 默认为 false
        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 _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;
}

var Person = /*#__PURE__*/ function () {
    "use strict";

    function Person() {
        _classCallCheck(this, Person);
    }

    _createClass(Person, [{
        key: "sayAge",
        value: function sayAge() {
            console.log("我今年 " + this.age);
        }

    }], [{
        key: "sayJob",
        value: function sayJob() {
            console.log("我的工作是 " + Person.job);
        }
    }]);

    return Person;
}();

_defineProperty(Person, "job", "前端工程师");

Person.prototype.age = 18;
复制代码
  1. 可以看到,此时的Person变成了一个IIFEIIFE执行之后仍然返回Person类,但内部还封装了一个constructor构造函数和一个_createclass函数。

  2. 构造函数Person,这个构造函数里面有我们检查是否用new调用的_classCallCheck方法。虽然上面没有显示调用constructor,但是Class默认会自动创建一个,这也就是书中所说的空constructor

  3. _defineProperties函数,它接收类(或者类的原型)和一个存放对象的数组做为参数,之后遍历数组中的每个对象,定义每个方法的特性,并将它们逐一添加到类(或者类的原型)上面。这里所涉及到的特性包括:

    • enumberable:该属性(方法)是否可枚举。如果方法本身已经定义了该特性,则使用该特性;如果没有定义,则定义该方法为不可枚举。
    • configurable:该属性(方法)是否可以配置。
    • writable:如果该属性是数据属性而不是访问器属性,那么会有一个value,此时设置该属性为可写。

    默认 enumerablefalseconfigurabletrue,是为了防止 Object.keys() 之类的方法遍历到。

    然后通过判断 value 是否存在,来判断是否是 gettersetter

    如果存在 value,就为 descriptor 添加 valuewritable 属性,如果不存在,就直接使用 getset 属性。

  4. _createClass函数,首先,它可以接收三个参数:

    • 第一个参数:类(这里是Person类);
    • 第二个参数:存放对象的数组,每个对象都是关于类的原型方法的特性描述对象(这里是sayAge());
    • 第三个参数:存放对象的数组,每个对象都是关于类的静态方法的特性描述对象(这里是sayJob())。

    接着,它会依次检查是否有传入第二个和第三个参数,如果有,就调用_defineProperties函数,分别为类的原型定义原型方法,为类本身定义静态方法。

  5. _defineProperty函数,它接收类,属性名和属性值作为参数,如果指定的属性在类或类的原型链中,定义每个属性的特性,并将它们逐一添加到类上面;或者,重写类上的属性值。这里所涉及到的特性包括:

    • enumberable:该方法可枚举。
    • configurable:该方法可配置。
    • writable:该方法可重写。

    最后返回类。

继承实现

//转换前
class Vehicle {

}
class Bus extends Vehicle {
    constructor() {
        super()
    }
}

//转换后
require("core-js/modules/es.symbol.description.js");

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) {
    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 _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}

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 _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 _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Vehicle = function Vehicle() {
    "use strict";

    _classCallCheck(this, Vehicle);
};

var Bus = /*#__PURE__*/ function (_Vehicle) {
    "use strict";

    _inherits(Bus, _Vehicle);

    var _super = _createSuper(Bus);

    function Bus() {
        _classCallCheck(this, Bus);

        return _super.call(this);
    }

    return Bus;
}(Vehicle);

复制代码
  • _typeof(obj)函数是 一个工具函数,主要是为了对 Symbol 进行正确的处理。
    • 它首先会检查当前环境是否支持原生的 Symbol,如果支持就直接返回 typeof obj 表达式的计算结果;
    • 如果不支持,再检查 obj 是不是通过 polyfill 实现的 Symbol 的一个实例,如果是就返回它的类型(也就是返回 "symbol"),如果不是,就返回 typeof obj 的计算结果。
    • 在这里,这个函数假定了我们当前的环境是原生支持 Symbol 或者通过 polyfill 实现了支持的。
  • _setPrototypeOf(o, p)函数检查当前环境是否支持调用Object.setPrototypeOf()方法,如果不支持,就通过__proto__手动给实例建立原型关系。
  • _getPrototypeOf(o)函数检查当前环境是否支持调用 Object.setPrototypeOf()方法,如果支持就调用Object.getPrototypeOf()方法;如果不支持,就声明一个Object.setPrototypeOf()函数,返回o.__proto__
  • _possibleConstructorReturn(self,call) 这个辅助函数主要是用来实现 super() 的效果,对应到寄生组合式继承上则是借用构造函数继承的部分,有所不同的是,该方法返回一个 this 并赋给子类的 this
    • 两个参数self代表构造函数的实例,call代表构造函数的返回值;
    • if语句块里面的判断是检查call的类型,当它是一个对象或者函数的时候,直接将其作为返回值;
    • 否则就返回_assertThisInitialized(self)
  • _assertThisInitialized(self) 调用这个方法的时候,我们期望的结果是this已经得到初始化了。
    • 如果检查发现thisundefined,就会抛出一个错误,提示我们由于没有调用super(),所以无法得到this
    • 否则就返回this
    • 为什么用void 0而不用undefined是因为undefined在IE低版本中能被重写。
  • _isNativeReflectConstruct()这个方法用于检测当前环境是否支持原生的 Reflect
    • 使用Reflect.construct的主要原因是当调用来Reflect.construct()创建对象,new.target值会自动指定到target(或者newTarget,前提是newTarget指定了)。
    • 解决了使用Super.apply或者Object.create会将构造函数内部的new.target指向undefined的问题。

看一下最后面这段代码:

var Bus = /*#__PURE__*/ function (_Vehicle) {
    "use strict";

    _inherits(Bus, _Vehicle);

    var _super = _createSuper(Bus);

    function Bus() {
        _classCallCheck(this, Bus);

        return _super.call(this);
    }

    return Bus;
}(Vehicle);
复制代码

这里的Bus同样是一个IIFE,并且实际上也是返回一个Bus子类结构函数,不同的是,它内部还封装了其它方法的调用。

  • _inherits(Bus, _Vehicle)函数是实现继承的其中一个核心方法,可以说它的本质就是ES5中的寄生组合式继承。

    • 这个方法接收一个父类和子类作为参数
      1. 检查 extends 的继承目标(即父类),必须是函数或者是 null
      2. 使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype属性;
      3. 设置子类的 __proto__ 属性指向父类(目的是让子类继承父类的静态方法)。
  • _super.call(this)函数,就是_createSuper(Derived)函数,创建一个 super(),来调用父元素构造函数,主要是处理其返回类型。

    • 获取原型,也就是_Vehicle(在之前已经完成继承)。

    • 检查当前环境是否支持Reflect

      • 支持,执行Reflect.construct(Super, arguments, NewTarget),最终返回一个基于Super父构造函数的实例,相当于执行了new Super(...arguments)
        • 这个实例的__proto__constructorNewTarget,因此在某种程度上,你可以说这就是一个子类实例,不过它拥有父类实例的所有属性。
      • 不支持,执行Super.apply(借用构造函数继承)。
    • 检查返回值

      • 此时result的结果可能是三种:

        • 一个继承了父类实例所有属性的子类实例;
        • 可能是父类构造函数中自定义返回的一个非空对象;
        • 可能是默认返回的undefined
      • 如果是第一种和第二种情况,直接返回result

      • 第三种情况,对当初传进去的子类实例(已经通过Super.apply进行了增强),也就是this进行断言,然后返回出去。

参考:

javascript高级程序设计第四版

类 MDN

阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

JS 总结之 class

从 Babel 转译过程浅谈 ES6 实现继承的原理

ES6 系列之 Babel 是如何编译 Class 的(上)

ES6 系列之 Babel 是如何编译 Class 的(下)

从 Prototype 开始说起(下)—— ES6 中的 class 与 extends

es6类和继承的实现原理

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享