核心
其實總的來說就是 export 導出的是變量的句柄(或者說符號綁定、近似於 C 語言裏面的指針,C++裏面的變量別名),而 export default 導出的是變量的值。
需要注意的是:模塊裏面的內容只能在模塊內部修改,模塊外部只能使用。esModule在語法層面做了一層淺層
的保護(即將import導入的變量聲明爲常量)
而變量的句柄必須通過 var、let、const、function 這些關鍵字聲明纔可以由 js 引擎生成,而值(或者說數據)可以通過變量運算或者字面量直接生成。
下面是測試用例:
// a.js export let a = 'a' export let objA = { a: 'a' } let defaultA = 1 export default defaultA export function fn(str) { a = str defaultA = str }
// test1.js import b, { a, fn, objA } from './a.js' console.log(a, '---', b, '---', objA.a, '---', 'test1.js') setTimeout(() => { objA.a = 'hello world' fn('hello world') console.log(a, '---', b, '---', objA.a, '---', 'test1.js') })
// test2.js import b, { a, objA } from './a.js' console.log(a, '---', b, '---', objA.a, '---', 'test2.js') setTimeout(() => { console.log(a, '---', b, '---', objA.a, '---', 'test2.js') }, 100)
// main.js import './test1.js' import './test2.js'
運行main.js
,輸出結果如下:
分析
- 通過
a
值的變化可以看出,在test1.js
中的修改會影響到test2.js
中a
的值,驗證我們說的導出句柄這個觀點。 - 通過
b
的運行結果可以驗證export default
導出變量的值的觀點。 - 通過
objA.a
的運行結果可以驗證淺層保護的觀點,其實和const obj = {}
,我們可以修改obj
的屬性,只要不對obj
重新賦值都是允許的是同一個邏輯。
如果看到這裏你完全理解上面的內容,那麼下面的內容就建議你跳過了,因爲下面是一些細節的展開和補充,對你來說可能會有些囉嗦和浪費時間。如果上面的內容你還不是很理解,那麼可以再看看下面的內容,看看是否對你有幫助。
那我們就按照以下幾個方面具體來講講這個問題,順便再做一些擴展和補充。
- 1、句柄和值
- 2、ES module 和 commonJs
- 3、關於 Tree Shaking 的思考
句柄和值
其實句柄這個詞我個人理解爲權限,獲得句柄就是獲得某種東西的操作權限,比如拿到文件句柄就可以對文件進行讀寫操作。其實怎麼理解都可以,只不過我引用了句柄這個詞語。我想說明的是 export 導出的是一個變量的句柄(或者說是引用),這個概念類似於 C 語言裏面的指針,C++裏面的變量別名。也就是說,導入模塊在拿到這個變量時,對這個變量的操作實際上是在操作原來的導出變量本身。
而值其實就是一份數據,也可以理解成 export default 導出的是一份數據拷貝。
擴展
一、js 中聲明變量的幾種方式
- var、let、const
- function
- class
- import(準確來講並沒有創建新的變量,但是這個關鍵字導入了被導入模塊的變量的引用,而在 js 引擎層面並沒有聲明新的變量)
注意:
// main.js export { default as a } from 'xxx/a.xxx'
這種情況下,a 這個變量在 main.js 這個模塊中是訪問不到的。如果想要在 main.js 這個模塊中訪問到 a 模塊,需要使用 import 語句進行導入,再使用 export 暴露給外界。
// main.js import a from 'xxx/a.xxx' export a
二、堆棧內存
- 堆內存:存放引用類型的數據,例如對象、數組等
- 棧內存:存放基本數據類型和引用類型的地址(存放佔用空間固定的數據)
ES module 和 CommonJS
1、實現層面
ES module 和 CommonJS 比較大的一個區別就是一個是官方規範,一個是社區規範。官方規範自然就能的到 js 語法層面的實現支持,而社區規範只能通過在現有的語法基礎上進行擴展來實現。
2、單獨導出和默認導出
其實 CommonJS 的實現也特別簡單,看一眼 webpack 的打包結果就知道了。核心原理就是將一個個模塊放到函數中運行,這樣利用函數作用域的特點,就可以實現模塊之間的環境隔離。所以在 CommonJS 中,module.exports 和 exports 本質上就是同一個對象,這個對象就是這個模塊(函數)運行時 return 的對象。
而 ES module 則不然,export 和 export default 有着本質的差別,那就是一個導出變量的句柄,一個導出變量的值。
擴展(關於 export 導出的細節)
關於 export 導出,出了這種下面常用的方式:
// a.js export let a = 1
還有一種方式:
// b.js let b = 1 export { b }
而這兩種模塊的導入方式都是一樣:
import { a } from 'xx/a.js'
import { b } from 'xx/b.js'
既然前面說了,export 導出的是變量的句柄,那麼顯然下面這種方式是要報錯的:
// b.js export { b: 1 } // SyntaxError: Unexpected token ':'
因爲導入方式一樣,那麼很自然的我就想測試一下,我按照下面這種方式來測試一下看會不會產生衝突
let b = 1 export { b } export let b = 2 // SyntaxError: Identifier 'b' has already been declared
很顯然使用 let、const 這樣的關鍵字會產生一個重複定義的衝突,那麼我們再試一下另外一個可以讓我們多次重複聲明同一個變量的 var 關鍵字。
var b = 1 export { b } export var b = 2 // SyntaxError: Duplicate export of 'b'
改成 var 之後,不會在一開始編輯器就提示我們錯誤了,而是在運行時,報一個重複導出的錯誤。所以通過測試,這兩種 export 導出方式還是不會產生衝突的。
3、動態導入
CommonJS 動態導入就很簡單,其實就是運行函數。其實 CommonJS 導入本身就是在運行函數,所以動態或者靜態其實都一樣。
const a = require('xxx/a.js')
ES module 動態導入,那就需要語法的支持,使用下面這種語法:
const a = await import('xxx/a.js')
關於 Tree Shaking 的思考
我們知道,ES module 是支持 Tree Shaking 的,但是 CommonJS 是不支持的。
其實 Tree Shaking 面臨的核心困難就是怎麼確定一個函數或者模塊它是否包含副作用。如果寫的都是純函數,那麼 Tree Shaking 其實是很實現的。那麼像有些函數,在編譯時直接可以運行函數得到調用結果,進而在生產運行時,直接省去求值的耗時。
所以 Tree Shaking 的核心是在於副作用的檢測,特別是在複雜的模塊引用關係裏面,確定每個模塊裏的某些內容是否存在副作用。另外爲了更好的 Tree Shaking,比較推薦的方案是使用 ES module,並且使用 export 導出,這方式可以更好的進行 Tree Shaking。