写在开头
ES6
常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。
相关文章
- ES6常用但被忽略的方法(第一弹解构赋值和数值)
- ES6常用但被忽略的方法(第二弹函数、数组和对象)
- ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )
- ES6常用但被忽略的方法(第四弹Proxy和Reflect)
- ES6常用但被忽略的方法(第五弹Promise和Iterator)
- ES6常用但被忽略的方法(第六弹Generator )
- ES6常用但被忽略的方法(第七弹async)
- ES6常用但被忽略的方法(第八弹Class)
- ES6常用但被忽略的方法(第十弹项目开发规范)
- ES6常用但被忽略的方法(第十一弹Decorator)
- ES6常用但被忽略的方法(终弹-最新提案)
Module
- ES6-Module
CommonJS
和AMD
模块,都只能在运行时确定这些东西。ES6
可以在编译时就完成模块加载,效率要比CommonJS
模块的加载方式高,这种加载称为“编译时加载”或者静态加载。- 优势
- 能进一步拓宽
JavaScript
的语法,比如引入宏(macro
)和类型检验(type system
)这些只能靠静态分析实现的功能。 - 不再需要
UMD
模块格式。 - 将来浏览器的新
API
就能用模块格式提供,不再必须做成全局变量或者navigator
对象的属性。 - 不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供。
- 能进一步拓宽
严格模式
ES6
的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
。- 限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀
0
表示八进制数,否则报错 - 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
和arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
- 尤其需要注意this的限制。
ES6
模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this
。
export 命令
export
命令用于规定模块的对外接口。- 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用
export
关键字输出该变量。除了输出变量,还可以输出函数或类(class
)。
// index.js
export const name = 'detanx';
export const year = 1995;
export function multiply(x, y) {
return x * y;
};
// 写法二
const name = 'detanx';
const year = 1995;
function multiply(x, y) {
return x * y;
};
export { name, year, multiply }
export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。重命名后,可以用不同的名字输出多次。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
var m = 1;
export m;
// 正确
export var m = 1;
var m = 1;
export {m};
var n = 1;
export {n as m};
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。export *
命令会忽略模块的default
方法。
// 整体输出
export * from 'my_module';
import 命令
- 使用
export
命令定义了模块的对外接口以后,其他JS
文件就可以通过import
命令加载这个模块。想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { name, year } from './index.js';
import { name as username } from './profile.js';
import
命令输入的变量都是只读的,因为它的本质是输入接口。 也就是说,不允许在加载模块的脚本里面,改写接口。如果a
是一个对象,改写a
的属性是允许的。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件(例如使用webpack
配置路径),告诉JavaScript
引擎该模块的位置。
import {myMethod} from 'util';
import
命令具有提升效果,会提升到整个模块的头部,首先执行。
foo(); // 不会报错
import { foo } from 'my_module';
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
- 多次重复执行同一句
import
语句,那么只会执行一次,而不会执行多次。
import 'lodash';
import 'lodash'; // 只会执行一次
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
模块的整体加载
- 除了指定加载某个输出值,还可以使用整体加载,即用星号(
*
)指定一个对象,所有输出值都加载在这个对象上面。
import * as user from './index.js';
user.name; // 'detanx'
user.year; // 1995
export default 命令
export default
命令,为模块指定默认输出。其他模块加载该模块时,import
命令(import
命令后面,不使用大括号)可以为该匿名函数指定任意名字。
// export-default.js
export default function () {
console.log('detanx');
}
// import-default.js
import customName from './export-default';
customName(); // 'detanx'
- 使用
export default
时,对应的import
语句不需要使用大括号;使用export
,对应的import
语句需要使用大括号。 一个模块只能有一个默认输出,因此export default
命令只能使用一次。
export default function crc32() { ...}
import crc32 from 'crc32';
export function crc32() { ... };
import { crc32 } from 'crc32';
export 与 import 的复合写法
- 如果在一个模块之中,先输入后输出同一个模块,
import
语句可以与export
语句写在一起。写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
- 模块的接口改名和整体输出,也可以采用这种写法。
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
- 默认接口的写法如下。
export { default } from 'foo';
- 具名接口改为默认接口的写法如下。
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
- 同样地,默认接口也可以改名为具名接口。
export { default as es6 } from './someModule';
ES2020 之前,有一种import语句,没有对应的复合写法。
import * as someIdentifier from "someModule";
ES2020
补上了这个写法。
export * as ns from "mod";
// 等同于
import * as ns from "mod";
export {ns};
应用
- 公共模块
- 例如项目有很多的公共方法放到一个
constant
的文件,我们需要什么就加载什么。
// constants.js 模块 export const A = 1; export const B = 3; export const C = 4; // use.js import {A, B} from './constants';
- 例如项目有很多的公共方法放到一个
- import()
import
命令会被JavaScript
引擎静态分析,先于模块内的其他语句执行(import
命令叫做“连接”binding
其实更合适)。所以我们只能在最顶层去使用。ES2020
引入import()
函数,支持动态加载模块。import()
返回一个Promise
对象。
const main = document.querySelector('main'); import(`./section-modules/${someVariable}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; });
import()
函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()
函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()
类似于Node
的require
方法,区别主要是前者是异步加载,后者是同步加载。- 适用场景按需加载、条件加载、动态的模块路径。
- 注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./myModule.js') .then(({export1, export2}) => { // ...· });
- 上面代码中,
export1
和export2
都是myModule.js
的输出接口,可以解构获得。 - 如果模块有
default
输出接口,可以用参数直接获得。
import('./myModule.js') .then(myModule => { console.log(myModule.default); });
- 上面的代码也可以使用具名输入的形式。
import('./myModule.js') .then(({default: theDefault}) => { console.log(theDefault); });
- 如果想同时加载多个模块,可以采用下面的写法。
Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]) .then(([module1, module2, module3]) => { ··· });
import()
也可以用在async
函数之中。
async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]); } main();
Module 加载实现
简介
- 传统加载
- 默认情况下,浏览器是同步加载
JavaScript
脚本,即渲染引擎遇到<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。为了解决<script>
标签打开defer
或async
属性,脚本就会异步加载。 defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM
结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
- 默认情况下,浏览器是同步加载
- 加载规则
- 浏览器加载
ES6
模块,也使用<script>
标签,但是要加入type="module"
属性。等同于打开了<script>
标签的defer
属性。
<script type="module" src="./foo.js"></script> <!-- 等同于 --> <script type="module" src="./foo.js" defer></script>
- 对于外部的模块脚本,有几点需要注意。
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
"use strict"
。 - 模块之中,可以使用
import
命令加载其他模块(.js
后缀不可省略,需要提供绝对URL
或相对URL
),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
import utils from 'https://example.com/js/utils.js'; const x = 1; console.log(x === window.x); //false console.log(this === undefined); // true
- 利用顶层的
this
等于undefined
这个语法点,可以侦测当前代码是否在ES6
模块之中。
const isNotModuleScript = this !== undefined;
- 浏览器加载
ES6 模块与 CommonJS 模块的差异
- 讨论
Node.js
加载ES6
模块之前,必须了解ES6
模块与CommonJS
模块完全不同。 - 它们有两个重大差异。
CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。CommonJS
模块是运行时加载,ES6
模块是编译时输出接口。(因为CommonJS
加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)
CommonJS
模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。除非写成一个函数,才能得到内部变动后的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// 写成函数
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
$ node main.js
3
4
ES6
模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
ES6
输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
export
通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
- 上面的脚本
mod.js
,输出的是一个C
的实例。不同的脚本加载这个模块,得到的都是同一个实例。
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
- 现在执行
main.js
,输出的是1
。
$ babel-node main.js
1
- 证明了
x.js
和y.js
加载的都是C
的同一个实例。
Node.js 加载
Node.js
要求ES6
模块采用.mjs
后缀文件名。Node.js
遇到.mjs
文件,就认为它是ES6
模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。 如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。
{
"type": "module"
}
-
这时还要使用
CommonJS
模块,那么需要将CommonJS
脚本的后缀名都改成.cjs
。如果没有type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成CommonJS
模块。 -
总结:
.mjs
文件总是以ES6
模块加载,.cjs
文件总是以CommonJS
模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。 -
注意,
ES6
模块与CommonJS
模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import
命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import。
-
Node.js 加载 主要是介绍
ES6
模块和CommonJS
相互之间的支持,有兴趣的可以自己去看看。
循环加载
- “循环加载”(
circular dependency
)指的是,a
脚本的执行依赖b
脚本,而b
脚本的执行又依赖a
脚本。“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现,但很难避免尤其是特别复杂的项目。