likes
comments
collection
share

模块化构建 - import & export

作者站长头像
站长
· 阅读数 18

前言

在 javascript 发展的历史上一直没有模块化体系,因此无这对于开发大型、复杂项目造成了困难。 在 ES6 出现之前社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。直到 ES6 的出现才在语言标准的层面上实现了模块功能,简单易用,可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。 在JavaScript中,模块化可以通过多种方式实现,其中比较常见的有以下几种:

  1. CommonJS:主要用于服务端模块化,主要代表 Node.js,在 Node.js 中使用的模块化规范,采用同步加载模块的方式,通过 require 和 module.exports 来实现模块的导入和导出。

  2. AMD(Asynchronous Module Definition):异步模块定义规范,模块和模块的依赖可以被异步加载,他们的加载不会影响它后面语句的运行。有效避免了采用同步加载方式中导致的页面假死现象,AMD代表:RequireJS,通过 define 和 require 来实现模块的异步加载和导入。

  3. 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 的使用有更进一步的了解,在今后的面试或使用过程中能够避免出错😜

参考

  1. ES6的模块加载,你们真的完全懂了吗?
  2. 彻底明白JavaScript ES6 中的import和export