Better

Ethan的博客,欢迎访问交流

ES6 Class 与 Module

这应该是篇欠了很久的文章,当时 code review 同事代码的时候,发现同事在类中的构造函数,在某个分支使用了 return 提前返回,且不说这种方式优不优雅的问题,从语法上,这种方式会存在问题吗,个人觉得看上还是有点怪怪的,而且之前模拟 new 的实现的时候,如果调用 return 返回对象,会导致创建的结果为 return 返回的对象。最近在学习设计模式的时候,对于对象构造函数的执行时机有点好奇,于是就想起来了,具体看文章吧

ES6 Class

先回答一开始提到的问题,构造函数中提前返回会怎么样。我们都知道 class 的实现只是原型链的语法糖,所以提前返回对象应该是会不符合预期的。但需要注意的是,提前返回对象和提前返回还是有所差别的。

具体看下面的例子

export default class User {
  public name: string;
  public age: number;

  constructor(name: string, flag = false) {
    this.name = name;
    if (!flag) {
        return;
    }
    this.age = 20;
  }
}

上面的例子中,会成功创建出 User 答案吗,答案是可以的。因为 return 返回的是 undefined,并不是返回一个对象,改成 return {},则会导致对象不是正确 User。

再来看一个问题,在类中,我们既可以在声明类属性的时候直接赋值,也可以在构造函数的时候进行赋值,那这两种方式有什么区别呢?执行的先后顺序又是?

首先这两种方式创建出来的属性是没有区别的,构造函数用来传参创建,对于不依赖参数的,直接赋值也无妨,还能精简构造函数代码量。那执行实际呢。看下面的例子

function getDefaultAge() {
  console.log("execute default generator");
  return 20;
}

export default class User {
  public age = getDefaultAge();
  public name: string;

  constructor(name: string) {
    console.log("execute constructor");
    this.name = name;
  }
}
// 结果为
// execute default generator
// execute constructor

由此可见,对于直接赋值,引擎会直接进行处理。

扩展:顺便看看 Java 语言会有差别吗,而且 Java 还有 static block 的语法,看下面的代码

public class User {
    public static String ID;
    public String name;
    public int age = generate();

    public static int generate() {
        System.out.println("execute default generator");
        return 20;
    }

    static {
        System.out.println("static block execute");
        ID = "ID";
    }

    public User(String name) {
        System.out.println("execute constructor");
        this.name = name;
    }
}
// 结果为
// static block execute
// execute default generator
// execute constructor

在 Java 中,static 块中只能访问 static 级别的属性,static 最先执行,后面的步骤和 JS 一样。

ES6 Module

在学习 ES6 Module 之前,先了解下传统的方式

  • script 标签加载脚本
    • defer:渲染完再执行,多个 defer 脚本,会按照它们在页面出现的顺序加载
    • async:下载完就执行,多个 async 脚本是不能保证加载顺序的
  • 加载 ES6 模块,依旧使用 script 标签,添加 type="module" 属性
    • 代码是在模块作用域中运行,而不是全局作用域运行
    • 自动采用严格模式
    • 模块中可以通过 import 加载其他模块,通过 export 命令输出对外接口
    • 同一模块如果加载多次,将只执行一次

ES6 模块与 CommonJS 模块的差异

  • CommonJS 输出的是一个值的拷贝,ES6 模块输出的是值的引用
    • CommonJS 一旦输出一个值,模块内部的变化就影响不到这个值
    • ES6 中引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的模块中取值
    • ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错
    • ES6 中 export 通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
    • CommonJS 加载的是一个对象,module.exports
    • ES6 模块不是对象,对外接口只是一种静态定义,在代码解析阶段就会生成
  • CommonJS 模块的 require 是同步加载模块,ES6 模块的 import 命令是异步加载,有独立的模块依赖的解析阶段

Node.js 的模块加载方法

  • JS 现在有两种模块
    • ES6 模块,简称 ESM
    • CommonJS 模块,简称 CJS
  • 背景:从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持
    • .mjs 文件总是以 ES6 模块加载
    • .cjs 文件总是以 CommonJS 模块加载
    • .js 文件的加载取决于 package.json 里面 type 字段的设置
  • package.json 文件有两个字段可以指定模块的入口文件:mainexports,具体细节就不扯了
  • ES6 模块的 import 命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项

循环加载的处理

  • CommonJS 模块加载原理
    • require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象,大概有 id、exports、loaded 属性
    • CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  • CommonJS 循环加载
    • CommonJS 是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
    • CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。
  • ES6 循环加载
    • ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

webpack 模块化原理

在 webpack 打包的工程中,我们能直接编写 CommonJS 或 ES6 module 的代码,经过 webpack 构建后,就可以直接在浏览器中运行了。那 webpack 是如何做到的呢。我们其实可以从生成产物来进行分析

一下内容来自博客,webpack模块化原理-同步加载模块,自己暂时没有验证,不保证对错哈

同步加载 CommonJS 大致结果

  • 定义 installedModules,用来缓存已经加载过的模块
  • 定义 __webpack_require__ 用来同步加载模块
  • 使用 __webpack_require__ 加载入口,入口有新的依赖,则依旧通过 __webpack_require__ 调用

__webpack_require__ 执行流程

  • 通过 InstallledModuled 判断模块是否已缓存,如果已经缓存,则直接返回缓存模块的 exports 属性
  • 没有缓存的话,初始化一个新的模块对象,加入缓存
  • 执行模块函数,就会将模块标记为已加载,最后返回 module.exports

同步加载 ES6 module

  • 新增三个函数
    • __webpack_require__.o:Object.hasOwnProperty 的包裹函数。
    • __webpack_require__.d:为传入的 exports 对象定义一个新的属性,这个新的属性是可枚举的,并且使用 getter 方法来获取属性值。
    • __webpack_require__.r:为传入的 exports 对象定义一个 __esModule 属性,值为 true,标识这是一个 ES6 module。
  • 模块的区别
    • 入参名称由 exports 改成了 __webpack_exports__
    • 自动加上了 "use strict",使用严格模式
    • 调用 __webpack_require__.r,标识这是一个 ES6 module
    • 将 import 转换成了 __webpack_require____webpack_require__ 会返回一个 exports 对象,然后执行挂载在 exports 对象上的方法


留言