你不知道的 __esModule 和 Symbol.toStringTag

在研究 Vue3 源码时(3.2.45 版本)看到以下代码:

ts 复制代码
// packages/runtime-core/src/apiAsyncComponent.ts

// interop module default
if (
  comp &&
  (comp.__esModule || comp[Symbol.toStringTag] === 'Module')
) {
  comp = comp.default
}

__esModule 属性是 ES 模块的标识,如果 __esModule 属性为 true ,则说明该模块是 ES 模块。

如果模块的 Symbol.toStringTag 属性值为 Module ,则该模块也是 ES 模块。

在日常开发中,可以通过判断 __esModule 是否为 true ,Symbol.toStringTag 属性值是否为 Module 来判断一个模块是否为 ES 模块。

html 复制代码
<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module" src="main.js"></script>
</body>
</html>
js 复制代码
// b.js

export const name ='Tom'
export default function sayHello(){
  console.log('hello, world')
}
js 复制代码
// main.js

setTimeout(() => {
  const result  = import('./b.js')
  result.then(res=>{
    console.log(res)
  })
}, 0);

在浏览器中运行 main.js 模块的代码

可以看到, ES 模块对象会有个值为 ModuleSymbol.toStringTag 属性。我们可以将该属性作为 ES 模块的标识。

关于 Symbol.toStringTag 属性的认识

Symbol.toStringTag 属性可用于自定义对象的字符串描述。

所谓对象的字符串描述,就是对象调用 Object.prototype.toString() 方法时返回的字符串,例如:

js 复制代码
const map = new Map()
Object.prototype.toString.call(map) // [object Map]

Object.prototype.toString() 返回 "[object Type]",这里的 Type 是对象的类型。如果对象有 Symbol.toStringTag 属性,其值是一个字符串,则它的值将被用作 Type

js 复制代码
const map = new Map()
map[Symbol.toStringTag] // Map

不过一些早于 ES6 的对象没有 Symbol.toStringTag 属性,例如:

js 复制代码
const arr = [5, 6, 7]
arr[Symbol.toStringTag] // undefined

但是没有 Symbol.toStringTag 属性的对象也是能通过 Object.prototype.toString() 方法获取该对象的字符串描述的

js 复制代码
const arr = [5, 6, 7]
Object.prototype.toString.call(arr) // [object Array]

对于自定义的对象,开发者可通过 Symbol.toStringTag 属性自定义对象的字符串描述

js 复制代码
class XiaoMing {
  get [Symbol.toStringTag]() {
    return '小明';
  }
}

const xiaoMing = new XiaoMing()

console.log(Object.prototype.toString.call(xiaoMing));
// Expected output: "[object 小明]"
console.log(xiaoMing[Symbol.toStringTag]);
// Expected output: "小明"

关于 __esModule 属性的认识

__esModule 属性是 ES 模块的标识。

当使用打包工具,比如 Babel 、Webpack 或者使用 TypeScript 的编译器 tsc 将 ES 模块的代码转换成 CommonJS 模块的代码时,都在导出的模块对象(exports)打上 __esModule 为 true 的标记。

个人推测,这是为了让打包工具或编译器知道生成的代码的源码是 ES 模块的。

让我们看看 Babel 的例子,该例子的整体结构为:

Babel 的配置如下:

js 复制代码
// babel.config.js
const presets = [
  [
    "@babel/preset-env",
    {
      targets: {
        edge: "17",
        firefox: "60",
        chrome: "67",
        safari: "11.1",
      },
      useBuiltIns: "usage",
      corejs: "3.6.4",
      modules: "cjs",
    },
  ],
];

module.exports = { presets };

整个 Babel 例子的依赖为:

在 src 文件夹下创建 add.js 、index.js 文件:

js 复制代码
// src/add.js
function add(a, b) {
  return a + b;
}

export default add
js 复制代码
// src/index.js

import add from "./add.js";

const total = add(2, 3);

console.log(total);

add.js 、index.js 都是 ES 模块的,我们打算使用 Babel 将他们都转成 CommonJS 的。

在 package.json 的 scripts 创建 npm scripts 命令:

json 复制代码
{
  "scripts": {
    "babel": "babel src --out-dir lib"
  }
}

在 npm scripts 下编写的命令,会自动在 node_modules 下的 .bin 目录下寻找。 上面配置中的 babel src --out-dir lib ,相当于是 ./node_modules/.bin/babel src --out-dir lib

然后在终端 npm run babel 命令,得到代码转换后的结果:

js 复制代码
// lib/add.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;
js 复制代码
// lib/index.js

"use strict";

var _add = _interopRequireDefault(require("./add.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const total = (0, _add.default)(2, 3);
console.log(total);

lib/add.js 中可以看到,导出的对象 exports 被打上了 __esModule 为 true 的标记。

我们再来看看 Webpack 的例子,整个 Webpack 例子的项目结构如下:

对应的 package.json 文件配置为:

json 复制代码
{
  "private": "true",
  "scripts": {
    "webpack": "webpack --config webpack.config.js"
  },
  "dependencies": {
    "webpack": "^5.92.0",
    "webpack-cli": "^5.1.4"
  }
}

在 src 文件夹下创建 add.jsindex.js 文件,代码分别为:

js 复制代码
// src/add.js

function add(a, b) {
  return a + b;
}

export default add
js 复制代码
// src/index.js

import add from "./add.js";

const total = add(2, 3);

console.log(total);

在终端运行 npm run webpack 命令,可得代码打包后的结果为(删去了大部分注释):

js 复制代码
// dist/main.js

(() => {
  "use strict";
  var __webpack_modules__ = {
    "./src/add.js": (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__,
      });
      function add(a, b) {
        return a + b;
      }

      const __WEBPACK_DEFAULT_EXPORT__ = add;
    },
  };
  // The module cache
  var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = (__webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {},
    });

    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // Return the exports of the module
    return module.exports;
  }

  (() => {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          });
        }
      }
    };
  })();

  (() => {
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  (() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
      // 重点!!!
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      // 重点!!!
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })();

  var __webpack_exports__ = {};
  __webpack_require__.r(__webpack_exports__);
  var _add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
    /*! ./add.js */ "./src/add.js"
  );

  const total = (0, _add_js__WEBPACK_IMPORTED_MODULE_0__["default"])(2, 3);

  console.log(total);
})();

上面的转换结果中出现了与本文主题相关的两个属性 Symbol.toStringTag__esModule

其实这两个属性的目的都是为转换后的代码打上 ES 模块的标记,用于告诉 Webpack 等其他打包工具,该打包后的代码源码为 ES 模块。

最后再看看 TypeScript 的例子,在 Typescript 中,将 ES 模块的代码编译成 CommonJS 的,也会在输出的代码结果上打上 __esModule 为 true 的标记。

TypeScript 例子的整体目录结构如下:

相关的编译配置为(tsconfig.json):

json 复制代码
{
  "compilerOptions": {
    "module": "commonjs",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "sourceMap": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
}

package.json 配置如下:

json 复制代码
{
  "private": "true",
  "scripts": {
    "tsc": "tsc"
  },  
  "devDependencies": {
    "typescript": "^5.4.5"
  }
}

然后在 src 下创建 add.tsindex.ts 文件,代码分别为:

ts 复制代码
// src/add.ts

function add(a:number, b:number) {
  return a + b;
}

export default add
ts 复制代码
// src/index.ts

import add from "./add.js";

const total = add(2, 3);

console.log(total);

在终端运行 npm run tsc 命令,可得代码转换的结果为:

js 复制代码
// dist/add.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function add(a, b) {
  return a + b;
}
exports.default = add;
js 复制代码
// dist/index.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var add_js_1 = require("./add.js");
var total = (0, add_js_1.default)(2, 3);
console.log(total);

可以看到 TypeScript 编译器也会把 ES 模块的代码编译成 CommonJS 后,在编译后的结果代码中,将导出的 exports 对象打上 __esModule 为 true 的标记。

读源码的一些感想

一开始笔者并不知道下面两行代码中 __esModuleSymbol.toStringTag 属性是做什么用的,只是直觉告诉我可能跟 ES 模块有关系。

ts 复制代码
// packages/runtime-core/src/apiAsyncComponent.ts

// interop module default
if (
  comp &&
  (comp.__esModule || comp[Symbol.toStringTag] === 'Module')
) {
  comp = comp.default
}

然后为了弄懂这两行代码,查阅了许多的资料,便输出了以下文章:

  1. 认识 CommonJS 模块化规范

  2. 掌握 JS 模块化开发:细说 ES Module 模块化规范

  3. 什么,你还不知道 CommonJS 模块与 ES 模块的区别?

  4. JS 模块是如何处理模块的循环加载的?

  5. JS 模块进阶:ES 模块与 CommonJS 模块的互相引用

  6. 搭建 JS 模块知识体系,掌握 JS 模块化开发

因此,在阅读优秀框架源码的过程中,通过深挖一个点,可以学到很多相关知识,最后很多知识点都可以通过一条线连接起来,所以掘友们,现在就开始看源码吧!

知识点总结

Symbol.toStringTag 属性用于自定义对象的字符串描述。在浏览器中,可判断对像的 Symbol.toStringTag 属性是否为 Module 来判断该模块是否为 ES 模块。

__esModule 是 ES 模块的标识,在一些打包工具或编译器中,比如:Babel 、Webpack 、Typescript 的编译器 tsc ,会将 ES 模块转成 CommonJS 模块后,在导出的模块对象 exports 打上 __esModule 为 true 的标识。

js 复制代码
Object.defineProperty(exports, "__esModule", { value: true });

综上所述,在日常的开发中,可借助判断 __esModule 是否为 true 或 Symbol.toStringTag 是否为 Module 判断当前模块是否为 ES 模块。

参考

Object.prototype.toString()

Symbol.toStringTag

相关推荐
IT女孩儿43 分钟前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
天天进步20152 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz2 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇2 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒2 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端
程序猿阿伟3 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒3 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript