zoukankan      html  css  js  c++  java
  • 深入理解JS中的对象(三):class 的工作原理

    目录

    • 序言
    • class 是一个特殊的函数
    • class 的工作原理
    • class 继承的原型链关系
    • 参考

    1.序言

    ECMAScript 2015(ES6) 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法(class)不会为JavaScript引入新的面向对象的继承模型。


    2.class 是一个特殊的函数

    ES6 的 class 主要提供了更多方便的语法去创建老式的构造器函数。我们可以通过 typeof 得到其类型:

    class People {
        constructor(name) {
            this.name = name;
        }
    }
    
    console.log(typeof People) // function
    

    那 class 声明的类到底是一个什么样的函数呢?我们可以通过在线工具 ES6 to ES5 来分析 class 背后真正的实现。


    3.class 的工作原理

    下面通过多组代码对比,来解析 class 声明的类将转化成什么样的函数。


    第一组:用 class 声明一个空类

    ES6的语法:

    class People {}
    

    这里提出两个问题:

    1.class 声明的类与函数声明不一样,不会提升(即使用必须在声明之后),这是为什么?

    console.log(People) // ReferenceError
    
    class People {}
    

    在浏览器中运行报错,如下图:

    ReferenceError


    2.不能直接像函数调用一样调用类People(),必须通过 new 调用类,如 new People(),这又是为什么?

    class People {}
    
    People() // TypeError
    

    在浏览器中运行报错,如下图:

    TypeError


    转化为ES5:

    "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; 
        } 
    }
    
    // 判断 Constructor.prototype 是否出现在 instance 实例对象的原型链上
    function _classCallCheck(instance, Constructor) { 
        if (!_instanceof(instance, Constructor)) {
             throw new TypeError("Cannot call a class as a function"); 
        } 
    }
    
    var People = function People() {
      // 检查是否通过 new 调用
      _classCallCheck(this, People);
    };
    

    针对上面提到的两个问题,我们都可以用转化后的 ES5 代码来解答:

    对于问题1,我们可以看到 class 声明的类转化为的是一个函数表达式,并且用变量 People 保存函数表达式的值,而函数表达式只能在代码执行阶段创建而且不存在于变量对象中,所以如果在 class 声明类之前使用,就相当于在给变量 People 赋值之前使用,此时使用是没有意义的,因为其值为 undefined,直接使用反而会报错。所以 ES6 就规定了在类声明之前访问类会抛出 ReferenceError 错误(类没有定义)。

    对于问题2,我们可以看到 People 函数表达式中,执行了 _classCallCheck 函数,其作用就是保证 People 函数必须通过 new 调用。如果直接调用 People(),由于是严格模式下执行,此时的 this 为 undefined,调用 _instanceof 函数检查继承关系其返回值必然为 false,所以必然会抛出 TypeError 错误。

    补充:类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。


    第二组:给类添加公共字段和私有字段

    ES6的语法:

    class People {
        #id = 1      // 私有字段,约定以单个的`#`字符为开头
        name = 'Tom' // 公共字段
    }
    

    转化为ES5:

    ...
    
    // 将类的公共字段映射为实例对象的属性
    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 People = function People() {
      _classCallCheck(this, People);
    
      // 初始化私有字段
      _id.set(this, {
        writable: true,
        value: 1
      });
    
      // 将类的公共字段映射为实例对象的属性
      _defineProperty(this, "name", 'Tom');
    };
    
    // 转化后的私有字段(会自动检查命名冲突)
    var _id = new WeakMap();
    

    对比转化前后的代码可以看出:

    对于私有字段,在使用 class 声明私有字段时,约定是以字符 '#' 为开头,转化后则将标识符中的 '#' 替换为 '_',并且单独用一个 WeakMap 类型的变量来替代类的私有字段,声明在函数表达式后面(也会自动检查命名冲突),这样就保证了类的实例对象无法直接通过属性访问到私有字段(私有字段根本就没有在实例对象的属性中)。

    对于公共字段,则是通过 _defineProperty 函数将类的公共字段映射为实例对象的属性,如果是对已有属性进行重载,则会通过 Object.defineProperty 函数来进行设置,设置属性的可枚举性(enumerable)、可配置性(configurable)、可写性(writable)。


    第三组:给类添加构造函数与实例属性

    ES6的语法:

    class People {
        #id = 1      // 私有字段,约定以单个的`#`字符为开头
        name = 'Tom' // 公共字段
    
        constructor(id, name, age) {
          this.#id = id
          this.name = name
          this.age =  age // 实例属性 age
        }
    }
    

    转化为ES5:

    ...
    
    // 设置(修改)类的私有字段
    function _classPrivateFieldSet(receiver, privateMap, value) { 
        var descriptor = privateMap.get(receiver); 
        if (!descriptor) { 
            throw new TypeError("attempted to set private field on non-instance"); 
        } 
        if (descriptor.set) { 
            descriptor.set.call(receiver, value); 
        } else { 
            if (!descriptor.writable) { 
                throw new TypeError("attempted to set read only private field"); 
            } 
            descriptor.value = value; 
        } 
        return value; 
    }
    
    var People = function People(id, name, age) {
      _classCallCheck(this, People);
    
      _id.set(this, {
        writable: true,
        value: 1
      });
    
      _defineProperty(this, "name", 'Tom');
    
      // constructor 从这开始执行
      
      _classPrivateFieldSet(this, _id, id);
    
      this.name = name;
      this.age = age;
    };
    
    var _id = new WeakMap();
    

    对比转化前后的代码可以看出:

    类的构造函数(constructor)里面的代码的执行时机是在字段定义(字段映射为实例对象的属性)之后。而对私有字段的赋值(修改)是专门通过 _classPrivateFieldSet 函数来实现的。


    第四组:给类添加原型方法和静态方法

    ES6的语法:

    class People {
        #id = 1
        name = 'Tom'
    
        constructor(id, name, age) {
          this.#id = id
          this.name = name
          this.age =  age 
      }
      
        // 原型方法
        getName() { return this.name }
    
        // 静态方法
        static sayHello() { console.log('hello') }
    }
    

    转化为ES5:

    ...
    
    // 设置对象的属性
    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); 
        }
    }
    
    // 将类的方法映射到构造函数的原型(Constructor.prototype)的属性上
    // 将类的静态方法映射到构造函数(Constructor)的属性上
    function _createClass(Constructor, protoProps, staticProps) { 
        if (protoProps) _defineProperties(Constructor.prototype, protoProps); 
        if (staticProps) _defineProperties(Constructor, staticProps); 
        return Constructor; 
    }
    
    var People = function () {
      function People(id, name, age) {
        // ...
      }
    
      // 设置类的方法和静态方法
      _createClass(People, [{
        key: "getName",
        value: function getName() {
          return this.name;
        }
      }], [{
        key: "sayHello",
        value: function sayHello() {
          console.log('hello');
        }
      }]);
    
      return People;
    }();
    
    var _id = new WeakMap();
    

    对比一下第三组和第四组转化后的代码,可以明显发现:

    1. 类的字段通过 _defineProperty 函数映射到实例对象(this)的属性上。

    2. 类的方法则通过 _createClass 函数映射到构造函数的原型(Constructor.prototype)的属性上,

    3. 类的静态方也通过 _createClass 函数映射到构造函数(Constructor)的属性上。


    第五组:类的继承

    ES6的语法:

    // 父类(superClass)
    class People {}
    
    // 子类(subClass)继承父类
    class Man extends People {}
    

    转化为ES5:

    ...
    
    var People = function People() {
      _classCallCheck(this, People);
    };
    
    var Man = function (_People) {
      // Man 继承 _People
      _inherits(Man, _People);
    
      // 获取 Man 的父类的构造函数
      var _super = _createSuper(Man);
    
      function Man() {
        _classCallCheck(this, Man);
    
        // 实现了父类构造函数的调用, 子类的 this 继承父类的 this 上的属性
        return _super.apply(this, arguments);
      }
    
      return Man;
    }(People);
    

    在 _inherits 函数中,实现了原型链和静态属性的继承:

    // 实现继承关系
    function _inherits(subClass, superClass) { 
        if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } 
        // Object.create(proto, propertiesObject) 方法
        // 创建一个新对象,使用 proto 来提供新创建的对象的__proto__
        // 将 propertiesObject 的属性添加到新创建对象的不可枚举(默认)属性(即其自身定义的属性,而不是其原型链上的枚举属性)
        subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); 
        if (superClass) _setPrototypeOf(subClass, superClass); 
    }
    
    // 设置对象 o 的原型(即 __proto__ 属性)为 p
    function _setPrototypeOf(o, p) { 
        _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; 
        return _setPrototypeOf(o, p); 
    }
    

    1.通过 Object.create 函数调用可知:

    (1)subClass.prototype.__proto__ === superClass.prototype ,相当于实现了原型链的继承

    (2)subClass.prototype.constructor === subClass ,表明 subClass 构造函数的显示原型对象(prototype)的 constructor 属性指向原构造函数

    2.通过调用 _setPrototypeOf(subClass, superClass)可知:

    (1)subClass.__proto__ === superClass,相当于实现了静态属性的继承


    在 Man 构造函数中,通过调用其父类的构造函数(_super),实现了子类的 this 继承父类的 this 上的属性:

    // 获得父类的构造函数
    function _createSuper(Derived) { 
        var hasNativeReflectConstruct = _isNativeReflectConstruct(); 
        return function () { 
            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); 
        }; 
    }
    
    // 判断 call 的类型,返回合适的 Constructor
    function _possibleConstructorReturn(self, call) { 
        if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } 
        return _assertThisInitialized(self); 
    }
    
    // 断言 selft 是否初始化
    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 { 
            Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); 
            return true; 
        } catch (e) { 
            return false; 
        } 
    }
    
    // 获取 o 对象的原型(__proto__)
    function _getPrototypeOf(o) { 
        _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; 
        return _getPrototypeOf(o); 
    }
    

    从上述可知 class 继承的实现主要包含三部分:

    • 原型链的继承
    • 静态属性的继承
    • 通过调用父类的构造函数,获得父类的构造函数 this 上的属性

    4.class 继承的原型链关系

    实例代码:

    class People {
        constructor(name) {
            this.name = name
        }
    }
      
    class Man extends People {
        constructor(name, sex) {
            super(name)
            this.sex = sex
        }
    }
    
    var man = new Man('Tom', 'M')
    

    根据上面分析所知道的类(class)的继承的实现原理,并结合 深入理解JS中的对象(一):原型、原型链和构造函数 中所提到的构造函数的原型链关系,可得示例代码的完整原型链关系如下图:

    class 继承的原型链关系


    5.参考

    类- JavaScript | MDN

    exploring-es6 - class

    为什么说ES6的class是语法糖?

    深入理解JavaScript系列(15):函数(Functions)

    class继承做了什么呢?

  • 相关阅读:
    LDAP2-创建OU创建用户
    GNE: 4行代码实现新闻类网站通用爬虫
    为什么每一个爬虫工程师都应该学习 Kafka
    新闻网页通用抽取器GNEv0.04版更新,支持提取正文图片与源代码
    写了那么久的Python,你应该学会使用yield关键字了
    新闻类网页正文通用抽取器
    为什么Python 3.6以后字典有序并且效率更高?
    为什么你需要少看垃圾博客以及如何在Python里精确地四舍五入
    数据工程师妹子养成手记——数据库篇
    一行js代码识别Selenium+Webdriver及其应对方案
  • 原文地址:https://www.cnblogs.com/forcheng/p/12913103.html
Copyright © 2011-2022 走看看