前言
起因是因為一次來回引入引發(fā)的問題葵诈。例如A引入了B的方法,B引入了A的方法掌腰,然后在調(diào)用時發(fā)現(xiàn)遇到了問題,于是決定做一篇關(guān)于模塊引入的整理张吉。
Module語法
歷史上齿梁,JavaScript 一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件肮蛹,再用簡單的方法拼裝起來勺择。
在 ES6 之前,社區(qū)制定了一些模塊加載方案伦忠,最主要的有 CommonJS 和 AMD 兩種省核。前者用于服務(wù)器,后者用于瀏覽器昆码。ES6 在語言標準的層面上气忠,實現(xiàn)了模塊功能邻储,而且實現(xiàn)得相當簡單,完全可以取代 CommonJS 和 AMD 規(guī)范旧噪,成為瀏覽器和服務(wù)器通用的模塊解決方案吨娜。
ES6 模塊的設(shè)計思想是盡量的靜態(tài)化,使得編譯時就能確定模塊的依賴關(guān)系淘钟,以及輸入和輸出的變量宦赠。CommonJS 和 AMD 模塊,都只能在運行時確定這些東西米母。比如袱瓮,CommonJS 模塊就是對象,輸入時必須查找對象屬性爱咬。
require和import本質(zhì)上就是commonjs和ES6module的區(qū)別尺借。一個是JavaScript社區(qū)指定的野生規(guī)范,一個是后續(xù)指定的官方規(guī)范精拟。
// 常規(guī)用法
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// ES6模塊
import { stat, exists, readFile } from 'fs';
import的一些注意事項
import命令輸入的變量都是只讀的燎斩,因為它的本質(zhì)是輸入接口。也就是說蜂绎,不允許在加載模塊的腳本里面栅表,改寫接口。
// 報錯
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是靜態(tài)執(zhí)行师枣,所以不能使用表達式和變量怪瓶,這些只有在運行時才能得到結(jié)果的語法結(jié)構(gòu)。
因此也不存在著頁面根據(jù)條件判斷去import資源這種說法践美。
import會執(zhí)行所加載模塊洗贰。例如:import 'lodash'
,僅僅執(zhí)行l(wèi)odash模塊陨倡,但是不輸入任何值敛滋。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
目前階段,通過 Babel 轉(zhuǎn)碼兴革,CommonJS 模塊的require命令和 ES6 模塊的import命令绎晃,可以寫在同一個模塊里面,但是最好不要這樣做杂曲。因為import在靜態(tài)解析階段執(zhí)行庶艾,所以它是一個模塊之中最早執(zhí)行的。下面的代碼可能不會得到預(yù)期結(jié)果擎勘。
關(guān)于import()函數(shù)咱揍,其誕生的目的是為了讓import也有動態(tài)加載的能力,實現(xiàn)require先前擁有的基本功能货抄。
關(guān)于require和import本質(zhì)區(qū)別
寸志:形式上看上去五花八門述召,本質(zhì)可以涵蓋為以下三點朱转。
- CommonJS 還是 ES6 Module 輸出都可以看成是一個具備多個屬性或者方法的對象蟹地;
- default 是 ES6 Module 所獨有的關(guān)鍵字积暖,export default fs 輸出默認的接口對象,import fs from 'fs' 可直接導(dǎo)入這個對象怪与;
- ES6 Module 中導(dǎo)入模塊的屬性或者方法是強綁定的夺刑,包括基礎(chǔ)類型;而 CommonJS 則是普通的值傳遞或者引用傳遞分别。
第3點具體該如何理解遍愿?
// counter.js
exports.count = 0
setTimeout(function () { ++exports.count }, 500)
// commonjs.js
const {count} = require('./counter')
setTimeout(function () {
console.log('read count after 1000ms in commonjs is', count)
}, 1000)
// 輸出結(jié)果:read count after 1000ms in commonjs is 0
//es6.js
import {count} from './counter'
setTimeout(function () {
console.log('read count after 1000ms in es6 is', count)
}, 1000)
// 輸出結(jié)果:read count after 1000ms in es6 is 1
count為number類型,因此commonjs中是值傳遞耘斩,傳遞0之后后續(xù)原來的參數(shù)發(fā)生變化并不會影響到引用方的count值沼填。而es6中為強綁定,因此后續(xù)也跟著發(fā)生了改變括授。
關(guān)于循環(huán)引用
解答這個問題需要先了解commonjs和es6底層的加載原理
commonjs
CommonJS 的一個模塊坞笙,就是一個腳本文件。require命令第一次加載該腳本荚虚,就會執(zhí)行整個腳本薛夜,然后在內(nèi)存生成一個對象。
{
id: '...', // 模塊名
exports: { ... }, // 模塊輸出的各個接口
loaded: true, // 表示是否執(zhí)行完畢
...
}
執(zhí)行一次后版述,再次執(zhí)行只會去exports上取值梯澜,返回第一次運行的結(jié)果。
CommonJS 模塊遇到循環(huán)加載時渴析,返回的是當前已經(jīng)執(zhí)行的部分的值晚伙,而不是代碼全部執(zhí)行后的值,兩者可能會有差異俭茧。所以撬腾,輸入變量的時候,必須非常小心恢恼。
es6
ES6 模塊是動態(tài)引用民傻,如果使用import從一個模塊加載變量(即import foo from 'foo'),那些變量不會被緩存场斑,而是成為一個指向被加載模塊的引用漓踢,需要開發(fā)者自己保證,真正取值的時候能夠取到值漏隐。
// 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';
直接拋出這個典型的例子喧半。執(zhí)行a.mjs后,引用b青责,已知它從a輸入了foo接口挺据,這時不會去執(zhí)行a取具,而是認為這個接口已經(jīng)存在了。因此執(zhí)行到第三行時發(fā)現(xiàn)foo并沒有定義ReferenceError扁耐。
可以通過將bar暇检,foo寫成函數(shù)的形式,這是因為函數(shù)具有提升作用婉称,在執(zhí)行import {bar} from './b'
時块仆,函數(shù)foo就已經(jīng)有定義了,所以b.mjs加載的時候不會報錯王暗。這也意味著悔据,如果把函數(shù)foo改寫成函數(shù)表達式,也會報錯俗壹。
心得體會
關(guān)于import的討論科汗,先前分析過它預(yù)加載、動態(tài)加載的行為绷雏。本次熟悉了語法用法和加載的機制头滔,盡量通過函數(shù)的形式去返回可能存在循環(huán)調(diào)用的模塊,在發(fā)生循環(huán)調(diào)用時也要尤為警惕避免產(chǎn)生不可預(yù)見的bug之众。
參考
Module 的語法 - 阮老師的es6教程
Module 的加載實現(xiàn) - 阮老師的es6教程
require拙毫,import區(qū)別?-知乎