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
命令第一次導入加載模塊內容,就會執行整個腳本,而後在內存生成一個對象正是由於有了這層看不見的函數,因此一個模塊就是一個函數做用域,與其餘模塊做用域互相獨立