这应该是篇欠了很久的文章,当时 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
文件有两个字段可以指定模块的入口文件:main
和exports
,具体细节就不扯了- 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 对象上的方法
- 入参名称由 exports 改成了