1. 模块加载方案 commonJSnode
背景:es6
历史上,JavaScript 一直没有模块(module)体系,json
没法将一个大程序拆分红互相依赖的小文件,再用简单的方法拼装起来。promise
其余语言都有这项功能: 浏览器
Ruby 的require
缓存
Python 的import
服务器
甚至就连 CSS 都有@import
异步
可是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目造成了巨大障碍async
在 ES6 以前,社区制定了一些模块加载方案,最主要的有:函数
CommonJS 用于服务器
AMD 用于浏览器
ES6 在语言标准的层面上,实现了模块功能,并且实现得至关简单,彻底能够取代 CommonJS 和 AMD 规
范,成为浏览器和服务器通用的模块解决方案
ES6 模块的设计思想: 是尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。
好比,CommonJS 模块就是对象,输入时必须查找对象属性。
运行时加载:实质是总体加载fs
模块(即加载fs
的全部方法),生成一个对象(_fs
),而后再从这个对象上面读取 3 个方法
let { stat, exists, readFile } = require('fs'); // CommonJS模块 // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
ES6 模块 不是对象,而是经过 export
命令显式指定 输出的代码,再经过 import
命令输入
编译时加载: 实质是从fs
模块加载 3 个方法,其余方法不加载。
import { stat, exists, readFile } from 'fs'; // ES6模块
ES6 能够在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
固然,这也致使了无法引用 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)
2. 模块功能主要由两个命令构成:export
和 import
export、import
命令 能够出如今模块的任何位置,
只要处于模块顶层就能够,
不能处于块级做用域内,不然就会报错
export
用于输出模块的对外接口
一个模块就是一个独立的文件。
注意1. export
语句输出的接口,与其对应的值是动态绑定关系,
即经过该接口,能够取到模块内部实时的值
export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // 输出变量 foo,值为bar,500 毫秒以后变成baz
不一样于CommonJS 模块输出的是值的缓存,不存在动态更新
注意2. export
命令规定的是对外的接口,必须与模块内部的变量创建一一对应关系。
// 报错 export 1; // 报错 var m = 1; export m;
// 报错
function f() {}
export f;
/**** 正确写法 ****/ // 写法一 export var m = 1; // 写法二 var m = 1; export {m}; // 写法三 var n = 1; export {n as m};
// 正确
export function f() {};
// 正确
function f() {}
export {f};
export
命令输出变量模块文件内部的全部变量,外部没法获取。
若是你但愿外部可以读取模块内部的某个变量,就必须使用 export
关键字输出该变量
// profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958;
优先考虑如下写法。由于这样就能够在脚本尾部,一眼看清楚输出了哪些变量。
// profile.js var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export {firstName, lastName, year};
export
命令输出函数或类(class)export function multiply(x, y) { return x * y; };
as...}
关键字重命名function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion // v2 能够用不一样的名字输出两次。 };
import
用于输入其余模块提供的功能
其余 JS 文件就能够经过 import
命令加载这个模块
// main.js import {firstName, lastName, year} from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; }
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.foo = 'hello'; // 合法操做
import
命令具备提高效果,会提高到整个模块的头部,首先执行本质是,import
命令是编译阶段执行的,在代码运行以前就输入完成了。
import
是静态执行,因此不能使用表达式和变量,这些只有在运行时才能获得结果的语法结构import 'lodash'; import 'lodash'; // 屡次重复执行同一句 import 语句,那么只会执行一次,而不会执行屡次
require
命令 和 ES6 模块的import
命令,能够写在同一个模块里面,可是最好不要这样作import
在静态解析阶段执行,因此它是一个模块之中最先执行的。下面的代码可能不会获得预期结果。require('core-js/modules/es6.symbol'); require('core-js/modules/es6.promise'); import React from 'React';
模块的总体加载
// circle.js
export function area(radius) { return Math.PI * radius * radius; }; export function circumference(radius) { return 2 * Math.PI * radius; };
// index.js import * as circle from './circle'; console.log('圆面积:' + circle.area(4)); console.log('圆周长:' + circle.circumference(14));
export default 模块指定默认输出
使用import
命令的时候,用户须要知道所要加载的变量名或函数名,不然没法加载。
可是,用户确定但愿快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到 export default
命令,为模块指定默认输出
一个模块只能有一个默认输出, 所以 export default
命令只能使用一次
使用 export default
时,对应的 import
语句不须要使用大括号
// export-default.js export default function foo() { console.log('foo'); }; // 或者写成 function foo() { console.log('foo'); }; export default foo;
import
语句中,同时输入默认方法和其余接口,能够写成下面这样export default function (obj) { // ··· } export function each(obj, iterator, context) { // ··· } export { each as forEach }; /**** 导入 ****/ import _, { each, forEach } from 'lodash';
跨模块常量 const
引入import()
函数,完成动态加载
import
函数的参数specifier
,指定所要加载的模块的位置。
import
命令可以接受什么参数,import()
函数就能接受什么参数,二者区别主要是后者为动态加载。
import()
相似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载
import()
返回一个 Promise 对象
const main = document.querySelector('main'); import(`./section-modules/${someVariable}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; });
3. 浏览器加载
默认状况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>
标签就会停下
来,等到执行完脚本,再继续向下渲染。若是是外部脚本,还必须加入脚本下载的时间
若是脚本体积很大,下载和执行的时间就会很长,所以形成浏览器堵塞,用户会感受到浏览器“卡死”
了,没有任何响应。这显然是很很差的体验,因此浏览器容许脚本异步加载,
下面就是两种异步加载的语法
<script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>
<script>
标签打开 defer
或 async
属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer
与 async
的区别是:
defer
要等到整个页面在内存中正常渲染结束
(DOM 结构彻底生成,以及其余脚本执行完成),才会执行
async
一旦下载完,渲染引擎就会中断渲染,
执行这个脚本之后,再继续渲染
<script>
标签,可是要加入type="module"
属性<script type="module" src="./foo.js"></script>
浏览器对于带有 type="module"
的 <script>
,都是异步加载,不会形成堵塞浏览器,
即等到整个页面渲染完,再执行模块脚本,等同于打开了 <script>
标签的 defer
属性。
ES6 模块也容许内嵌在网页中,语法行为与加载外部脚本彻底一致
<script type="module"> import utils from "./utils.js"; // other code </script>
注意:
use strict
。import
命令加载其余模块(.js
后缀不可省略,须要提供绝对 URL 或相对 URL),也可使用export
命令输出对外接口。this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无心义的。this
等于 undefined
这个语法点,能够侦测当前代码是否在 ES6 模块之中。const isNotModuleScript = this !== undefined
4. ES6 模块与 CommonJS 模块彻底不一样。
CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成
ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
/**** 定义接口 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
JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。
等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值
// 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 再取值,发现值变了
5. Node 对 ES6 模块的处理比较麻烦,由于它有本身的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。
目前的解决方案是,将二者分开,ES6 模块 和 CommonJS 采用各自的加载方案
import
加载规则相同,Node 的.mjs
文件支持 URL 路径。import './foo?query=1'; // 加载 ./foo 传入参数 ?query=1
:
、%
、#
、?
等特殊字符,最好对这些字符进行转义。由于 Node 会按 URL 规则解读
import
命令只支持加载本地模块(file:
协议),不支持加载远程模块import
命令会去 node_modules
目录寻找这个模块。好比import './foo'
,Node 会依次尝试四个后缀名
./foo.mjs
./foo.js
./foo.json
./foo.node
。
若是这些脚本文件都不存在,Node 就会去加载 ./foo/package.json
的 main
字段指定的脚本。
若是 ./foo/package.json
不存在 或者 没有 main
字段,那么就会抛出错误。
6. ES6 模块加载 CommonJS 模块
CommonJS 模块的输出 都定义在 module.exports
这个属性上面
// a.js module.exports = { foo: 'hello', bar: 'world' }; // 等同于 export default { foo: 'hello', bar: 'world' }; /**** export 指向 modeule.exports, 即 exports 变量 是对 module 的 exports 属性的引用 所以 ****/ module.exports = func; // 正确 export = func; // 错误
module.exports
会被视为默认输出,即import
命令实际上输入的是这样一个对象{ default: module.exports }
module.exports
// 写法一 import baz from './a'; // baz = {foo: 'hello', bar: 'world'}; // 写法二 import {default as baz} from './a'; // baz = {foo: 'hello', bar: 'world'}; // 写法三 import * as baz from './a'; // baz = { // get default() {return module.exports;}, // get foo() {return this.default.foo}.bind(baz), // get bar() {return this.default.bar}.bind(baz) // }
require
命令第一次导入加载模块内容,就会执行整个脚本,而后在内存生成一个对象正是由于有了这层看不见的函数,因此一个模块就是一个函数做用域,与其余模块做用域互相独立