模块化构建 - import & export
前言
在 javascript 发展的历史上一直没有模块化体系,因此无这对于开发大型、复杂项目造成了困难。 在 ES6 出现之前社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。直到 ES6 的出现才在语言标准的层面上实现了模块功能,简单易用,可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。 在JavaScript中,模块化可以通过多种方式实现,其中比较常见的有以下几种:
-
CommonJS:主要用于服务端模块化,主要代表 Node.js,在 Node.js 中使用的模块化规范,采用同步加载模块的方式,通过 require 和 module.exports 来实现模块的导入和导出。
-
AMD(Asynchronous Module Definition):异步模块定义规范,模块和模块的依赖可以被异步加载,他们的加载不会影响它后面语句的运行。有效避免了采用同步加载方式中导致的页面假死现象,AMD代表:RequireJS,通过 define 和 require 来实现模块的异步加载和导入。
-
ES6模块化:ES6 新增的模块化规范,尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。所以说ES6是编译时加载,通常使用 import 和 export 这两个关键字,分别实现模块的引入和导出模块。
本章的重点在于 es6 中的 import & export :
export
export 用于规定模块的对外接口,一个文件就是一个独立的模块,文件内部的所有变量无法被外界获取,如果希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。 export 允许我们将函数、对象、原始值从一个文件中导出,并使其在其他文件中可用。
export function myFunction() {
// ...
}
或者
function myFunction() {
// ...
}
export {myFunction};
此时将导出一个名为 myFunction 的函数,使其在其他文件中可用。 在 export 输出变量时,还可以使用 as 关键字对输出的变量进行重命名
function myFunction() {
// ...
}
export {myFunction as newFunction };
另外,export语句输出的变量,与对应的值是动态绑定的, 通过接口,可以实时获取到模块内部的值。
export var foo = 'bar';
setTimeout(() => foo = 'haha', 500);
上面代码输出变量 foo,值为 bar,500 毫秒之后变成 haha。
export 可以出现在模块的任何位置,但是必须处于模块顶层。也就是说不能出现在函数、循环或条件语句等块级作用域内。如果处于块级作用域内
function myFunction() {
export var data = 123 // SyntaxError
}
myFunction()
就会出现报错。 当 export 关键字出现在块级作用域内,编译器无法确定模块导出的接口,块级作用域内的代码在运行时才会执行,而编译器只能在编译时进行静态分析。因此,在块级作用域内的 export 会导致编译错误。这也违背了 ES6 模块的设计初衷。
import
import 用于在 JavaScript 中引入一个模块。它允许我们从其他 JavaScript 文件中导入函数、类、变量等,并在当前文件中使用它们。 使用 export 定义的模块,可以通过 import 命令加载。
import { myFunction } from './myModule.js';
从 myModule.js 文件中导入 myFunction,并使其在当前文件中可用。 import 命令接受一对大括号 { },引入的变量、函数、类必须与对外导出的名称相同。
同样的 import 也可以使用 as 关键字,对导入的变量进行重命名
import { myFunction as newFunction} from './myModule.js';
如果多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次。
import 'lodash';
import 'lodash';
而 import 命令本质上是一种输入接口,能够从其他模块导入数据。那么当我们试图修改引入的变量时会发生什么呢?
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
运行时代码出现报错 这是因为在ES6模块系统中,通过 import 命令导入的变量都是只读的,因此不允许在加载模块的脚本中修改这些变量。 上面代码中,脚本加载了变量 a,对其重新赋值就会报错,因为 a 是一个只读的接口。但是,如果 a 是一个对象,改写 a 的属性是允许的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
这里 a 的属性被成功改写,并且其他模块也可以读到改写后的值,但不建议这么做。 如果在加载模块的脚本中修改导入的变量,那么就会破坏模块之间的隔离性,导致代码的可维护性和可靠性下降。
因此,在使用import命令时,不要试图修改导入的变量,而是应该将其视为只读数据,以确保代码的正确性和稳定性。
import 的执行顺序
在我们使用 import 时需要注意的是:import 具有提升效果,会提升到整个模块的头部,首先执行。
这是因为ES6模块在加载时是静态分析的,在编译的过程中就会确定模块的依赖关系和加载顺序。 当一个模块被加载时,会先执行该模块中的所有顶层代码,包括 import 命令。 在执行 import 命令时,会加载所依赖的模块,将导出的接口绑定到当前模块的命名空间中。
因此,在整个模块执行之前,所有的 import 命令都已经执行完毕,并且所依赖的模块也已经被加载完毕。
举个🌰:
// moduleB.js
console.log('haha')
export default function hello() {
console.log('Hello, world!');
}
// moduleA.js
import hello from './moduleB.js';
hello();
// haha
// Hello, world!
当我们加载 moduleA 模块时,
首先会执行所有的顶层代码,包括 import hello from './moduleB.js' 语句。 import 语句会被提升到模块的头部,首先执行,然后会加载 moduleB 模块,输出 'haha' ,并将其中默认导出的函数绑定到当前模块的命名空间中。 再回到 moduleA 中继续解析,当执行到 hello() 函数时,输出了'Hello, world!'。
需要注意的是,虽然 import 具有提升效果,但是它并不会改变模块中代码的执行顺序。 顶层代码的执行顺序仍然是按照书写顺序依次执行的。只不过在执行顶层代码之前,会先执行所有的 import 命令。
import { ... } from 语法
import {} from 语法虽然类似于对象的解构赋值,但是它并不是真正的解构, 因为它并没有创建一个新的变量来存储导入的值。
在解构赋值中,我们可以将一个对象或数组中的属性或元素解构到新的变量中。例如:
const obj = { a: 1, b: 2 };
const { a, b } = obj;
console.log(a); // 1
console.log(b); // 2
在这个例子中,我们将 obj 对象中的 a 和 b 属性解构到了新的变量 a 和 b 中。
而在模块导入中,{} 中的变量名表示的是当前模块中已经被导出的变量名或函数名。 通过 import 语句导入这些变量或函数时,它们会被绑定到当前模块的命名空间中,可以通过相应的变量名来访问这些值。
// moduleA.js
export const foo = 'foo';
export const bar = 'bar';
// moduleB.js
import { foo, bar } from './moduleA.js';
console.log(foo); // 'foo'
console.log(bar); // 'bar'
将 moduleA 模块中导出的 foo 和 bar 变量绑定到了 moduleB 模块的命名空间中。因此,我们可以直接通过 foo 和 bar 变量名来访问这两个变量的值。
需要注意的是,如果我们在 import 语句中使用了未导出的变量名或函数名,或者导入了不存在的模块,都会导致语法错误。
因此,虽然 {} 看起来类似于解构赋值,但是它们在语义上是不同的,不能混淆使用。
在使用 import 语句时,需要确保所导入的变量名或函数名存在,并且与被导出的变量名或函数名一致。
export.default
在 JavaScript 中,一个模块可以有多个导出,包括默认导出和命名导出。
通过 export 导出的对象,在使用 import 命令时,需要知道所加载的变量名或函数名,否则无法加载。
而 export default 则是一种默认导出方式,它可以导出一个默认的值或对象,且不需要使用名称来标识它。
// module.js
export default {
name: 'module'
}
// app.js
import module from './module.js'
console.log(module.name) // 'module'
在使用 import 语句时会将默认导出的对象赋值给变量 module,并且在引入时不需要使用 {}。 如果当一个模块同时使用了 export default 和命名导出,可以使用下面的方式来导入它们:
// module.js
export const name = 'module'
export default {
version: '1.0'
}
// app.js
import module, { name } from './module.js'
console.log(module.version) // '1.0'
console.log(name) // 'module'
注意 export default 只能导出一个默认值,而且不能与其他命名导出一起使用。
import()
由于 import 是 ES6 中的标准语法,用于在静态环境下导入模块。它需要在代码的顶层使用,并且导入的模块名必须是字符串常量,不能使用变量或表达式,import 会在代码执行之前进行模块解析,属于静态导入。
而这里要介绍的 import() 与 import 有所不同, import() 是 ES2020 中的动态导入语法,它允许在运行时根据条件异步加载模块。
同时 import() 可以接受变量或表达式作为参数运行时加载,属于动态导入。
const modulePath = './moduleA.js';
import(modulePath).then(module => {
console.log(module.foo);
});
import() 返回一个 Promise 对象,因此我们需要使用 then() 方法来获取导入的模块。
与 import 相反,import() 语句不能在代码的顶层使用,必须在函数内部或其他代码块中使用。
另外,import() 语句可以在运行时根据条件异步加载模块,这使得它可以用于按需加载模块、优化应用程序性能等场景。
总结
在前端开发中,import 和 export 是非常重要的关键字。它们允许我们将代码分解为模块,使得代码更易于管理和维护。
通过使用这些关键字,我们可以轻松地将代码分解为可重用的组件,并在不同的文件之间共享代码。
import 语句中的 "解构赋值" 并不是真正意义上的解构赋值,而是 named imports,语法上和解构赋值很像,但还是有所差别
- 一般 export default 的内容,需要使用 import xxx from 'xx' 直接接收,不能在 import 时解构
- 通过 export xx 的内容,需要使用 import { xx, xx } from 'xx' 或 import * as xx from 'xx' 接收
希望大家能够通过本文对 export & import 的使用有更进一步的了解,在今后的面试或使用过程中能够避免出错😜
参考
转载自:https://juejin.cn/post/7241114001324556343