FE-DSHUI / DSHUI

前端王者小分队读书会
4 stars 1 forks source link

《重学ES6:class及extends的实现》——2021-03-08 #62

Open isbaselvy opened 3 years ago

isbaselvy commented 3 years ago
/**
 * 1. class 实现
 * 先从最简单的 class 开始看,下面这段代码涵盖了使用 class 时所有会出现的情况(静态属性、构造函数、箭头函数)。
 */
{
class Person {
    static instance = null;
    static getInstance() {
        return super.instance;
    }
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // 不直接使用 = 来定义的方法,最终都会被挂载到原型上
    sayHi() {
        console.log('hi');
    }
    // 使用 = 定义的属性和方法,最终都会被放到构造函数中
    sayHello = () => {
        console.log('hello');
    }
    sayBye = function () {
        console.log('bye');
    }
}

/**
 * 转换成ES5
 * 最外层的 Person 变量被赋值给了一个立即执行函数,立即执行函数里面返回的是里面的 Person 构造函数,
 * 实际上最外层的 Person 就是里面的 Person 构造函数。
 * 在 Person 类上用 static 设置的静态属性instance,在这里也被直接挂载到了 Person 构造函数上。
 * 你是不是很好奇,为什么在 Person 类上面设置的 sayHi 和 sayHello、sayBye 三个方法,编译后被放到了不同的地方处理?
 * 从编译后的代码中可以看到 sayHello 和 sayBye 被放到了 Person 构造函数中定义,
 * 而 sayHi 用 _createClass 来处理(_createClass 将 sayHi 添加到了 Person 的原型上面)。
 * 曾经我也以为是 sayHello 使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到 sayBye 这种用法才知道这和箭头函数无关。
 * 实际上 class 中定义属性还有一种写法,这种写法和 sayBye 如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。 
 */
'use strict';

/**
 * _createClass
 * @param Constructor 构造函数
 * @param protoProps 需要挂载到原型上的方法
 * @param staticProps 需要挂载到类上的静态方法
 * 这个函数在 Person 原型上面添加了 sayHi 方法。
 */
var _createClass = function () {
    function defineProperties(target, props) {
        // 设置了传入的各属性的 enumerable(是否可枚举) 和 configurable(是否可配置)、writable(是否可修改)等数据属性
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            // todo 疑问 enumerable的属性不可被遍历到
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            // 有value属性则可写
            if ("value" in descriptor)
                descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
    return function (Constructor, protoProps, staticProps) {
        // 如果有 protoProps ,那么挂载到 Constructor 原型上面。
        if (protoProps) {
            defineProperties(Constructor.prototype, protoProps);
        }

        // 如果有 staticProps,则挂载到 Constructor 构造函数上
        if (staticProps) {
            defineProperties(Constructor, staticProps);
        }

        return Constructor;
    };
}();

/**
 * Person 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。
 * 在 _classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,
 * 如果不在上面则抛出错误。
 * 这一步主要是为了避免直接将 Person 类当做函数来调用。
 * 因此,在 ES5 中构造函数是可以当做普通函数来调用的,但在 ES6 中的类是无法直接当普通函数来调用的
 */
function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Person = function () {
    function Person(name, age) {
        // 检查是Person类是否被当初构造函数调用,不是则不执行
        _classCallCheck(this, Person);
        // 使用 = 定义的属性和方法,最终都会被放到构造函数中
        this.sayHello = function () {
            console.log('hello');
        };

        this.sayBye = function () {
            console.log('bye');
        };

        this.name = name;
        this.age = age;
    }

    _createClass(Person, [{
        key: 'sayHi',
        value: function sayHi() {
            console.log('hi');
        }
    }]);

    return Person;
}();

Person.instance = null;

{
    /**
     * 如果我们将 name 后面的 'tom' 换成函数呢?甚至箭头函数呢?这不就是 sayBye 和 sayHello 了吗?
    因此,在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中。
     */
    class Person {
        name = 'tom';
        age = 23;
    }
    // 等价于
    class Person {
        constructor() {
            this.name = 'tom';
            this.age = 23;
        }
    }
}
}

/**
 * 2. extends 实现
 * 以下面的 ES6 代码为例:
 */
{
    class Parent {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }
        getName() {
            return this.name;
        }
        getAge() {
            return this.age;
        }
    }
    class Child extends Parent {
        constructor(name, age) {
            super(name, age);
            this.name = name;
            this.age = age;
        }
        getName() {
            return this.name;
        }
    }

    /**
     * babel 后的代码则是这样的:
     */
    "use strict";

    // 省略 _createClass
    // 省略 _classCallCheck
    // 省略Person

    /**
     * 调用父类的构造函数
     * @param {*} self 构造函数的 this
     * @param {*} call :Object.getPrototypeOf(Child).call(this, name, age)),
     * 在 _inherits 中设置了 Child 的 [[Prototype]] 指向了 Parent,
     * 因此可以将后面这串代码简化为 Parent.call(this, name, age)。
     * @returns :正常情况下,应该会返回 undefined,但不排除 Parent 构造函数中直接返回一个对象或者函数的可能性。
     */
    function _possibleConstructorReturn(self, call) {
        // 如果没有 self,这里就会直接抛出错误,提示 super 函数还没有被调用。
        if (!self) {
            throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
        }
        // 最后会对 call 进行判断,如果 call 为引用类型,那么返回 call,否则返回 self。
        // call 就是 Parent.call(this, name, age) 执行后返回的结果。
        return call &&
            (typeof call === "object" || typeof call === "function") ? call : self;
    }

    /**
     * 类似寄生组合是继承
     * 1.设置 subClass.prototype 的 [[Prototype]]指向 superClass.prototype 的 [[Prototype]]
     * 2.设置 subClass 的 [[Prototype]] 指向 superClass
     * @param {*} subClass 子构造函数
     * @param {*} superClass 父构造函数
     */
    function _inherits(subClass, superClass) {
        // 父类必须是个function,不是则抛出报错
        if (typeof superClass !== "function" && superClass !== null) {
            throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
        }
        // 类似寄生组合式继承,将 subClass.prototype 设置为 superClass.prototype 的实例
        subClass.prototype = Object.create(superClass && superClass.prototype, {
            constructor: {
                value: subClass,
                enumerable: false,
                writable: true,
                configurable: true
            }
        });
        // 将 子类 设置为 父类 的实例(优先使用 Object.setPrototypeOf)
        if (superClass)
            Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
    }

    var Child = function (_Parent) {
        _inherits(Child, _Parent);

        function Child(name, age) {
            // 判断当前实例是否是调用constructor生成
            _classCallCheck(this, Child);

            // 在 Child 方法中,最终拿到 _possibleConstructorReturn 执行后的结果作为新的 this 来设置构造函数里面的属性
            var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));
            //todo  如果直接用 this,而不是 _this,会出现什么问题?
            _this.name = name;
            _this.age = age;
            return _this;
        }

        // 将getName方法挂在Child的原型上
        _createClass(Child, [{
            key: "getName",
            value: function getName() {
                return this.name;
            }
        }]);

        return Child;
    }(Parent);

    /**
     * 小课堂:构造函数的饿返回
     * 在构造函数中,如果什么也没有返回或者返回了原始值,那么默认会返回当前的 this;
     * 而如果返回的是引用类型,那么最终实例化后的实例依然是这个引用类型(仅相当于对这个引用类型进行了扩展)。
     */
    {
        const obj = {};
        function Parent(name) {
            this.name = name;
            return obj;
        }
        const p = new Parent('tom');
        obj.name; // 'tom'
        p === obj; // true
    }
}