es5 实现继承,原来我一直没实现完整

[PS:如果觉得太啰嗦,直接跳转目录:es5 实现继承即可;](#PS:如果觉得太啰嗦,直接跳转目录:es5 实现继承即可; "#heading-0")

这不是快过年了,在家里闲的没事干,就一直在倒腾编译器,我一直好奇这个装饰器打包的时候怎么处理的,最后处理成什么样了,于是我写了一个类并加上了装饰器:

js 复制代码
export default class Super {
  @decorateMethod
  hello() {
    console.log("hello");
  }
}

function decorateMethod(target, key, descriptor) {
  console.log(target, key, descriptor);
}

同时创建了一个 js 文件,先读取源码然后进行 ast 解析,写到这里我就江郎才尽了,不知道对这个 ast 应该如何 tranform 得到 es5 的装饰器实现:

js 复制代码
const { parse } = require("@babel/core");
const traverse = require("@babel/traverse");
const generate = require("@babel/generator").default;
const fs = require("fs");
const path = require("path");

fs.readFile(
  path.join(__dirname, "./index.js"),
  {
    encoding: "utf-8",
  },
  (err, data) => {
    if (err) {
      console.log("error", err);
    } else {
      main(data);
    }
  }
);

function main(code) {
  const ast = parse(code, {
    plugins: [
      [
        "@babel/plugin-syntax-decorators",
        {
          decoratorsBeforeExport: false,
        },
      ],
    ],
  });

  traverse.default(ast, {
    ClassMethod(node) {
      const decorators = node.node.decorators;
      node.node.decorators = [];
      if (decorators.length > 0) {
        decorators.forEach((decorator) => {
          const methodName = decorator.expression.name;
          node.parent;
        });
      }
    },
  });

  const { code: newCode } = generate(ast);
  fs.writeFile(path.join(__dirname, "./newIndex.js"), newCode, (err) => {
    if (err) {
    } else {
      console.log("success");
    }
  });
}

既然不知道怎么写,那就直接抄答案,看看 rollup 打包之后的源代码,先安装father,新建一个文件命名为:.fatherrc.js,然后配置一下,看这确实是零配置:

js 复制代码
const { defineConfig } = require("father");

export default defineConfig({
  esm: {},
  cjs: {},
  umd: {},
  platform: "browser",
});

得到了源码:

js 复制代码
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
var _class;
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
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, _toPropertyKey(descriptor.key), descriptor);
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  Object.keys(descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  if ("value" in desc || desc.initializer) {
    desc.writable = true;
  }
  desc = decorators
    .slice()
    .reverse()
    .reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}
var Super =
  ((_class = /*#__PURE__*/ (function () {
    function Super() {
      _classCallCheck(this, Super);
    }
    _createClass(Super, [
      {
        key: "hello",
        value: function hello() {
          console.log("hello");
        },
      },
    ]);
    return Super;
  })()),
  _applyDecoratedDescriptor(
    _class.prototype,
    "hello",
    [decorateMethod],
    Object.getOwnPropertyDescriptor(_class.prototype, "hello"),
    _class.prototype
  ),
  _class);
export { Super as default };
function decorateMethod(target, key, descriptor) {
  console.log(target, key, descriptor);
}

看完源码恍然大悟,es5 实现装饰器只不过就是把这个装饰器函数执行了一下,那么其实 es6 的装饰器就类似于语法糖了;同样我在这段源码中还发现了类的实现;

es5 中的类一般是怎么写的?一般是定义一个构造函数,然后定义它的原型对象,如下:

js 复制代码
function Super(){

}

Super.prototype = {
}

封装一下得到这个方法:

js 复制代码
function _createClass(Constructor, protoProps = {}, staticProps = {}) {
  Object.keys(protoProps).forEach((key) => {
    Constructor.prototype[key] = protoProps[key];
  });

  Object.keys(staticProps).forEach((key) => {
    Constructor[key] = staticProps[key];
  });

  return Constructor;
}

function Super() {
  if (!(this instanceof Super)) {
    throw new Error("Super must be called with the new operator");
  } else {
    Super.instanceCount++;
  }
}

// 用 function 实现 class
_createClass(
  Super,
  {
    hello() {
      console.log("hello");
    },
  },
  {
    instanceCount: 0,
  }
);

再来看看别人实现的类:

js 复制代码
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, _toPropertyKey(descriptor.key), descriptor);
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}

这里只说区别

  1. 他是用 Object.defineProperty实现的给对象添加属性
  2. 它将 key 进行了Primitive转化

这里可以看出来别人实现的比我想的要周到的多,所以到这里我想到了继承,这里我先自己心里背一下八股文:class 与 es5 实现继承的区别,es5 实现继承的源码

es5实现继承

如果面试官恰好问了这一道题,大家应该都觉得是送分题,但是送分题就一定能够答好吗?答案是否定的。先来看看我心目中是怎么实现继承的:

js 复制代码
function $extends(Super) {
  function Child() {
    Super.call(this);
  }

  Child.prototype = Object.create(Super.prototype);
  Child.prototype.constructor = Child;

  return Child;
}

一直以来我觉得这样实现就是 100 分了,直到编译了一下,看看大佬是怎么实现这个继承的。在一开始的代码里面加上一个子类,继承 Super:

diff 复制代码
export default class Super {
  @decorateMethod
  hello() {
    console.log("hello");
  }
}

+ class Child extends Super {}

function decorateMethod(target, key, descriptor) {
  console.log(target, key, descriptor);
}

运行:father dev,先梳理一下大致的执行逻辑:

graph TD _inherits --> _createSuper--->_createClass--->_super.apply

_inherites做了三件事:第一是校验父类是否为函数,第二:设置构造函数的prototype(这一步我做了),第三:设置构造函数的__proto__(这一步我没做),这里需要画原型图理解一下,为什么要将父类设置为子类的原型呢?这是为了继承父类的静态属性和静态方法。

js 复制代码
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 },
  });
  Object.defineProperty(subClass, "prototype", { writable: false });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

_createSuper:用来创建父类函数,然后在子类中调用该函数,并返回它的返回值,这个时候就可以通过getPrototypeof(Child)获取到父类 Super,因为它们已经有了继承关系;

js 复制代码
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);
  };
}

_super.apply:这里就类似于上面的 Super.call(this)

那么自己到底哪些地方没有实现完整呢?

  1. 没有继承父类的静态属性和静态方法:解决方案就是将父类设置为子类的原型,需要判断是否支持setPrototypeOf,支持则直接使用该方法设置原型,不支持则使用__proto__,__proto__不是规范中规定的属性,所以不稳定,但是也没有其他办法来兼容setPrototypeOf
  2. 无法继承内置对象 上面的$extend方法传入 Date,这个时候 new 调用会发现返回的并不是 date 对象;为什么会出现这样的情况呢?因为 ES5 的继承根本无法实现内置对象的继承,怎么办呢?这里就用了一个讨巧的方式,既然调用子类不行,这里直接全部改为调用父类,然后返回其值,子类以及子类原型的属性和方法手动添加到这个对象上去,简单来说就是这样的:
js 复制代码
function Child(){
    // 子类不返回 new 新创建的对象而是直接返回父类执行后的对象
    return Super.apply(this,arguments)
}

当然这里如果支持Reflect.construct,那么就用Reflect.construct来生成实例对象:

js 复制代码
var NewTarget = _getPrototypeOf(this).constructor;  // Child
result = Reflect.construct(Super, arguments, NewTarget);

Reflect.construct()等同于 new 操作符,唯一不同的就是它的第三个参数可以指定 new.target对象;在构造函数中可以访问,new.target对象,它通常用来检测函数是否被 new 操作符调用

  1. 在执行父类时没有传参数:别看这个小细节,还是很容易漏掉的

思考题

最后留下一个思考题:不支持Reflect.construct的情况下,可以这样返回吗:result = new Super(...Array.from(arguments));

相关推荐
Hello-Mr.Wang4 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
WG_178 分钟前
C++多态
开发语言·c++·面试
鱼跃鹰飞28 分钟前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
程序员凡尘31 分钟前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
北岛寒沫5 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy5 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步6 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者6 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js