写在开头
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脚本。“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现,但很难避免尤其是特别复杂的项目。