- webpack4打包配置babel7轉碼ES6
- Module語法與API的使用
- import()
- Module加載實現原理
- Commonjs規范的模塊與ES6模塊的差異
- ES6模塊與Nodejs模塊相互加載
- 模塊循環加載
一、webpack4打包配置babel7轉碼ES6
1.webpack.config.js
在webpack中配置babel轉碼其實就是在我之前的webpack配置解析基礎上添加babel的加載器,babel的具體轉碼配置在之前也有一篇詳細的博客做了解析:
在原本的webpack配置基礎上添加加載器(原配置詳細見:webpack安裝與核心插件):
//下載babel加載器:babel-loader npm install babel-loader --save-dev
然后關鍵的webpack.config.js配置代碼是下面這一段:
1 module.exports = { 2 ... 3 module:{ 4 rules:[ 5 { 6 test: /\.js$/, 7 loader:'babel-loader', 8 exclude: /node_modules/ 9 } 10 ] 11 } 12 ... 13 }
完整的webpack.config.js代碼在這里:
1 const path = require('path'); 2 const root = path.resolve(__dirname); 3 const htmlWebpackPlugin = require('html-webpack-plugin'); 4 5 module.exports = { 6 mode:'development', 7 entry:'./src/index.js', 8 output:{ 9 path: path.join(root, 'dist'), 10 filename: 'index.bundle.js' 11 }, 12 module:{ 13 rules:[ 14 { 15 test: /\.js$/, 16 loader:'babel-loader', 17 exclude: /node_modules/ 18 } 19 ] 20 }, 21 plugins:[ 22 new htmlWebpackPlugin({ 23 template:'./src/index.html', 24 filename:'index.html' 25 }) 26 ] 27 }
2. .babelrc轉碼配置以及package.json
1 //關於這個配置的具體解析可以到(ES6入門一:ES6簡介及Babel轉碼器)了解 2 { 3 "presets":["@babel/preset-env"], 4 "plugins": [ 5 ["@babel/plugin-proposal-class-properties",{"loose":true}], 6 ["@babel/plugin-proposal-private-methods",{"loose":true}] 7 ] 8 }
1 //包 2 { 3 "name": "demo", 4 "version": "1.0.0", 5 "description": "", 6 "main": "webpack.config.js", 7 "scripts": { 8 "test": "echo \"Error: no test specified\" && exit 1" 9 }, 10 "author": "", 11 "license": "ISC", 12 "devDependencies": { 13 "@babel/core": "^7.6.4", 14 "@babel/plugin-proposal-class-properties": "^7.5.5", 15 "@babel/plugin-proposal-private-methods": "^7.6.0", 16 "@babel/polyfill": "^7.6.0", 17 "@babel/preset-env": "^7.6.3", 18 "babel-loader": "^8.0.6", 19 "html-webpack-plugin": "^3.2.0", 20 "webpack": "^4.41.2" 21 } 22 }
3.工具區間及目錄解構:
//工作區間 src//文件夾 a.js//index.js依賴的模塊 index.js//入口文件 index.html//解構文件 .babelrc//babel轉碼配置 package.json//包 webpack.config.js//webpack打包配置文件
src中的代碼文件:
1 // 導出 2 3 export let num = 10; 4 export const str = "HELLO"; 5 export function show(){ 6 7 } 8 9 export class Node { 10 constructor(value, node = null){ 11 this.value = value; 12 this.node = node; 13 } 14 }
1 import {num, str, show, Node} from './a.js'; 2 3 console.log(num,str,show,Node);
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <meta http-equiv="X-UA-Compatible" content="ie=edge"> 7 <title>Document</title> 8 </head> 9 <body> 10 11 </body> 12 </html>
4.下載打包及轉碼必備的包:
npm install//也可以手動一個個下載npm install ... --save-dev
包全部下載完成以后,就可以執行打包指令了,這里我使用動態時時監聽自動打包(-w指令)
webpack -w --config ./webpack.config.js
5.這里插入一條:在visual Studio Code編輯器中的控制台在:View-Terminal(快捷鍵:Ctrl + ` ),這可以免去在開啟系統的控制台或者使用git的控制台。
准備工作做完以后就開始Module的語法分析了,由於前面已經解析了webpack和babel的使用,加上模塊發開發必然繞不開打包和轉碼這個環節,所以在這里補充了webpack的babel轉碼打包配置,上面的配置是最簡單的入門版,如果想深入了解webpack打包可以考慮讀一讀Vue-cli的源碼,后面我也會有Vue-cli源碼解析的博客,但是可能會要等一段時間。
二、Module語法與API的使用
在解析語法之前,強烈建議先閱讀這篇博客:js模塊化入門與commonjs解析與應用。通常作為ES6模塊化的相關內容都會先闡述為什么要實現模塊,這的確很重要,但是在commonjs規范的模塊化在ES6之前就已經出現了,可以說ES6模塊化是基於commonjs的模塊化思想實現的,但是commonjs也並非js模塊化思想的始祖。在這之前我們很多時候會使用閉包來實現一些功能,這些功能對外暴露一個API,比如jQuery就是在一個閉包內實現的,然后將jQuery賦給window的jQuery和$這兩個屬性,這種做法就是為了保證jQuery庫實現時其不會對全局產生污染。另外閉包也在一定程度上可以將程序分塊實現,但是閉包還是在一個js文件中實現的分塊,並不能像java那樣實現獨立的文件快。
這些都是為什么要實現模塊化的原因,在之前的“js模塊化入門與commonjs解析與應用”中有比較詳細的說明,順便也了解以下nodejs的模塊化的一些語法,這是有必要的。
1.ES6模塊化導出(export)導入(import)指令
【在之前的webpack打包及babel轉碼示例基礎上繼續語法演示,a.js作為index.js的依賴文件】
1 // a.js==>導出 2 export let num = 10; 3 export const str = "HELLO"; 4 export function show(){ 5 6 } 7 export class Node { 8 constructor(value, node = null){ 9 this.value = value; 10 this.node = node; 11 } 12 }
再在index.js中導入a.js導出的內容:
1 import {num, str, show, Node} from './a.js'; //導出語法:import {...} from ./a.js 2 console.log(num,str,show,Node);
對比nodejs的導出( module.exports )導入的和導出( require(url) )實現這很容易理解,因為nodejs是基於平台API和工具方法實現,而ES6是基於語言的底層編譯指令實現。
如果前面你使用了動態監聽打包指令,這時候只需要使用瀏覽器打開./dist/index.html你會發現控制台有了這樣的打印結果(如果關閉了就重新打包一次):
10 "HELLO" ƒ show() {} ƒ Node(value) { var node = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; _classCallCheck(this, Node); this.value = value; this.node = node; }
2.關於ES6導出的對象寫法:
相信前面你已經發現了一個問題,寫這么多export?ES6一樣可以直接使用一個大括號導出“{}”,比如前面的a.js導出可以這樣寫:
1 // a.js==>導出 2 let num = 10; 3 const str = "HELLO"; 4 function show(){ 5 6 } 7 class Node { 8 constructor(value, node = null){ 9 this.value = value; 10 this.node = node; 11 } 12 } 13 export {num, str, show, Node} //導出
3.導出語法大括號中“{}”不是解構賦值語法,比如導出之后直接給導出的元素執行一個寫入操作:
1 import {num, str, show, Node} from './a.js'; 2 3 num = 2;//Uncaught ReferenceError: num is not defined
我們看到了這並非解構賦值,因為解構賦值是將一個變量的值賦給另一個,執行了一個全新的變量聲明操作,但是import指令只負責導出對應模塊的內容接口,並不在當前模塊重新聲明變量,這也就意味着我們只能直接讀取導出的內容而不能寫入,那如果有需要寫入的操作怎么辦呢?
4.在導入模塊中給導出模塊的變量執行寫入操作:
1 //a.js導出模塊 2 let num = 10; 3 function show(){ 4 num = 2; 5 } 6 export {num, show} 7 //index.js導入模塊 8 import {num, show} from './a.js'; 9 10 console.log(num);//10 11 show(); 12 console.log(num);//2
關於這種數據操作方式,在實際項目開發中我們通常會在導出模塊中給變量定義get和set方法,然后將這兩個方法發用一個對象的方式導出。
5.導入重命名:
如果在導入模塊中出現了與導出模塊相同名稱,或者為了更加方便區別導入的內容,通常會需要將導出內容名稱通過as指令修改。
1 import {num as num_aModule, str as as_aModule,show as show_aModule, Node as Node_aModule} from './a.js'; 2 console.log(num_aModule);//10 3 show_aModule(); 4 console.log(num_aModule);//2
6.默認導出:
默認導出只能一次導出一個元素,且只能使用一次默認導出。默認導出可以導出模塊中的變量,甚至可以直接導出一個值,這個值導出不需要使用as重命名,而是在默認導入時直接給他一個名稱就可以了。
1 // a.js==>導出 2 let num = 10; 3 const str = "HELLO"; 4 function show(){ 5 num = 2; 6 } 7 class Node { 8 constructor(value, node = null){ 9 this.value = value; 10 this.node = node; 11 } 12 } 13 export default [1,2,3,4]; //默認導出 14 export {num, str, show, Node} 15 16 //index.js==>導入模塊 17 import arr from "./a.js"; //導入默認元素 18 import {num as num_aModule, str as as_aModule,show as show_aModule, Node as Node_aModule} from './a.js'; 19 console.log(num_aModule);//10 20 show_aModule(); 21 console.log(num_aModule);//2 22 23 console.log(arr);//[1, 2, 3, 4]
7.通配符(*)整體導出模塊內容:
import arr from "./a.js"; //導入默認元素 import * as a_Module from './a.js'; console.log(a_Module);//打印出一個導入模塊的內容對象,這個對象的元素只有get方法 console.log(a_Module.str);//HELLO
8.模塊繼承:
假設現在有一個模塊b.js,b繼承a.js模塊,並且index.js依賴b.js,直接在b.js模塊中使用export導出a模塊就可以了。
1 //b.js 2 export * from './a.js' 3 4 //index.js 5 import arr from "./a.js"; //導入默認元素,需要注意默認導出元素不會被繼承 6 import * as b_Module from './b.js'; 7 console.log(b_Module);//打印出一個導入模塊的內容對象,這個對象的元素只有get方法 8 console.log(b_Module.str);//HELLO 9 console.log(arr)
如果出現了b模塊自身也存在需要導出的元素,這時候就只能先將a模塊中的元素通過import導入進來,在使用export將a和b自身導出元素一起導出
1 // a.js==>導出 2 let num = 10; 3 const str = "HELLO"; 4 function show(){ 5 num = 2; 6 } 7 class Node { 8 constructor(value, node = null){ 9 this.value = value; 10 this.node = node; 11 } 12 } 13 export default [1,2,3,4]; //默認導出 14 export {num, str, show, Node} 15 16 //b.js模塊 17 import {num, str, show, Node} from "./a.js"; 18 let bstr = "biu"; 19 export {num, str, show, Node, bstr} 20 21 //index.js 22 import arr from "./a.js"; //導入默認元素 23 import * as b_Module from './b.js'; 24 console.log(b_Module);//打印出一個導入模塊的內容對象,這個對象的元素只有get方法 25 console.log(b_Module.str);//HELLO 26 console.log(b_Module.bstr);//biu 27 console.log(arr)// [1, 2, 3, 4]
三、import()
之前的內容一直沒有涉及一個問題,就是模塊是怎么加載的?既然ES6出現了模塊的標准API那是不是意味着瀏覽器可以自行加載解析模塊?它們又如何加載?
就目前來講其實我們很少關注ES6的模塊加載問題,畢竟ES6的模塊在瀏覽器的兼容還需要一些時間,現階段我們使用ES6的模塊都是通過轉碼打包,最后客戶端請求到的js文件都是基於編譯后的文件通篇加載,所以不管開發時基於多少模塊生成的js文件,都是在一個js文件中執行通篇加載同步執行,但是有時候我們總有一些業務需求想做成按需加載執行。
在HTML5中提供了有一種js異步執行機制worker,詳細可以了解HTML5之worker開啟JS多線程模式及window.postMessage跨域,這種機制嚴格來說是一種多線程的,在Promise那篇博客中我引用了《你不知道的js下篇》的相關內容闡述了異步與多線程的區別,有興趣可以了解以下。
而import()則是嚴格意義上的js異步機制,因為它的底層是基於Promise實現的,這里我就不詳細描述Promise的相關內容了,詳細可以了解:ES6入門八:Promise異步編程與模擬實現源碼,如果你了解了Promise你就可以瞬間明白import()的模塊異步導入原理,下面直接來看基於前面代碼index.js的異步導入b.js模塊的示例:
1 import arr from "./a.js"; //導入默認元素 2 import(`./b.js`).then((b_module) =>{ 3 console.log(b_module);//導入的b模塊對象最后被打印 4 },(err) => { 5 console.log(err); 6 }) 7 console.log("import是異步機制,所以我會先出現");//我第一個被打印 8 console.log(arr)// [1, 2, 3, 4],我第二個被打印
而對於import()的異步機制在webpack打包時並不會打包到生成到主index.js中,而是將import()中的`./b.js`打包成一個獨立的js文件,當import()被觸發時再發起一個請求獲取這個獨立js文件,所以這個js文件必然會是在主js文件執行完以后再執行。
關於import()方法有幾個值得注意的關鍵點:
1.通過import()方法導入的模塊不會打包到主js文件中。
2.只有import()方法被觸發時才會加載這個導入的模塊。
3.需要單獨發起一個網絡請求。
在這之前,如果我們需要異步加載一個js的話一般都是采用創建一個script元素,然后通過script的Element對象的src屬性發起一個網絡請求,這種方法有幾個閉端:第一、需要於文檔對象模型進行通信來創建元素對象,相比import()的性能肯定有很大的差別;第二、需要使用回調來來接兩個js任務,這顯然代碼的閱讀比模塊化導入導出要差。
除了創建script元素對象,還有就是前面的worker機制,這種機制是一種多線程機制,有獨立線程,通過線程的事件機制通信,這種操作相對模塊化也要復雜一些,但其兼容性是一個問題,不能像import()作為純js雖然ES6也還沒有特別好的兼容性,但import()可以通過轉碼工具降級來解決兼容性問題。
四、Module加載實現原理
1.瀏覽器加載機制:
為什么要了解瀏覽器的加載機制呢?我們知道模塊本身就是一個獨立的js文件,雖然瀏覽器兼容性不是很好,但是在部分新版本瀏覽器中也是可以通過script標簽來加載的。作為模塊機制它們是相互依賴的關系,而模塊必然需要與主js文件有所區分,這就得要依靠script標簽的type屬性來標識它們的類型,而相互依賴的關系有必然影響到執行順序的問題,我們知道網絡傳輸的因素會導致最后接收到的文件順序並不一定就是我們的請求順序,所以這一切我們都需要從瀏覽器的加載機制開始。
關於瀏覽器加載機制的詳細內容請移駕到這篇博客:瀏覽器加載解析渲染網頁原理,除了詳細的解析博客以外,這里簡單的回顧一些相關的關鍵內容,如果對瀏覽器的加載解析機制有一定了解,通過這些相關的內容就可以開始了解模塊的加載機制了。
1.1.同步加載模式
1 //內嵌腳本--同步解析執行,阻塞頁面 2 <script type="text/javascript"> 3 ... 4 </script> 5 //外部腳本--同步解析加載執行,阻塞頁面 6 <script type="text/javascript" src="path/to/index.js"> 7 ... 8 </script>
1.2.異步加載模式
1 //defer異步加載--等到頁面正常渲染完成以后執行 2 <script type="text/javascript" src="path/to/index.js" defer></script> 3 4 //async異步加載--加載完成以后就立即執行 5 <script type="text/javascript" src="path/to/index.js" async></script>
1.3.ES6模塊加載規則
//模塊加載(type值為module)--異步加載:默認等同於defer <script type="module" src="path/to/index.js"></script>
ES6模塊加載采用module的媒體類型,默認情況下為defer異步加載模式,等待頁面加載完成以后執行。
但模塊加載也可以為async異步加載模式,需要手動添加async屬性,設置async屬性以后模塊就會在加載完成以后立即執行。
//模塊async模式加載,手動添加async屬性 <script type="module" src="path/to/index.js" async></script>
2.import引入模塊及引入外部模塊的加載
ES6有了模塊化就必然有模塊引入,通過import引入的模塊與引入外部模塊腳本完全一致。
1 <script type="module"> 2 import utile from './utils.js'; //引入內部模塊 3 import outerUtile from 'https://example.com/js/utils.js' //引入外部模塊 4 </script>
引入的腳本必然是在引入指定所在的js腳本基礎上實現,所以引入腳本的加載也就不再考慮阻塞頁面這個問題,這是由引入腳本的加載執行方式決定的,但是ES6引入腳本必須是同步模式,引入指令是肯定會阻塞js的執行。這個很容易理解,因為引入其他模塊就代表着當前模塊必然要依賴其他模塊,如果依賴的模塊還沒加載執行就執行當前模塊,那當前模塊的依賴其他模塊的功能必然不能正常執行。
引入模塊除了同步執行以外,還有一些需要注意的問題:
2.1.每個模塊都有自己獨立的作用,模塊內部的變量外部不可見;(解決了命名沖突問題)
2.2.模塊默認自動采用嚴格模式;
2.3.加載外部模塊時不能省略‘.js’后綴;
2.4.模塊頂層對象指向undefined,而不是window;
2.5.同一個模塊加載多次,只執行一次。
五、Commonjs規范的模塊與ES6模塊的差異
ES6模塊在前面API的使用中展示了修改引入模塊的變量需要在依賴模塊中導出一個修改方法,這是因為ES6導出的變量是改變了的get方法,如果直接在引入模塊中做寫入操作會出現未聲明的錯誤。
但是Commonjs對於引入的數據直接進行寫操作並不會報錯,並且會寫入成功,但是這種寫入成功並不是正真的成功,只是對當前模塊引入的變量而言,因為Commonjs模塊引入機制是對導出模塊將變量合成一個對象然后引入模塊復制這個對象,所以會出現以下情況,來看下面這個nodejs的示例:
1 //導出模塊 a.js 2 let num = 5; 3 function foo(){ 4 num++; 5 } 6 module.exports = { //導出 7 num : num, 8 foo : foo 9 } 10 //引入模塊 11 let aModule = require('./a.js'); 12 console.log(aModule.num);//5 13 aModule.foo(); 14 console.log(aModule.num);//5
上面這種情況這是因為Commonjs導出的是一個對象,而這時候執行的foo方法內num則是在引入模塊中尋找這個變量,而不是在引入的對象中尋找這個變量,出給foo這種寫入的是一個"this.num++"才可以實現這個累加寫入的操作。
如果是在ES6中上面示例中的寫法就可以實現正常的累加寫入和讀取,前面API解析中已經有類似的代碼片段就不再重復展示。
總結上面這種情況ES6與Commonjs模塊的區別就是:
1.ES6導入的是對依賴模塊的數據只讀引用;
2.Commonjs導入的則是依賴模塊導出的一組數據鍵值對的復制操作。
六、ES6模塊與Nodejs模塊相互加載
1.使用ES6模塊語法import導入Nodejs模塊:
在Nodejs模塊中導出的方式是module.exports = {...},如果這個模塊被ES6模塊語法import導入,就相當於Nodejs默認導出一個對象(export default {...})。
1 //a.js 2 module.exports = { 3 foo:'hello', 4 bar:'world' 5 } 6 //如果a.js被ES6的import導入,上面的導出就相當於下面這種形式 7 //export default { 8 // foo:'hello', 9 // bar:'world' 10 //} 11 12 //index.js采用ES6語法導入a.js 13 import baz from './a.js';//baz = {foo:'hello',bar:'world'} 14 import {default as baz} from './a.js';////baz = {foo:'hello',bar:'world'}
需要注意一種情況是如果ES6模塊語法import使用了通配符導入nodejs模塊,這時候獲得的模塊數據引用對象中會包括nodejs整體變量get方法:get default()、同時還分別包括每個變量的get方法:get foo()、get bar()。例如上面的示例被通配符導出會是這樣的結果:、
1 import * as baz from './a.js'; 2 //baz ={ 3 // get default () {return module.exports;}, 4 // get foo() {return thisl.default.foo}.bind(baz), 5 // get bar() {return this.default.bar}.bind(baz) 6 //}
2.使用Nodejs的require方法加載ES6的模塊:
nodejs使用require()導入ES6模塊時,將ES6模塊導出的數據接口轉換成一個對象的屬性。(但是需要注意的是node並不直接支持ES6的模塊語法,所以如果需要在nodejs模塊中導入ES6的模塊,需要使用轉碼工具降級,降級操作前面的示例已經有展示,也可以參考這篇博客:ES6入門一:ES6簡介及Babel轉碼器)
1 //es6模塊 2 let num = 10; 3 const str = "HELLO"; 4 let arr = [1,2,3,4]; 5 function show(){ 6 num = 2; 7 } 8 class Node { 9 constructor(value, node = null){ 10 this.value = value; 11 this.node = node; 12 } 13 } 14 export default arr; //默認導出 15 export {num, str, show, Node}
1 { 2 "name": "Node_Module", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1" 8 }, 9 "keywords": [], 10 "author": "", 11 "license": "ISC", 12 "devDependencies": { 13 "@babel/cli": "^7.6.4", 14 "@babel/core": "^7.6.4", 15 "@babel/plugin-proposal-class-properties": "^7.5.5", 16 "@babel/plugin-proposal-decorators": "^7.6.0", 17 "@babel/plugin-proposal-private-methods": "^7.6.0", 18 "@babel/polyfill": "^7.6.0", 19 "@babel/preset-env": "^7.6.3" 20 } 21 }
1 //關於這個配置的具體解析可以到(ES6入門一:ES6簡介及Babel轉碼器)了解 2 { 3 "presets":["@babel/preset-env"], 4 "plugins": [ 5 ["@babel/plugin-proposal-class-properties",{"loose":true}], 6 ["@babel/plugin-proposal-private-methods",{"loose":true}] 7 ] 8 }
npx babel a.js -o aa.js//將es6模塊語法降級生成aa.js文件
index.js:nodejs模塊
1 let aModule = require('./aa.js'); //導入降級后的es6模塊 2 console.log(aModule);
在nodejs環境下執行index.js文件
node index.js
打印結果:
{ show: [Function: show], default: [ 1, 2, 3, 4 ], num: 10, str: 'HELLO', Node: [Function: Node] }
七、模塊循環加載
循環加載是指a模塊執行依賴b模塊,而b模塊又依賴a模塊。雖然這種極端的強耦合一般不存在,但是在復雜的項目中可能出現a依賴b,b依賴c,然后c依賴a的情況。而且ES6模塊與Commonjs模塊的加載原理存在差異,兩者是有區別的。在解析區別之前我想強調一點,無論是ES6模塊還是Commonjs模塊都只加載一遍。
1.commonjs模塊的加載模式
1 //a.js 2 exports.done = false; 3 var b = require('./b.js'); 4 console.log("在a.js中導入b.js",b.done); 5 exports.done = true; 6 console.log('a.js執行完畢'); 7 8 //b.js 9 exports.done = false; 10 var b = require('./a.js'); 11 console.log("在b.js中導入a.js",b.done); 12 exports.done = true; 13 console.log('b.js執行完畢');
在node環境中執行a.js,打印以下結果:
在b.js中導入a.js false b.js執行完畢 在a.js中導入b.js true a.js執行完畢
從打印結果看到的差別就是a導入b時,b執行又導入a,這時候a導入給b的done是一個初始值,並不是a模塊最終導出的結果。這是因為a沒有執行完,所以導出了一個初始值,但是a會等待b執行完再執行導入指令后面的程序。這里有個很關鍵的注意點就是每個模塊只執行一次,因為b導入a時,a已經執行就不會再次執行,而是基於a當前的執行結果導入a的數據。
2.ES6模塊的加載模式
1 //a.js 2 import {bar} from './b.js'; 3 console.log('./a.js'); 4 console.log(bar); 5 export let foo = 'foo'; 6 7 //b.js 8 import {foo} from './a.js'; 9 console.log('b.js'); 10 console.log(foo); 11 export let bar = 'bar';
注意這里如果直接使用babel轉碼的話需要將轉碼后的代碼中的引入文件名改成對應的轉碼后的文件名稱,然后再在node環境中執行轉碼后生成的文件。當然你也可以使用打包工具導出到一個html文件中測試。
//打印結果 b.js undefined ./a.js bar
ES6模塊的執行方式與Commonjs的執行方式有很大的區別,重點就是在於模塊沒有執行完成就不能獲取該模塊中的導出數據,在示例中當a執行時,就立即導入b,然后b執行。這時候b又導入了a。當b導入a時,a還沒有執行完,所以打印除了undefined。
這里你可能會有點迷惑,即使代碼改成下面這種情況也還是同樣的打印結果:
1 //a.js 2 import {bar} from './b.js'; 3 export let foo = 'foo'; 4 console.log('./a.js'); 5 console.log(bar); 6 7 //b.js 8 import {foo} from './a.js'; 9 export let bar = 'bar'; 10 console.log('b.js'); 11 console.log(foo);
ES6的這種情況是由於加載模塊本質上是生成一個模塊引用給模塊導入的位置,而這個引用必須是文件全部執行完成以后才會生成,所以在模塊執行完成以前引用ES6模塊中導出的數據都是undefined。
注:ES6模塊標准並不是由T39標准組織制定,而是由一個獨立的一個組織來制定的,相關標准和意見可以到這個連接查看: https://whatwg.github.io/loader/
