Skip to content

JavaScript模块化

1. 视频地址

本笔记由以下视频整理而来,感谢禹神的分享🌹

2. 模块化概述

2.1 什么是模块化

  • 将程序文件依据一定规则拆分成多个文件,这种编程方式就是模块化;
  • 拆分出来的每个文件就是一个模块,模块中的数据都是私有的,模块之间互相隔离;
  • 同时也能通过一些方法,将模块内的指定数据、方法“交出去”,供其他模块使用;

2.2 为什么需要模块化

随着应用的复杂度越来越高,代码量和文件数量都会急剧增加,会逐渐引发以下问题:

  1. 全局污染问题;

    如果不同的JavaScript文件中定义了同名的方法或变量,那么会导致先导入的文件被覆盖的问题。

    image-20241121203351050

    image-20241121203420958

  2. 依赖混乱问题;

    例如,在如下示例中,JavaScript文件要按照指定的顺序导入,否则会出问题。

    image-20241121203632114

  3. 数据安全问题;

    例如,在以下代码中,我们只想暴露用户的姓名和性别信息,但是如果将整个JavaScript文件导入,则会暴露整个用户信息。

    image-20241121204036075

    image-20241121204131037

2.3 模块化规范

随着时间的推移,针对JavaScript的不同运行环境,相继出现了多种模块化规范,按时间排序,分别为:

  1. CommonJS——服务端应用广泛
  2. AMD——略过
  3. CMD——略过
  4. ES6 模块化——浏览器端应用广泛

2.4 导入与导出的概念

模块化的核心思想就是:模块之间是隔离的,通过导出和导入进行数据和功能的共享。

  • 导出:模块公开其内部的一部分(如变量、函数等),使这些内容可以被其他模块使用。
  • 导入:模块引入和使用其他模块导出的内容,以重用代码和功能。

image-20241121204600223

3. CommonJS 模块化

CommonJS最初名为ServerJS,是于2009年由Kevin Dangoor提出的,用于非浏览器环境的模块化规范,

CommonJS 规范-维基百科

3.1 CommonJS导出

在CommonJS中,我们可以通过设置module.exports来声明导出,例如在student.js文件中:

js
let name = '张三'
let age = 18

function getTel() {
    return '12345678'
}

function getAddress() {
    return '北京市'
}

module.exports = {
    name,
    age,
    getTel,
}

我们也可以通过如下方式声明导出:

js
exports.name = name
exports.age = age
exports.getTel = getTel

但是,我们不能通过以下方式来声明导出:

js
exports = {
    name,
    age,
    getTel,
}

原因如下:

  • 模块内部的thisexportsmodule.exports在初识时,都指向同一个对象,该空对象就是当前模块导出的数据,如下图:

    image-20241123151055648

  • 无论如何修改导出对象,最终导出的都是module.exports的值。

  • exports是对module.exports的初始饮用,仅为了方便给导出对象添加属性,所以不能使用exports=value的形式导出数据,但是可以使用module.exports=value导出数据。

3.2 CommonJS导入

在CommonJS中,我们可以使用关键字require来导入其他模块,例如,在index.js中:

js
const student = require('./student.js')
console.log(student)

输出结果如下:

txt
{ name: '张三', age: 18, getTel: [Function: getTel] }

3.3 扩展理解

一个JS模块在执行时,是被包裹在一个内置函数中执行的,所以每个模块都有自己的作用域,我们可以通过如下方式验证这一说法:

js
console.log(arguments)
console.log(arguments.callee.toString())

内置函数如下:

js
function (exports, require, module, __filename, __dirname) {
  let name = '张三'
  let age = 18

  function getTel() {
      return '12345678'
  }

  function getAddress() {
      return '北京市'
  }

  module.exports = {
      name,
      age,
      getTel,
  }

  console.log(arguments)
  console.log(arguments.callee.toString())
}

可以看到,内置函数的参数中有exportsmodule,所以我们可以直接使用exportsmodule

3.4 在浏览器端使用

环境准备student.js

js
let name = '张三'
let age = 18

function getTel() {
    return '12345678'
}

function getAddress() {
    return '北京市'
}

module.exports = {
    name,
    age,
    getTel,
}

index.js

js
const student = require('./student.js')
console.log(student)

index.html

html
<body>
    <script src="./index.js"></script>
</body>

打开控制台,发现报错:

txt
Uncaught ReferenceError: require is not defined
    at index.js:1:17

这是由于CommonJS在设计之初,是不支持浏览器环境使用的。如果我们想在浏览器环境使用CommonJS,需要将JS代码编译为浏览器可以识别的,需要使用browserify

  • 安装browserifynpm i browserify
  • 使用browserify编译需要引入浏览器的脚本文件:npx browserify index.js -o build.js
  • 在网页中引入编译后的文件:<script src="./build.js"></script>

之后在浏览器的控制台中就能看到结果啦~

4. ES6 模块化规范

ES6模块化规范是一个官方标准的规范,它是在语言标准的层面上实现了模块化功能,是目前最流行的模块化规范,且浏览器与服务端均支持该规范。

4.1 ES6模块化初体验

首先准备student.js

js
export const name = 'jack'
export const sex = 'male'

function greet() {
    console.log('hello');
}

export function sum(num1, num2) {
    return num1 + num2
}

注意,在namesexsum的声明前,我们都加关键字export,表示导出。

然后新建index.js

js
import * as student from "./student.js";
console.log(student)

我们使用import关键词来导入模块。

如果此时我们直接在Node环境中运行index.js,是会报错的:

报错信息

SyntaxError: Cannot use import statement outside a module

所以我们将其引入网页,创建index.html

html
<script type="module" src="./index.js"></script>

注意,我们使用module关键词来标注脚本文件为模块。

之后,在浏览器的控制台中,就能看到引入的内容啦。

如果我们想在Node环境中直接运行index.js文件,需要怎么做呢?其实,从报错信息中我们已经得到了解决方法:

在Node环境中使用ES6模块化

To load an ES module, set "type": "module" in the package.json or use the .mjs extension.

  • 方式一:在package.json文件中加入配置type:"module"
  • 方式二:将js后缀名改为mjs

在之后的内容中,我们统一在Node环境中进行导出与导入的示范。

4.2 ES6导出

在ES6中,有如下导出方式:

  • 分别导出:在正常声明的前面加上关键字export,表示导出该声明,例如:

    js
    export const name = 'jack'
    export const sex = 'male'
    
    function greet() {
        console.log('hello');
    }
    
    export function sum(num1, num2) {
        return num1 + num2
    }

    分别导出结果

    [Module: null prototype] {

    name: 'jack',

    sex: 'male',

    sum: [Function: sum]

    }

  • 统一导出:在模块声明最后使用关键字export指定导出的内容,如下:

    js
    const name = 'jack'
    const sex = 'male'
    
    function greet() {
        console.log('hello');
    }
    
    function sum(num1, num2) {
        return num1 + num2
    }
    
    export { name, sex, sum }

    注意,export后面的内容不是对象,所以{name}不是{name:name}的简写形式,后者会报错。

    统一导出结果

    [Module: null prototype] {

    name: 'jack',

    sex: 'male',

    sum: [Function: sum]

    }

  • 默认导出:在模块声明最后使用关键字export default指定导出的内容,如下:

    js
    const name = 'jack'
    const sex = 'male'
    
    function greet() {
        console.log('hello');
    }
    
    function sum(num1, num2) {
        return num1 + num2
    }
    
    export default { name, sex, sum }

    注意:export default导出的内容是一个对象,所以{name}不是{name:name}的简写形式。

    txt
    默认导出结果
    [Module: null prototype] {
     default: { name: 'jack', sex: 'male', sum: [Function: sum] }
    }

4.3 ES6导入

对于ES6模块化来说,使用何种导入方式,要根据导出方式决定:

  • 导入全部:可用于所有导出方式

    js
    import * as student from './student.js'
  • 命名导入:对应于分别导出和统一导出方式

    js
    import {name,sex,sum} from './student.js'

    我们可以使用as给导入的内容取别名:

    js
    import {name as studentName,sex,sum} from './student.js'
  • 默认导入:对应于默认导出方式

    js
    import xxx from './student.js'

    xxx可以是任意有效标识符,表示默认导出的对象。

  • 命名导入和默认导入混合使用

    例如,现在有student.js使用了分别导出、统一导出和默认导出:

    js
    export const name = 'jack' // 分别导出
    const sex = 'male'
    
    function greet() {
        console.log('hello');
    }
    
    function sum(num1, num2) {
        return num1 + num2
    }
    
    export { sex } // 统一导出
    
    export default { sum } // 默认导出

    然后我们可以使用命名导入和默认导入:

    js
    import sumClass, { name as studentName, sex } from './student.js'
    console.log(sumClass.sum(1, 2))
    console.log(studentName)
    console.log(sex)

    结果如下:

    txt
    3
    jack
    male
  • 动态导入:我们可以在网页中使用import()动态导入脚本文件,然后执行相应逻辑。

    首先准备random.js文件:

    js
    export function printRandomInternal() {
        console.log(Math.random())
    }

    然后准备index.html文件:

    html
    <body>
        <button onclick="printRandom()">动态导入模块,输出随机数</button>
    
        <script>
            function printRandom() {
                import('./random.js').then(module => {
                    module.printRandomInternal()
                })
            }
        </script>
    </body>

    当我们点击按钮时,会动态导入random.js文件并执行其中的方法。

  • 只导入不接受数据:我们也可以只导入,不接受数据

    js
    import "./student.js"

    使用这种方式,表示在导入时执行student.js文件中的代码。

4.4 数据引用问题

4.4.1 示例一:普通方法

js
function demo() {
    let sum = 1

    function increment() {
        sum++
    }

    return {
        sum,
        increment
    }
}

const { sum, increment } = demo()

console.log(sum)
increment()
increment()
console.log(sum);

输出结果

1

1

4.4.2 示例二:使用CommonJS

js
let sum = 1

function increment() {
    sum++
}

module.exports = { sum, increment }
js
const { sum, increment } = require('./02_demo.js')

console.log(sum);
increment()
increment()
console.log(sum);

输出结果

1

1

4.4.3 示例三:使用ES6

js
let sum = 1
function increment() {
    sum++
}

export {
    sum,
    increment
}
js
import { sum, increment } from "./03_demo.js"

console.log(sum);
increment()
increment()
console.log(sum);

输出结果

1

3

4.4.4 结论

  • ES6模块化规范中,导出和导入的东西使用同一块内存,所以在一个模块中修改同一个数据,会导致该数据在另一个模块中发生变化。

    而在CommonJS模块化规范中,数据是使用复制的方式进行导入的,所以在一个模块中修改同一个数据,不会导致该数据在龙一个模块发生变化。

  • ES6和CommonJS模块化规范中,导入的数据是常量,所以修改导入的数据会报错。