官方文檔:[https://es6.ruanyifeng.com/](https://es6.ruanyifeng.com/)
一、Class
1、Class
Class只是一個語法糖,其功能用es5也能實現,但是比es5更符合類的期待定義:
constructor代表構造方法,而this指向new 生成的實例
定義類方法時,可以不使用function
注意:類的內部所有定義的方法,都是不可枚舉的(non-enumerable)。
//定義類
class Point {constructor(x, y) {this.x = x;this.y = y;}toString() {return '(' + this.x + ', ' + this.y + ')';}
}
使用
new Point(x,y)
2、constructor
類的默認方法,new生成對象實例的時候執行的就是這個方法一個類必須有constructor方法
constructor默認返回實例對象
3、Class不存在變量提升
```plain new Foo(); // ReferenceError class Foo {} ```4、Class的繼承
extends關鍵字class ColorPoint extends Point {}
注意:
1.子類必須調用super,子類本身沒有this,super是父類的構造函數,調用super,子類才有this
這是因為子類實例的構建,是基于對父類實例加工,只有super方法才能返回父類實例。
5、類的prototype屬性和__proto__屬性
1.子類的__proto__指向父類2.子類的prototype的__proto__指向父類的.prototype
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
super關鍵字
1.調用super方法時,super代表父類的constructor方法
2.作為屬性super調用時,super代表父類本身
ES6改變了Object構造函數的行為,一旦發現Object方法不是通過new Object()這種形式調用,ES6規定Object構造函數會忽略參數。
6、getter和setter
7、Generate方法
直接在方法屬性前加*8、靜態方法static
一個方法前加static,則該方法不會被繼承,而是通過類來直接調用父類的靜態方法,可以被子類繼承。
靜態方法也可以從super上調用,因為super指向父類本身
注意:Class內部只有靜態方法,沒有靜態屬性。
9、new.target
(在構造函數中)返回new命令作用于的那個構造函數。二、Module
1、嚴格模式
ES6 的模塊自動采用嚴格模式,不管你有沒有在模塊頭部加上`"use strict";` 。嚴格模式主要有以下限制。
- 變量必須聲明后再使用
- 禁止
this
指向全局對象 - 不能使用
fn.caller
和fn.arguments
獲取函數調用的堆棧 - 增加了保留字(比如
protected
、static
和interface
)
其中,尤其需要注意this
的限制。ES6 模塊之中,頂層的this
指向undefined
,即不應該在頂層代碼使用this
。
2、export命令
模塊功能主要由兩個命令構成:`export` 和`import` 。`export` 命令用于規定模塊的對外接口,`import` 命令用于輸入其他模塊提供的功能。一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果你希望外部能夠讀取模塊內部的某個變量,就必須使用export
關鍵字輸出該變量。下面是一個 JS 文件,里面使用export
命令輸出變量。
// 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 };
上面代碼在export
命令后面,使用大括號指定所要輸出的一組變量。它與前一種寫法是等價的,但是應該優先考慮使用這種寫法。因為這樣就可以在腳本尾部,一眼看清楚輸出了哪些變量。
export
命令除了輸出變量,還可以輸出函數或類(class)。
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
可以用不同的名字輸出兩次。
3、import命令
使用`export`命令定義了模塊的對外接口以后,其他 JS 文件就可以通過`import`命令加載這個模塊。// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {element.textContent = firstName + ' ' + lastName;
}
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
命令是編譯階段執行的,在代碼運行之前。
import()
類似于 Node 的require
方法,區別主要是前者是異步加載,后者是同步加載
3、模塊的整體加載
除了指定加載某個輸出值,還可以使用整體加載,即用星號(`*`)指定一個對象,所有輸出值都加載在這個對象上面。下面是一個circle.js
文件,它輸出兩個方法area
和circumference
。
// circle.js
export function area(radius) {return Math.PI * radius * radius;
}
export function circumference(radius) {return 2 * Math.PI * radius;
}
現在,加載這個模塊。
// main.js
import { area, circumference } from './circle';
console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
上面寫法是逐一指定要加載的方法,整體加載的寫法如下。
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
4、export default命令
從前面的例子可以看出,使用`import`命令的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。為了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default
命令,為模塊指定默認輸出。
// export-default.js
export default function () {console.log('foo');
}
上面代碼是一個模塊文件export-default.js
,它的默認輸出是一個函數。
其他模塊加載該模塊時,import
命令可以為該匿名函數指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代碼的import
命令,可以用任意名稱指向export-default.js
輸出的方法,這時就不需要知道原模塊輸出的函數名。需要注意的是,這時import
命令后面,不使用大括號。
5、export、import、export default幾種指令的用法總結
一,import(模塊、文件)引入方式
1 引入第三方插件
```plain import echarts from 'echarts' ```2.導入 css文件
```plain import 'iview/dist/styles/iview.css'; ```如果是在.vue文件中那么在外面套個style
1. <style>
2. @import './test.css';
3. </style>
3.導入組件
```plain 1. import name1 from './name1' 2. import name2 from './name2' 3. components:{ 4. name1, 5. name2, 6. }, ```4.import '@…'的語句
@ 等價于 /src 這個目錄,避免寫麻煩又易錯的相對路徑5.引入工具類
```plain 1. 第一種是引入單個方法 2. 3. import {axiosfetch} from './util'; 4. 5. 下面是寫法,需要export導出 6. export function axiosfetch(options) { 7. 8. } ```1. 第二種 導入成組的方法
2.
3. import * as tools from './libs/tools'
4.
5. 其中tools.js中有多個export方法,把tools里所有export的方法導入
6.
7. vue中怎么用呢?
8. Vue.prototype.$tools = tools
9. 直接用 this.$tools.method調用就可以了
說到這 export 和 export default 又有什么區別呢?
下面看下區別
1. 先是 export
2. import {axiosfetch} from './util'; //需要加花括號 可以一次導入多個也可以一次導入一個,但都要加括號
3. 如果是兩個方法
4. import {axiosfetch,post} from './util';
5. 再是 export default
6. import axiosfetch from './util'; //不需要加花括號 只能一個一個導入
二,export,import和export default的關系
export 與import是es6中新增模塊功能最主要的兩個命令。1.export與export default均可用于導出常量、函數、文件、模塊等
2.在一個文件或模塊中,export、import可以有多個,export default僅有一個
3.通過export方式導出,在導入時要加{ },export default則不需要{ }
一、import引入文件路徑
` import` 引入一個依賴包,不需要相對路徑。如:**import? app from ‘app’**;<font style="color:#C7254E;">import</font>
引入一個自己寫的js文件,是需要相對路徑的。如:import app from ‘./app.js’;
二、import引入文件變量名
1 、使用` export` 拋出的變量需要用{}進行` import`1. //a.js
2. export const str = "blablabla~";
3. export function log(sth) {
4. return sth;
5. }
6.
7. 對應的導入方式:
8.
9. //b.js
10. import { str, log as _log } from 'a'; //也可以分開寫兩次,導入的時候帶花括號。還可以用as重命名
2、使用<font style="color:#C7254E;">export default</font>
拋出的變量,只需要自己起一個名字就行:
1. //a.js :
2. var obj = { name: ‘example’ };
3. export default obj;
4.
5. //b.js:
6. import newNname from ‘./a.js’; //newNname 是自己隨便取的名字,這里可以隨便命名
7. console.log(newNname .name); // example;
總結
其中export和export default最大的區別就是export不限變量數 可以一直寫,而export default? 只輸出一次 而且 export出的變量想要使用必須使用{}來盛放,而export default 不需要 只要import任意一個名字來接收對象即可。
三,部分導入和部分導出,全部導入和全部導出
一、部分導出和部分導入
部分導出和部分導入的優勢,當資源比較大時建使用部分導出,這樣一來使用者可以使用部分導入來減少資源體積,比如element-ui官方的就推薦使用部分導入來減少項目體積,因為element-ui是一個十分龐大的框架,如果我們只用到其中的一部分組件, 那么只將用到的組件導入就可以了。1. //部分導出
2. //A.js
3. export function helloWorld(){
4. conselo.log("Hello World");
5. }
6. export function test(){
7. conselo.log("this's test function");
8. }
9.
10. //部分導入
11. //B.js
12. import {helloWorld} from "./A.js" //只導入A.js中的helloWorld方法
13. helloWorld(); //執行utils.js中的helloWorld方法
如果我們需要A.js中的全部資源,則可以全部導入,如下:
1. import * as _A from "./A.js" //導入全部的資源,_A為別名,在調用時使用
2. _A.helloWorld(); //執行A.js中的helloWorld方法
3. _A.test(); //執行A.js中的test方法
二、全部導出和全部導入
如果使用全部導出,那么使用者在導入時則必須全部導入,推薦在寫方法庫時使用部分導出,從而將全部導入或者部分導入的權力留給使用者。需要注意的是:一個js文件中可以有多個export,但只能有一個export default
1. //全部導出 A.js
2. var helloWorld=function(){
3. conselo.log("Hello World");
4. }
5. var test=function(){
6. conselo.log("this's test function");
7. }
8. export default{
9. helloWorld,
10. test
11. }
12.
13. //全部導入 B.js
14. import A from "./A.js"
15. A.helloWorld();
16. A.test();
6、Module 的加載實現
瀏覽器加載
傳統方法
HTML 網頁中,瀏覽器通過`<!-- 頁面內嵌的腳本 -->
<script type="application/javascript">// module code
</script>
<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
默認情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>
標簽就會停下來,等到執行完腳本,再繼續向下渲染。如果是外部腳本,還必須加入腳本下載的時間。如果腳本體積很大,下載和執行的時間就會很長,因此造成瀏覽器堵塞,用戶會感覺到瀏覽器“卡死”了,沒有任何響應。這顯然是很不好的體驗,所以瀏覽器允許腳本異步加載,下面就是兩種異步加載的語法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代碼中,<script>
標簽打開defer
或async
屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行后面的命令。
defer
與async
的區別是:
1、defer
要等到整個頁面在內存中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),才會執行;async
一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以后,再繼續渲染。一句話,<font style="color:#F5222D;">defer</font>
是“渲染完再執行”,<font style="color:#F5222D;">async</font>
是“下載完就執行”。
2、如果有多個defer
腳本,會按照它們在頁面出現的順序加載,而多個async
腳本是不能保證加載順序的。
加載規則
瀏覽器加載 ES6 模塊,也使用`<script type="module" src="./foo.js"></script>
上面代碼在網頁中插入一個模塊foo.js
,由于type
屬性設為module
,所以瀏覽器知道這是一個 ES6 模塊。
瀏覽器對于帶有type="module"
的<script>
,都是異步加載,不會造成堵塞瀏覽器,即等到整個頁面渲染完,再執行模塊腳本,等同于打開了<script>
標簽的defer
屬性。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
如果網頁有多個<script type="module">
,它們會按照在頁面出現的順序依次執行。
<script>
標簽的async
屬性也可以打開,這時只要加載完成,渲染引擎就會中斷渲染立即執行。執行完成后,再恢復渲染。
<script type="module" src="./foo.js" async></script>
一旦使用了async
屬性,<script type="module">
就不會按照在頁面出現的順序執行,而是只要該模塊加載完成,就執行該模塊。
ES6 模塊與 CommonJS 模塊的差異
討論 Node.js 加載 ES6 模塊之前,必須了解 ES6 模塊與 CommonJS 模塊完全不同。它們有三個重大差異。
- CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
- CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
- CommonJS 模塊的
require()
是同步加載模塊,ES6 模塊的import
命令是異步加載,有一個獨立的模塊依賴的解析階段。
第二個差異是因為 CommonJS 加載的是一個對象(即<font style="color:#F5222D;">module.exports</font>
屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
下面重點解釋第一個差異。
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js
的例子。
// lib.js
var counter = 3;
function incCounter() {counter++;
}
module.exports = {counter: counter,incCounter: incCounter,
};
上面代碼輸出內部變量counter
和改寫這個變量的內部方法incCounter
。然后,在main.js
里面加載這個模塊。
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代碼說明,lib.js
模塊加載以后,它的內部變化就影響不到輸出的mod.counter
了。這是因為mod.counter
是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到內部變動后的值。
// lib.js
var counter = 3;
function incCounter() {counter++;
}
module.exports = {get counter() {return counter},incCounter: incCounter,
};
上面代碼中,輸出的counter
屬性實際上是一個取值器函數。現在再執行main.js
,就可以正確讀取內部變量counter
的變動了。
$ node main.js
3
4
ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import
,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import
有點像 Unix 系統的“符號連接”,原始值變了,<font style="color:#F5222D;">import</font>
加載的值也會跟著變。因此,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 模塊輸入的變量counter
是活的,完全反應其所在模塊lib.js
內部的變化。
由于 ES6 輸入的模塊變量,只是一個“符號連接”,所以這個變量是只讀的,對它進行重新賦值會報錯。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
上面代碼中,main.js
從lib.js
輸入變量obj
,可以對<font style="color:#F5222D;">obj</font>
添加屬性,但是重新賦值就會報錯。因為變量<font style="color:#F5222D;">obj</font>
指向的地址是只讀的,不能重新賦值,這就好比main.js
創造了一個名為obj
的const
變量。
最后,<font style="color:#F5222D;">export</font>
通過接口,輸出的是同一個值。不同的腳本加載這個接口,得到的都是同樣的實例。
// 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 的模塊加載方法
概述
JavaScript 現在有兩種模塊。一種是 ES6模塊,一種是 CommonJS 模塊。CommonJS 模塊是 Node.js 專用的,與 ES6 模塊不兼容。語法上面,兩者最明顯的差異是,CommonJS 模塊使用require()
和module.exports
,ES6 模塊使用import
和export
。
它們采用不同的加載方案。從 Node.js v13.2 版本開始,Node.js 已經默認打開了 ES6 模塊支持。
Node.js 要求 ES6 模塊采用.mjs
后綴文件名。也就是說,只要腳本文件里面使用import
或者export
命令,那么就必須采用.mjs
后綴名。Node.js 遇到.mjs
文件,就認為它是 ES6 模塊,默認啟用嚴格模式,不必在每個模塊文件頂部指定"use strict"
。
如果不希望將后綴名改成.mjs
,可以在項目的package.json
文件中,指定type
字段為module
。
{"type": "module"
}
一旦設置了以后,該目錄里面的 JS 腳本,就被解釋用 ES6 模塊。
# 解釋成 ES6 模塊
$ node my-app.js
如果這時還要使用 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
。
package.json 的 main 字段
`package.json`文件有兩個字段可以指定模塊的入口文件:`main`和`exports`。比較簡單的模塊,可以只使用`main`字段,指定模塊加載的入口文件。// ./node_modules/es-module-package/package.json
{"type": "module","main": "./src/index.js"
}
上面代碼指定項目的入口腳本為./src/index.js
,它的格式為 ES6 模塊。如果沒有type
字段,index.js
就會被解釋為 CommonJS 模塊。
然后,import
命令就可以加載這個模塊。
// ./my-app.mjs
import { something } from 'es-module-package';
// 實際加載的是 ./node_modules/es-module-package/src/index.js
上面代碼中,運行該腳本以后,Node.js 就會到./node_modules
目錄下面,尋找es-module-package
模塊,然后根據該模塊package.json
的main
字段去執行入口文件。
這時,如果用 CommonJS 模塊的require()
命令去加載es-module-package
模塊會報錯,因為 CommonJS 模塊不能處理export
命令。
package.json 的 exports 字段
`exports`字段的優先級高于`main`字段。它有多種用法。(1)子目錄別名
package.json
文件的exports
字段可以指定腳本或子目錄的別名。
// ./node_modules/es-module-package/package.json
{"exports": {"./submodule": "./src/submodule.js"}
}
上面的代碼指定src/submodule.js
別名為submodule
,然后就可以從別名加載這個文件。
import submodule from 'es-module-package/submodule';
// 加載 ./node_modules/es-module-package/src/submodule.js
下面是子目錄別名的例子。
// ./node_modules/es-module-package/package.json
{"exports": {"./features/": "./src/features/"}
}
import feature from 'es-module-package/features/x.js';
// 加載 ./node_modules/es-module-package/src/features/x.js
如果沒有指定別名,就不能用“模塊+腳本名”這種形式加載腳本。
// 報錯
import submodule from 'es-module-package/private-module.js';
// 不報錯
import submodule from './node_modules/es-module-package/private-module.js';
(2)main 的別名
exports
字段的別名如果是.
,就代表模塊的主入口,優先級高于main
字段,并且可以直接簡寫成exports
字段的值。
{"exports": {".": "./main.js"}
}
// 等同于
{"exports": "./main.js"
}
由于exports
字段只有支持 ES6 的 Node.js 才認識,所以可以用來兼容舊版本的 Node.js。
{"main": "./main-legacy.cjs","exports": {".": "./main-modern.cjs"}
}
上面代碼中,老版本的 Node.js (不支持 ES6 模塊)的入口文件是main-legacy.cjs
,新版本的 Node.js 的入口文件是main-modern.cjs
。
(3)條件加載
利用.
這個別名,可以為 ES6 模塊和 CommonJS 指定不同的入口。目前,這個功能需要在 Node.js 運行的時候,打開--experimental-conditional-exports
標志。
{"type": "module","exports": {".": {"require": "./main.cjs","default": "./main.js"}}
}
上面代碼中,別名.
的require
條件指定require()
命令的入口文件(即 CommonJS 的入口),default
條件指定其他情況的入口(即 ES6 的入口)。
上面的寫法可以簡寫如下。
{"exports": {"require": "./main.cjs","default": "./main.js"}
}
注意,如果同時還有其他別名,就不能采用簡寫,否則或報錯。
{// 報錯"exports": {"./feature": "./lib/feature.js","require": "./main.cjs","default": "./main.js"}
}
CommonJS 模塊加載 ES6 模塊
CommonJS 的`require()`命令不能加載 ES6 模塊,會報錯,只能使用`import()`這個方法加載。(async () => {await import('./my-app.mjs');
})();
上面代碼可以在 CommonJS 模塊中運行。
require()
不支持 ES6 模塊的一個原因是,它是同步加載,而 ES6 模塊內部可以使用頂層await
命令,導致無法被同步加載。
ES6 模塊加載 CommonJS 模塊
ES6 模塊的`import`命令可以加載 CommonJS 模塊,但是只能整體加載,不能只加載單一的輸出項。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。
CommonJS一旦輸出一個值,模塊內部的變化就影響不到這個值。
ES6模塊原始值變了,import輸入的值也會跟著變。
// 正確
import packageMain from 'commonjs-package';
// 報錯
import { method } from 'commonjs-package';
這是因為 ES6 模塊需要支持靜態代碼分析,而 CommonJS 模塊的輸出接口是module.exports
,是一個對象,無法被靜態分析,所以只能整體加載。
加載單一的輸出項,可以寫成下面這樣。
import packageMain from 'commonjs-package';
const { method } = packageMain;
還有一種變通的加載方法,就是使用 Node.js 內置的module.createRequire()
方法。
// cjs.cjs
module.exports = 'cjs';
// esm.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true
上面代碼中,ES6 模塊通過module.createRequire()
方法可以加載 CommonJS 模塊。但是,這種寫法等于將 ES6 和 CommonJS 混在一起了,所以不建議使用。
同時支持兩種格式的模塊
一個模塊同時要支持 CommonJS 和 ES6 兩種格式,也很容易。如果原始模塊是 ES6 格式,那么需要給出一個整體輸出接口,比如export default obj
,使得 CommonJS 可以用import()
進行加載。
如果原始模塊是 CommonJS 格式,那么可以加一個包裝層。
import cjsModule from '../index.js';
export const foo = cjsModule.foo;
上面代碼先整體輸入 CommonJS 模塊,然后再根據需要輸出具名接口。
你可以把這個文件的后綴名改為.mjs
,或者將它放在一個子目錄,再在這個子目錄里面放一個單獨的package.json
文件,指明{ type: "module" }
。
另一種做法是在package.json
文件的exports
字段,指明兩種格式模塊各自的加載入口。
"exports":{"require": "./index.js","import": "./esm/wrapper.js"
}
上面代碼指定require()
和import
,加載該模塊會自動切換到不一樣的入口文件。
Node.js 的內置模塊
Node.js 的內置模塊可以整體加載,也可以加載指定的輸出項。// 整體加載
import EventEmitter from 'events';
const e = new EventEmitter();
// 加載指定的輸出項
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {if (err) {console.error(err);} else {console.log(source);}
});
加載路徑
ES6 模塊的加載路徑必須給出腳本的完整路徑,不能省略腳本的后綴名。`import`命令和`package.json`文件的`main`字段如果省略腳本的后綴名,會報錯。// ES6 模塊中將報錯
import { something } from './index';
為了與瀏覽器的import
加載規則相同,Node.js 的.mjs
文件支持 URL 路徑。
import './foo.mjs?query=1'; // 加載 ./foo 傳入參數 ?query=1
上面代碼中,腳本路徑帶有參數?query=1
,Node 會按 URL 規則解讀。同一個腳本只要參數不同,就會被加載多次,并且保存成不同的緩存。由于這個原因,只要文件名中含有:
、%
、#
、?
等特殊字符,最好對這些字符進行轉義。
目前,Node.js 的import
命令只支持加載本地模塊(file:
協議)和data:
協議,不支持加載遠程模塊。另外,腳本路徑只支持相對路徑,不支持絕對路徑(即以/
或//
開頭的路徑)。
內部變量
ES6 模塊應該是通用的,同一個模塊不用修改,就可以用在瀏覽器環境和服務器環境。為了達到這個目標,Node.js 規定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內部變量。首先,就是this
關鍵字。ES6 模塊之中,頂層的this
指向undefined
;CommonJS 模塊的頂層this
指向當前模塊,這是兩者的一個重大差異。
其次,以下這些頂層變量在 ES6 模塊之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
循環加載
“循環加載”(circular dependency)指的是,`a`腳本的執行依賴`b`腳本,而`b`腳本的執行又依賴`a`腳本。// a.js
var b = require('b');
// b.js
var a = require('a');
通常,“循環加載”表示存在強耦合,如果處理不好,還可能導致遞歸加載,使得程序無法執行,因此應該避免出現。
但是實際上,這是很難避免的,尤其是依賴關系復雜的大項目,很容易出現a
依賴b
,b
依賴c
,c
又依賴a
這樣的情況。這意味著,模塊加載機制必須考慮“循環加載”的情況。
對于 JavaScript 語言來說,目前最常見的兩種模塊格式 CommonJS 和 ES6,處理“循環加載”的方法是不一樣的,返回的結果也不一樣。
CommonJS 模塊的加載原理
介紹 ES6 如何處理“循環加載”之前,先介紹目前最流行的 CommonJS 模塊格式的加載原理。CommonJS 的一個模塊,就是一個腳本文件。require
命令第一次加載該腳本,就會執行整個腳本,然后在內存生成一個對象。
{id: '...',exports: { ... },loaded: true,...
}
上面代碼就是 Node 內部加載模塊后生成的一個對象。該對象的id
屬性是模塊名,exports
屬性是模塊輸出的各個接口,loaded
屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其他還有很多屬性,這里都省略了。
以后需要用到這個模塊的時候,就會到exports
屬性上面取值。即使再次執行require
命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統緩存。
CommonJS 模塊的循環加載
CommonJS 模塊的重要特性是加載時執行,即腳本代碼在`require`的時候,就會全部執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。讓我們來看,Node 官方文檔里面的例子。腳本文件a.js
代碼如下。
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
上面代碼之中,a.js
腳本先輸出一個done
變量,然后加載另一個腳本文件b.js
。注意,此時a.js
代碼就停在這里,等待b.js
執行完畢,再往下執行。
再看b.js
的代碼。
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
上面代碼之中,b.js
執行到第二行,就會去加載a.js
,這時,就發生了“循環加載”。系統會去a.js
模塊對應對象的exports
屬性取值,可是因為a.js
還沒有執行完,從exports
屬性只能取回已經執行的部分,而不是最后的值。
a.js
已經執行的部分,只有一行。
exports.done = false;
因此,對于b.js
來說,它從a.js
只輸入一個變量done
,值為false
。
然后,b.js
接著往下執行,等到全部執行完畢,再把執行權交還給a.js
。于是,a.js
接著往下執行,直到執行完畢。我們寫一個腳本main.js
,驗證這個過程。
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
執行main.js
,運行結果如下。
$ node main.js
在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
上面的代碼證明了兩件事。一是,在b.js
之中,a.js
沒有執行完畢,只執行了第一行。二是,main.js
執行到第二行時,不會再次執行b.js
,而是輸出緩存的b.js
的執行結果,即它的第四行。
exports.done = true;
總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。
另外,由于 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼全部執行后的值,兩者可能會有差異。所以,輸入變量的時候,必須非常小心。
var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法
exports.good = function (arg) {return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {return foo('bad', arg); // 使用的是一個部分加載時的值
};
上面代碼中,如果發生循環加載,require('a').foo
的值很可能后面會被改寫,改用require('a')
會更保險一點。
ES6 模塊的循環加載
ES6 處理“循環加載”與 CommonJS 有本質的不同。ES6 模塊是動態引用,如果使用`import`從一個模塊加載變量(即`import foo from 'foo'`),那些變量不會被緩存,而是成為一個指向被加載模塊的引用,需要開發者自己保證,真正取值的時候能夠取到值。請看下面這個例子。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
上面代碼中,a.mjs
加載b.mjs
,b.mjs
又加載a.mjs
,構成循環加載。執行a.mjs
,結果如下。
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
上面代碼中,執行a.mjs
以后會報錯,foo
變量未定義,這是為什么?
讓我們一行行來看,ES6 循環加載是怎么處理的。首先,執行a.mjs
以后,引擎發現它加載了b.mjs
,因此會優先執行b.mjs
,然后再執行a.mjs
。接著,執行b.mjs
的時候,已知它從a.mjs
輸入了foo
接口,這時不會去執行a.mjs
,而是認為這個接口已經存在了,繼續往下執行。執行到第三行console.log(foo)
的時候,才發現這個接口根本沒定義,因此報錯。
解決這個問題的方法,就是讓b.mjs
運行的時候,foo
已經有定義了。這可以通過將foo
寫成函數來解決。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
這時再執行a.mjs
就可以得到預期結果。
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
這是因為函數具有提升作用,在執行import {bar} from './b'
時,函數foo
就已經有定義了,所以b.mjs
加載的時候不會報錯。這也意味著,如果把函數foo
改寫成函數表達式,也會報錯。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};
上面代碼的第四行,改成了函數表達式,就不具有提升作用,執行就會報錯。
我們再來看 ES6 模塊加載器SystemJS給出的一個例子。
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {counter++;return n === 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {return n !== 0 && even(n - 1);
}
上面代碼中,even.js
里面的函數even
有一個參數n
,只要不等于 0,就會減去 1,傳入加載的odd()
。odd.js
也會做類似操作。
運行上面這段代碼,結果如下。
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
上面代碼中,參數n
從 10 變為 0 的過程中,even()
一共會執行 6 次,所以變量counter
等于 6。第二次調用even()
時,參數n
從 20 變為 0,even()
一共會執行 11 次,加上前面的 6 次,所以變量counter
等于 17。
這個例子要是改寫成 CommonJS,就根本無法執行,會報錯。
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {counter++;return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function (n) {return n != 0 && even(n - 1);
}
上面代碼中,even.js
加載odd.js
,而odd.js
又去加載even.js
,形成“循環加載”。這時,執行引擎就會輸出even.js
已經執行的部分(不存在任何結果),所以在odd.js
之中,變量even
等于undefined
,等到后面調用even(n - 1)
就會報錯。
$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function