es6 之 import, export 以及 commonjs和es6的循環依賴比較

named import 和 default import

1、現象

在給公共業務組件單獨打包的時候,碰到一個須要export 2個mixin和一個報錯函數的場景。當時就直接這麼些寫的。
// common/index.js
// 初始化SelectMixin
    let selectUrl = '/example/common/getSelectBykeys'
    let setSelectUrlPrefix = () => {}
    
    const SelectMixin = select(selectUrl, setSelectUrlPrefix)
    
// 初始化AuthProviderMixin
    let authUrl = '/example/common/authUrl'
    let transferAuthResult = () => {}
    const AuthProviderMixin = authProvider(authUrl, transferAuthResult)
    
// 初始化request
    let handleRequestError = () => {}
    const RequestUtil = request(handleRequestError)
    
    export default {
        SelectMixin,
        AuthProviderMixin,
        RequestUtil
    }

複製代碼
而後在一個頁面引入SelectMixin,代碼以下
import { SelectMixin } from '../common/index.js'
複製代碼
結果是提示
"export 'SelectMixin' was not found in '../common/index.js' 複製代碼
在控制檯輸出SelectMixin的時候也是undefined

改爲javascript

import SelectMixin from '../common/index.js'
複製代碼
的確是有值,可是輸出後發現,值的內容以下

image


那麼問題來了。

2、爲何能獲取到這個導出的對象卻沒法解構到想要到值。

一篇簡書上看到這麼說的。
export default {
    SelectMixin,
    AuthProviderMixin,
    RequestUtil
}
複製代碼
通過webpack 和 babel 轉換成了
module.exports.default = {
    SelectMixin,
    AuthProviderMixin,
    RequestUtil
}
複製代碼

因此 SelectMixin 取不到值是正常的。瞬間感受他說的頗有道理的樣子。結果後面就沒了後來。真是一頓操做猛如虎。。。

後來一大佬過來了,他能懂。。。他說這是規範,就像平時的小括號和函數裏的小括號同樣同樣的。好像是這麼回事哈。。頂禮膜拜。。。

不過最後文章中還提到了一句

import 語句中的"解構賦值"並非解構賦值,而是 named imports,語法上和解構賦值很像,但仍是有所差異html

這裏我就直接去Google 「import語法上和解構賦值的差異 」,另外一名大佬就看到了 named imports ,這差距從小學語文就能看出來了。。扎心了。


3、named imports

在說named imported 以前先看看常常會碰到的下面的代碼。這是在這個文件裏寫了3個函數而後導出供其餘文件使用。而咱們使用的時候則如右圖。

image

小問題: 這裏引用 defaultExport 這個js 文件必定要這麼寫嗎?

上面的問題能夠想着先,而後咱們來看看下面這幾個知識點以後再回來講這個問題。 上面的代碼用了 export default ,而對應的import的在這個時候被稱做 default import。他們是成對使用的。因此用了 export default 則必定要用到 default import。這裏須要記住的知識點有如下幾點。java

3.1 default exports 和 default imports

// this is default export
// A.js 
export default 42
複製代碼
// this is default import
// B.js
import A from './A'
複製代碼
  1. 一個模塊只能有一個default exportsnode

  2. default imports 只對 default export 有用, default exports 須要用 default imports 去獲取。webpack

  3. 在 default imports 中,導出的時候,能夠爲其任意命名,由於 default export 是匿名的。在下面的例子裏,A, MyA, Something 都是內容相同的。只是其名字不一樣而已,這個語法就是獲取./A值的同時,爲其匿名的對象取個名字git

import A from './A'
import MyA from './A'
import Something from './A'
複製代碼

這裏第3點回答了上面的一個問題。這種導出的時候,是匿名的,因此引入的時候的import後面接的是爲這個匿名的對象取的一個名字,這個名字是任意的。因此不用必定要取文件的名字。

再回來看第2點。default imports 只對 default export 有用, default exports 須要用 default imports 去獲取。這一句解釋了在文中一開始提到問題。

第一點:一個模塊只能有一個default exports在一個文件裏寫多個export default 是錯誤的。會報錯的。規則是不容許的。就記住匿名導出每一個文件有且只能有一個。固然,不用匿名導出也是能夠的。export default關鍵詞後面能夠跟任何值:一個函數、一個類、一個對象,全部能被命名的變量

3.2 named exports 和 named imports

接下來咱們再來看看這兩段代碼。

image

小問題: 這裏引用 namedExport 這個js 文件必定要這麼寫嗎?

上面的問題能夠想着先。而後咱們來看看下面這幾個知識點以後再回來講這個問題。上面的代碼用了 named default ,而對應的import的在這個時候被稱做 named import。他們是成對使用的。因此用了 named default 則必定要用到 named import。這裏須要記住的知識點有如下幾點。es6

// this is named export
// A.js 
export const A = 42
複製代碼
// this is named import
// B.js
import { A } from './A'
複製代碼
  1. 一個模塊能夠有多個 named exports
  2. named imports 只對 named export 有用, named exports 須要用 named imports 去獲取
  3. 這裏沒法像default import 同樣,給導出的對象任意取名,須要一一對應。固然可也提供了給 named import其餘寫法,給named export 從新命名的機會。
// B.js
import { A } from './A'
import { myA } from './A' // Doesn't work! import { Something } from './A' // Doesn't work!
複製代碼
  • 可是一個模塊導出多個named exports的時候,能夠像上面那般import,不過也能夠像解構同樣,寫在一塊兒以下面這樣。
// A.js
export const A = 42
export const myA = 43
export const Something = 44
複製代碼
// B.js
import { A, myA, Something } from './A'
複製代碼

這裏第3點回答了上面的一個問題。這種導出的時候,是具名的,因此要按名字去解析對應的導出。不過這種named import 給了兩種其餘的引入方式,能夠爲命名的函數從新修更名字。也可把因此具名模塊合成一個對象使用。

再回來看第2點。named imports 只對 named export 有用, named exports 須要用 named imports 去獲取。這一句解釋了在文中一開始提到問題。因此對於named export 只能用named import。

你能夠export任何的頂級變量:function、class、var、let、const。

另外還發現一個有意思的,忍不住想飆一句英文: amazing ,default export 和 named export 能夠混合使用,default import 和 named import 。

image

在es6解構裏能夠用冒號爲解構的變量重命名,在named import裏也能夠,使用的是as 。上面的代碼能夠這麼寫。
// B.js
import anyThing, { myA as myX, Something as XSomething } from './A'
複製代碼

補充

咱們能夠把 Default export 當成一個特殊的 named export ,其實default export 也能夠像這樣來解析。只是他默認是叫default的一個對象。切默承認以有任意一個名字去覆蓋他的匿名。github

import { default as anything } from './A'
複製代碼

不過這樣寫也是不被容許的,畢竟default 是一個保留字段。系統會直接報錯,不過講道理,拋去這個保留字段問題,這個寫法按道理也能獲取到default exportweb

import { default } from './A'
複製代碼

4、延伸--模塊的循環引用

在瞭解named import 過程當中碰到一個有意思的點就是 模塊點循環引用。文獻裏說到es6 對循環引用支持比CommonJs更好。特對這個進行了一番瞭解。segmentfault

循環引用

"循環引用"(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。

一般,」循環引用"表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。

可是實際上,這是很難避免的,尤爲是依賴關係複雜的大項目,很容易出現a依賴b,b依賴c,c又依賴a這樣的狀況。這意味着,模塊加載機制必須考慮」循環引用」的狀況。即便在設計初期經過很好的結構設計避免了,可是代碼一重構,」循環引用」仍是很容易就出現的。因此」循環引用」的狀況是不可能避免的。

4.1 CommonJS模塊的加載原理

這裏不討論其餘靜態文件只考慮腳本文件,CommonJS的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。以下:

{
    id: '',
    exports: '',
    parent: '',
    filename: null,
    loader: false,
    children: [],
    paths: ''
    // ...
}
複製代碼

這個對象裏,id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,省略。 具體能夠去看 www.ruanyifeng.com/blog/2015/1… 也能夠參考node源碼 github.com/nodejs/node…

當代碼用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。

咱們先來看一個例子,考慮一下答案。

images
這例子出自node官網,能夠移步此處 nodejs.org/api/modules…

不說答案,咱們直接說這個在運行c.js時當過程。上邊代碼之中,c.js先引入了a.js。則按照required的原理,會執行a.js整個腳本。

a.js腳本先輸出一個done變量,而後加載另外一個腳本文件b.js。注意,此時a.js代碼就停在這裏,等待b.js執行完畢,再往下執行。 再看b.js的代碼

b.js執行到第二行,就會去加載a.js,這時,就發生了」循環引用」。(CommonJs的循環引用的重要原則:一旦出現某個模塊被」循環引用」,就只輸出已經執行的部分,還未執行的部分不會輸出。) 這裏有得小夥伴會認爲是去執行a.js以前沒執行完的代碼。可是規則不是這麼定義的。由於a.js觸發了循環引用,則a.js會返回已經執行的部分代碼。

系統會去a.js模塊對應對象的exports屬性取值,a.js雖然尚未執行完,可是其exports裏確實有值的,從exports屬性取回已經執行的部分的值,而不是最後的值。 a.js已經執行的部分,只有一行,即 exports.done = false;

因此對於b.js來講,引入的a.js值爲false。而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.js接着往下執行,直到執行完畢。

c.js就第一句就將a.js和b.js所有加載完畢了。a.js和b.js最終返回的都是true。

因此最終會選擇答案B。

上面是commonjs的循環引用的原理。接下來咱們再來看另一個例子。

images
例子出自此: exploringjs.com/es6/ch_modu…

執行a.js 以後的結果是右邊兩種狀況。這個比前面一個例子好理解。咱們先來看一下es6 modules 的加載原理。

4.2 es6 modules 的加載原理

ES6模塊的運行機制與CommonJS不同,它遇到模塊加載命令import時,不會去執行模塊,而是隻生成一個引用。 等到真的須要用到時,再到模塊裏面去取值。

所以,ES6模塊是動態引用,不存在緩存值的問題,並且模塊裏面的變量,綁定其所在的模塊。

那再看上面的例子。a.js之因此可以執行,緣由就在於ES6加載的變量,都是動態引用其所在的模塊。只要引用是存在的,代碼就能執行。

若是按照CommonJS規範,上面的代碼是無法執行的。a先加載b,而後b又加載a,這時a尚未任何執行結果,因此輸出結果爲null,即對於b.js來講,變量foo的值等於null,後面的foo()就會報錯。

4.3 CommonJs補充

在commonJs裏常常會看到exports 和 module.exports 。這個會比較混淆。

CommonJS定義的模塊分爲: 模塊標識(module)、模塊定義(exports) 、模塊引用(require)。

在一個node執行一個文件時,會給這個文件內生成一個 exports和module對象, 而module又有一個exports屬性。他們之間的關係以下圖,都指向一塊{}內存區域。

images

再看個例子

從上面能夠看出,其實require導出的內容是module.exports的指向的內存塊內容,並非exports的。

簡而言之,區分他們之間的區別就是 exports 只是 module.exports的引用,輔助後者添加內容用的。


5、因此兩種循環引用的關鍵仍是在對模塊引用時的處理方式不一樣。

5.1 commonJs

  • 對於基本數據類型,屬於複製。即會被模塊緩存。同時,在另外一個模塊能夠對該模塊輸出的變量從新賦值。

  • 對於複雜數據類型,屬於淺拷貝。因爲兩個模塊引用的對象指向同一個內存空間,所以對該模塊的值作修改時會影響另外一個模塊。

  • 當使用require命令加載某個模塊時,就會運行整個模塊的代碼。

  • 當使用require命令加載同一個模塊時,不會再執行該模塊,而是取到緩存之中的值。也就是說,CommonJS模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。

  • 循環加載時,屬於加載時執行。即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。

5.2 Es6

  • ES6模塊中的值屬於【動態只讀引用】。

  • 對於只讀來講,即不容許修改引入變量的值,import的變量是隻讀的,不管是基本數據類型仍是複雜數據類型。當模塊遇到import命令時,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。

  • 對於動態來講,原始值發生變化,import加載的值也會發生變化。不管是基本數據類型仍是複雜數據類型。

  • 循環加載時,ES6模塊是動態引用。只要兩個模塊之間存在某個引用,代碼就可以執行。

參考

www.jianshu.com/p/ba6f582d5…

stackoverflow.com/questions/3…

hackernoon.com/import-expo…

2ality.com/2014/09/es6… exploringjs.com/es6/ch_modu…

www.cnblogs.com/unclekeith/…

www.ruanyifeng.com/blog/2015/1…

www.ruanyifeng.com/blog/2015/0…

github.com/nodejs/node…

segmentfault.com/a/119000001…

zhuanlan.zhihu.com/p/27159745

相關文章
相關標籤/搜索