Skip to main content

ES6

CommonJS 和 ES Modules

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(静态编译)。
  • CommonJs 是单个值导出,ES6 Module 可以导出多个
  • CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  • CommonJs 的 this 是当前模块,ES6 Module 的 this 是 undefined
  • CommonJS 是服务器端模块的规范,CommonJS 规范加载模块是同步的

CommonJSES Modules (ESM) 是两种 JavaScript 中的模块化系统,它们的主要区别在于模块的定义方式、加载机制、语法和使用场景。下面从多个维度来对比这两者的区别:

1. 模块定义方式和语法

  • CommonJS:使用 require 语法引入模块,用 module.exportsexports 导出模块。CommonJS 是模块系统的早期实现,主要用于 Node.js 服务器端开发。

    // 导出(CommonJS)
    module.exports = {
    greet: function () {
    console.log("Hello World")
    },
    }
    // 导入(CommonJS)
    const myModule = require("./myModule")
    myModule.greet() // 输出:Hello World
  • ES Modules (ESM):使用 importexport 语法导入和导出模块。ESM 是 JavaScript 官方标准化的模块系统,原生支持浏览器和 Node.js。

    // 导出(ES Modules)
    export function greet() {
    console.log("Hello World")
    }

    // 导入(ES Modules)
    import { greet } from "./myModule.js"
    greet() // 输出:Hello World

2. 加载机制

  • CommonJS同步加载模块。这意味着在 require 调用时,Node.js 会立即执行并返回模块的内容,这种同步特性使得它非常适合在服务器端使用,因为服务器端代码不会受到网络延迟等因素的影响。
    • CommonJS 的模块在首次加载时会被执行,结果会被缓存,后续的 require 调用会直接返回缓存的结果,而不会重复执行模块代码。
  • ESM异步加载模块。ESM 的 import 语句是基于 Promise 的,这使得模块加载可以是异步的,尤其适用于浏览器端,避免阻塞页面的加载。
    • ESM 在加载时会进行静态分析,因此在编译阶段就能确定模块依赖关系,这样可以实现更好的优化和提前报错。

3. 运行时与编译时

  • CommonJS:是运行时模块解析系统。require 语句会在代码运行时被解释执行,依赖可以根据运行时的条件动态加载。

    if (someCondition) {
    const moduleA = require("./moduleA")
    } else {
    const moduleB = require("./moduleB")
    }
  • ESM:是编译时模块系统,导入模块的声明必须位于代码的顶层,不能在条件语句或函数中动态引入。这是为了让编译器能在编译时确定模块的依赖关系。

    // 以下是非法的,在 ESM 中不能使用动态导入
    if (someCondition) {
    import { funcA } from "./moduleA.js" // 错误
    }

4. 导出机制

  • CommonJS:通过 module.exportsexports 导出对象、函数或变量。它可以导出整个对象或模块的不同部分。

    // 导出单个对象或函数
    module.exports = function greet() {
    console.log("Hello")
    }

    // 导出多个值
    exports.greet = function () {
    console.log("Hello")
    }
  • ESM:使用 export 关键字导出,可以进行命名导出默认导出。命名导出允许导出多个变量或函数,而默认导出则只能导出一个默认值。

    // 命名导出
    export const name = "John"
    export function greet() {
    console.log("Hello")
    }

    // 默认导出
    export default function () {
    console.log("Default Export")
    }

5. 模块的缓存

  • CommonJS:模块在第一次加载后会被缓存,后续对该模块的 require 调用会返回缓存的结果,而不是重新执行模块代码。

    // 第一次 require 模块时,模块代码会执行
    const moduleA = require("./moduleA")

    // 第二次 require 相同模块时,返回的是缓存的结果
    const moduleA_again = require("./moduleA")
  • ESM:同样会对模块进行缓存,首次加载后,模块会被缓存并复用。和 CommonJS 一样,模块只会在第一次加载时执行代码。

6. 顶层作用域

  • CommonJS:每个模块都有自己的独立作用域,而不是全局作用域。模块内部的变量和函数默认是私有的,只有通过 module.exportsexports 公开的部分才对外可见。
  • ESM:同样具有模块作用域,默认情况下,模块内部的变量、函数等都是私有的,只有通过 export 导出的部分才对外可见。

7. 模块循环依赖

  • CommonJS:允许并且能够处理循环依赖问题,但在循环依赖的情况下,只有模块的部分代码会被导入(即已执行的部分)。

    // moduleA.js
    const moduleB = require("./moduleB")
    console.log("Module A")

    // moduleB.js
    const moduleA = require("./moduleA")
    console.log("Module B")
  • ESM:也允许循环依赖,但 ESM 会确保模块的所有依赖关系都被初始化,在加载时会执行一部分已解析的模块代码。因此 ESM 更适合处理循环依赖。

8. 浏览器支持

  • CommonJS:主要用于 Node.js 环境,浏览器原生不支持 CommonJS 模块化系统。在浏览器中使用 CommonJS 通常需要通过打包工具(如 Webpack 或 Browserify)将模块转换成浏览器可执行的代码。

  • ESM:浏览器原生支持 ES Modules,尤其是在现代浏览器中,使用 <script type="module"> 标签可以直接加载 ES 模块,无需额外的打包工具。

    <script type="module" src="app.js"></script>

9. 使用场景

  • CommonJS:由于它是 Node.js 的标准模块系统,主要用于服务器端开发。
  • ESM:逐渐成为 JavaScript 的标准模块系统,被广泛应用于浏览器和 Node.js 环境中。

10. 默认导出与命名导出

  • CommonJS:没有默认导出的概念,模块可以导出单个对象或多个属性,但没有专门的语法区分默认导出和命名导出。
  • ESM:支持默认导出命名导出。一个模块可以有多个命名导出,但只能有一个默认导出。

11 值得引用和拷贝

  • CommonJS 模块输出的是值的拷贝:当使用 CommonJS require 引入一个模块时,模块中的值会被拷贝到引入模块的地方。也就是说,模块的导出值是在模块首次加载时计算并返回的,而后续对该模块的 require 调用都会返回相同的拷贝。如果模块内部的值发生变化,这种变化不会影响到已经引入的拷贝

  • ES6 模块输出的是值的引用:当使用 ES Modules (import/export) 引入一个模块时,模块中的导出值是引用,即导出的是一个“绑定”,不会被拷贝。如果模块内部的值发生变化,在其他模块中通过 import 导入的值也会随之更新,因为它们引用的是同一个内存地址。

1. CommonJS 模块输出的是值的拷贝

CommonJS 的模块机制是运行时加载,模块在首次被 require 时会执行整个模块代码,并缓存导出的结果。后续的 require 操作不会重新执行模块代码,而是直接使用缓存的拷贝。

// counter.js (CommonJS 模块)
let count = 0

function increment() {
count++
}

module.exports = {
count,
increment,
}
// main.js (CommonJS 导入)
const counter = require("./counter")

console.log(counter.count) // 输出 0
counter.increment()
console.log(counter.count) // 输出 0 (模块导出的 count 是拷贝,值不会更新)

在这个例子中,counter.js 模块导出的 count 值在 require 时就被固定下来。即使 counter.increment() 修改了模块内部的 count,但由于 require 返回的是初始值的拷贝,所以 main.js 中的 count 不会更新。

2. ES Modules 输出的是值的引用

ES6 的模块机制是编译时加载,import 导入的是一个对原模块的引用,即使原模块中的变量发生变化,导入模块中的值也会实时更新。

// counter.js (ES Modules)
let count = 0

export function increment() {
count++
}

export { count }
// main.js (ES Modules 导入)
import { count, increment } from "./counter.js"

console.log(count) // 输出 0
increment()
console.log(count) // 输出 1 (模块导出的 count 是引用,值会更新)

在这个例子中,count 是通过 export 导出的,它是一个引用。当 increment() 被调用时,count 的值更新了,而 main.js 中的 count 也随之更新,因为它引用的是同一个内存地址。

总结

  • CommonJS 导出的是值的拷贝,模块首次加载时会执行并缓存,后续使用的都是这个缓存的结果,模块内部变量的变化不会影响到其他文件。
  • ES Modules 导出的是值的引用,模块导入时会动态引用模块的内部值,如果模块内部的值发生变化,导入的地方也会感知到这些变化。

所以,ES Modules 的 import 具有动态性,而 CommonJS 的 require 更像是静态的快照。

ES6 新增了哪些数据类型,说一下用法。

  1. let 和 const 变量:用于声明块级作用域的变量,分别用于声明可变和不可变的变量。
  2. 箭头函数:一种简洁的函数定义方式,可以用于创建匿名函数或具名函数,并且可以省略函数体的大括号和 return 关键字。
  3. 类(class):用于创建对象的模板,可以定义属性和方法,支持继承和多态等面向对象编程特性。
  4. Promise:一种用于异步编程的技术,可以更方便地处理异步操作的结果和错误。
  5. 模板字符串:一种用于创建字符串的方式,支持多行字符串和变量插值等功能。
  6. 解构赋值:一种从对象或数组中提取数据的方式,可以将多个变量同时赋值,也可以进行嵌套解构。
  7. 对象扩展符(...):一种用于将多个对象合并成一个对象的语法。
  8. Set 和 Map 数据结构:用于存储不重复的值和键值对的数据结构。
  9. Symbol 类型:一种用于创建唯一值的数据类型,可以用于对象的属性名或作为枚举值。

const let var 区别

varletconst
定义啥变量+常量变量常量
变量提升和初始化存在变量提升并初始化为undefined存在变量提升但不会初始化(导致暂时性死区)如果在初始化前使用,会导致报错
作用域全局(挂载在window上)块级作用域内
重复声明可以不可以不可以(初始化必须赋值)
值是否可变可以,const声明的变量不能再次赋值,但是值可以变

变量提升?

  • 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部

暂时性死区

Set、Map、WeakSet 、 WeakMap

Set和 Map

  1. Set 存储的是值的集合,而 Map 存储的是键值对的集合。
  2. Set 中的值是唯一的,而 Map 中的键是唯一的。
  3. Set 中可以使用 add() 方法添加元素,而 Map 中可以使用 set() 方法添加键值对。
  4. Set 可以使用 has() 方法检查值是否存在,而 Map 可以使用 has() 方法检查键是否存在。
  5. Set 可以使用 delete() 方法删除值,而 Map 可以使用 delete() 方法删除键值对。

Set 和 Map 都可以使用迭代器进行遍历。对于 Set,可以使用 for...of 循环,也可以使用 forEach() 方法;对于 Map,可以使用 for...of 循环,也可以使用 forEach() 方法,或者使用 entries() 方法获取键值对的迭代器,然后使用 for...of 循环遍历。例如:

// Set 遍历
const mySet = new Set([1, 2, 3]);
for (const value of mySet) {
console.log(value);
}
mySet.forEach((value) => {
console.log(value);
});

// Map 遍历
const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const [key, value] of myMap) {
console.log(key, value);
}
myMap.forEach((value, key) => {
console.log(key, value);
});
for (const entry of myMap.entries()) {
console.log(entry[0], entry[1]);
}

WeakSet 和 Set 的区别:

  • WeakSet 的成员只能是对象,而不能是其他类型的值;
  • WeakSet 对象中的对象是弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的引用,如果没有其他的变量或属性引用该对象,则该对象会被垃圾回收掉,这时 WeakSet 中也会自动删除对应的引用;
  • WeakSet 没有 size 属性,也不能够被遍历。

WeakMap 和 Map 的区别:

  • WeakMap 的键只能是对象,而不能是其他类型的值;
  • WeakMap 对象中的对象是弱引用的,与 WeakSet 类似;
  • WeakMap 没有 size 属性,也不能够被遍历;
  • WeakMap 的键所对应的对象可以被垃圾回收机制回收,此时 WeakMap 中对应的键值对也会被自动删除。

因为 WeakSet 和 WeakMap 对象中的对象是弱引用的,所以在实际开发中,它们主要用于临时存储一些对象,并且不会影响垃圾回收机制的正常运行,避免内存泄漏等问题。

模块化

什么是模块化

  • 模块划就是按照一定的规则把代码封装成若干的相互依赖的文件并进行组合
  • 每个模块内的数据都是私有的,只向外选择性的暴露一些方法和数据与外界进行数据通信

模块化的意义

  • 有利于代码分享、解耦以及复用
  • 团队并行开发
  • 避免命名冲突
  • 相互引用,按需加载

模块化的发展史

  • 自执行函数
  • AMD (Asynchronous Module Definition)
    • AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块
    • AMD规范则是非同步加载模块,需要定义回调define方式
  • CMD (Common Module Definition)
    • CMD 推崇就近依赖,只有在用到某个模块的时候再去 require
  • CommonJS (服务器端开发)
  • UMD (Universal Module Definition)
    • UMD 叫做通用模块定义规范(Universal Module Definition)可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行
  • ES6 Module (ESM,JS 官方标准模块定义方式)

common.js 和 es6 中模块引入的区别

目前浏览器端虽写法是以 esm 为主,但是各种前端工具转换为 cjs

在使用上的差别主要有:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(静态编译)。
  • CommonJs 是单个值导出,ES6 Module 可以导出多个
  • CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  • CommonJsthis 是当前模块,ES6 Modulethisundefined
  • CommonJS是服务器端模块的规范,CommonJS规范加载模块是同步的

注意:

  • export {<变量>}这种方式一般称为 命名式导出 或者 具名导出,导出的是一个变量的引用
  • export default 这种方式称为 默认导出 或者 匿名导出,导出的是一个值。