Code前端首页关于Code前端联系我们

JS-模块

terry 2年前 (2023-09-08) 阅读数 181 #Vue

详细信息请前往:阮一峰-es6-module-写得很好。

概述

在ES6之前,社区开发了一些模块加载解决方案,其中最重要的是CommonJS和AMD。第一个用于服务器,第二个用于浏览器。 ES6在语言标准层面实现了模块功能,而且实现相当简单。它可以完全取代CommonJS和AMD规范,成为浏览器和服务器的通用模块解决方案。

ES6 模块被设计为尽可能静态,因此可以在编译时指定模块依赖关系以及输入和输出变量。 CommonJS 和 AMD 模块都只能在运行时确定这些事情。例如,CommonJS 模块是对象,需要在输入时查找对象属性。

// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
 

上面代码的本质就是将模块fs作为一个整体进行加载(即加载所有的fs方法),生成一个对象(_fs),然后从这个对象中读取3个方法。这种加载称为“运行时加载”,因为这个对象只能在运行时获取,因此在编译时无法进行“静态优化”。

ES6 模块不是对象,而是通过 export 命令显式指定用于输出,然后使用 import 命令进行输入的代码。

// ES6模块
import { stat, exists, readFile } from 'fs';
 

上面代码的本质是从fs模块加载3个方法,而不是加载其他方法。这种加载称为“编译时加载”或静态加载,即ES6可以在编译时完成模块的加载,比CommonJS的模块加载效率更高。当然,这也使得无法引用 ES6 模块本身,因为它不是一个对象。

由于 ES6 模块是在编译时加载的,因此可以进行静态分析。有了它,JavaScript 语法可以进一步扩展,例如引入宏和类型系统等只能通过静态分析才能实现的功能。

严格模式

ES6 模块自动采用严格模式,无论您是否添加“use strict”;在模块头中。

严格模式主要有以下限制:

  • 变量必须在使用前声明
  • 函数参数不能有同名属性,否则会报错
  • 不能与命令 with
  • 一起使用
  • 不能给只读属性赋值,否则会报错
  • 前缀0不能作为八进制数,否则会报错
  • 不能删除不可删除的属性,否则会报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval 不在其外部作用域中引入变量
  • evalarguments 无法重新排列
  • arguments 不会自动反映功能参数的更改
  • 无法使用arguments.callee
  • 无法使用arguments.caller
  • 不允许 this 指向全局对象
  • 无法使用fn.callerfn.arguments获取函数调用堆栈
  • 添加保留字(如protectedstaticinterface

出口

模块的功能主要由两个命令组成:exportimportexport命令用于指定模块对外接口,import命令用于指定其他模块提供的功能。

该模块是一个独立的文件。文件内的所有变量都无法从外部访问。如果希望外部能够读取模块内部的变量,则必须使用export关键字来输出该变量。下面是一个使用export命令输出变量的JS文件。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
 

上面的代码是一个存储用户信息的文件profile.js。 ES6 将此视为一个使用 export 语句输出三个变量的模块。

除了上述之外,还有一种写法

export

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };
 

上面的代码位于导出语句之后,并使用大括号指定要输出的变量集。它相当于前一个(直接放在 var 命令之前),但应优先使用。因为这样你就能一目了然地看到脚本末尾写了哪些变量。

除了输出变量之外,导出命令还可以显示函数或类。

export function multiply(x, y) {
  return x * y;
};
 

上面的代码显示了函数multiply

通常,使用 export 的变量输出是其原始名称,但可以使用 as 关键字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
 

上面的代码使用as关键字来重命名函数v1和v2的外部接口。重命名后,v2可能会以不同的名称输出两次。

需要注意的是,导出命令指定了一个外部接口,并且必须与模块内部的变量进行单独匹配。

// 报错
export 1;

// 报错
var m = 1;
export m;
 

以上两种写法都会因为没有外部接口可用而报错。第一种写法直接输出1,第二种写法通过变量m直接输出1。1只是一个值,不是一个接口。正确的表示方法如下。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};
 

以上三种表示方式都是正确的,指定了外部接口m,其他脚本可以通过该接口获取值1。它们的本质是创建接口名称和模块内部变量之间的相互对应关系。

同样,函数和类的输出也必须符合这种表示方法。

// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};
 

另外,export命令的接口输出与对应的值具有动态绑定关系,即可以通过该接口获取模块内部的实时值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
 

上面的代码打印变量foo,值为 bar,500 毫秒后更改为 baz

这与 CommonJS 规范完全不同。 CommonJS 模块打印值缓存,并且不会发生动态更新,详细信息请参阅下面的《Module 的加载实现》。

最后,export 语句可以出现在模块中的任何位置,只要它位于模块的顶层即可。如果是在块级范围内,就会报错,下一节的import语句也会报错。这是因为静态优化不能在条件代码块内执行,这违背了 ES6 模块的最初设计意图。

function foo() {
  export default 'bar' // SyntaxError
}
foo()
 

上面代码中,函数中放置了命令export,就会报错。

导入

使用export命令定义模块对外接口后,其他JS文件就可以使用import命令加载该模块。

上述代码中的import命令用于加载文件profile.js并从中输入变量。 import 命令接受一对花括号,指定要从其他模块导入的变量名称。大括号内的变量名称必须与导入模块的外部接口名称相同(profile.js)。

如果要重命名输入变量,import 命令必须使用关键字 as 来重命名输入变量。

import { lastName as surname } from './profile.js';
 
命令

import指定的所有变量都是只读的,因为它们的本质是输入接口。换句话说,不得在加载模块的脚本中覆盖该接口。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;
 

上面的代码中,脚本加载了变量a,如果重新赋值就会报错,因为a是只读接口。但是,如果 a 是一个对象,则允许覆盖 a 的属性。

import {a} from './xxx.js'

a.foo = 'hello'; // 合法操作
 

上述代码中,属性a可以成功覆盖,其他模块也可以读取覆盖后的值。然而,这种表示方法很难检查错误。建议将所有输入变量视为完全只读,并且不要轻易更改其属性。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果没有路径,只有模块的名称,那么必须有一个配置文件告诉 JavaScript 引擎模块的位置。

import { myMethod } from 'util';
 

在上面的代码中,util是模块文件的名称。由于它不包含路径,因此必须将其配置为告诉引擎如何获取该模块。

注意,命令import具有提升作用,会被提升到整个模块的头部并首先执行。

foo();

import { foo } from 'my_module';
 

上面的代码不会报错,因为import是在调用foo之前运行的。这种行为的本质是命令import在编译阶段、代码执行之前执行。

集成模块加载(*)

// main.js

import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
 

上面的write方法一一指定了要加载的方法。总加载的写入方法如下。

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
 

导出默认

命令

export default 指定模块的默认输出。这是唯一的价值。一个模块只能有一个默认输出。不建议在开发过程中使用。

// 第一组
export default function crc32() { // 输出
  // ...
}

import crc32 from 'crc32'; // 输入

// 第二组
export function crc32() { // 输出
  // ...
};

import {crc32} from 'crc32'; // 输入
 

参见

阮一峰-ES6入门

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门