從零開始學_JavaScript_系列(67)——es6的import和export(chrome61版本後可用)
1、概述
1.1、module是什麼
俗稱【模組】。
簡單來說,假如我寫了三個功能,每個功能1000行程式碼。
如果放在一個js檔案裡,那就是3000行程式碼;
如果每個功能放在一個js檔案裡,那就是各1000行程式碼。
在往常情況,我們可能需要在html檔案裡,通過script標籤來載入這三個js檔案。但這個帶來幾個問題:
- 必須依賴html檔案;
- 沒有辦法做到按需載入(除非你用例如
document.write
或者建立新的DOM標籤,之類的方法寫入標籤); - 不能做到js檔案對js檔案的依賴關係;
- 不能做到只暴露指定介面(可以通過立即執行函式返回一個物件,來變相實現,但是很麻煩);
- 會汙染全域性變數;
- 不方便;
為了解決以上問題,有一些通行的規範,比如CommonJS(被Node使用,一般用於伺服器端)、AMD規範等(一般用於瀏覽器端)。而ES6也推出了自己的規範。
1.2、AMD、CMD、CommonJS
我將之前原本寫在這裡的內容單獨截取出來了,參照上面。
1.3、es6的模組化
es6的模組化設計思想有以下特點:
- 靜態化,載入哪些,可以在編譯的時候就確定,而不是隻有當執行的時候才確定;
- 編譯的時候就能確認依賴關係,這樣好優化(缺點是某些模組不確定需不需要用,非同步載入需要另外實現,很麻煩);
- 通過
export
顯式的指定匯出的內容,避免全域性汙染,例如讓模組內部的一些東西影響到其他模組; - 模組內部的作用域是獨立的,不同模組即使有同名變數,只要該變數不影響外部,那麼也是互不干擾的;
另外,es6的模組內部,自動啟用【嚴格模式】。
2、import和export
2.1、準備工作
由於瀏覽器還不支援import和export(Chrome61版本和之後支援),本文成文時間是61版本之前,因此需要轉碼。
然後從我的github上down下來這個資料夾下的東西:
- 先全域性安裝轉碼工具;
- 然後再修改入口檔案
foo.js
,以及他引用的檔案的程式碼; - 並執行
npm run test
完成轉碼; - 開啟
test.html
檢視效果。
每次做完後,重複2、3、4步流程即可開始下一次
2.2、export
export
用於規定模組的對外介面。
通俗的說,只有通過這個方式暴露的才可以被外界訪問。而其他的,相對於外界是獨立的(不會造成影響,除非你使用全域性變數)。
關於export
匯出模組內部的方法:
首先,因為瀏覽器不支援,因此只能轉碼執行。因此,不排除在某種情況下,某些寫法會出問題的情況,如果遇見報錯,建議換種方法試試。
其次,2.3再講import
的注意點,這裡只會簡單提一下。
最後,程式碼要編譯轉碼後才能執行,切記載入轉碼後的結果。
1、聲明後直接匯出的寫法:
// bar.js
export const myFirstName = 'Dong';
export let myLastName = 'Wang'
// foo.js
import {myFirstName} from './bar.js'
import {myLastName} from './bar.js'
console.log(`My name is ${myFirstName} ${myLastName}`)
引入的時候,需要引入的變數名和匯出的變數名一致才可以,比如foo.js裡的{myFirstName}
和bar.js裡的myFirstName
是一樣的。不然是無法正確被import進來。
注意:匯出的無論是變數還是常量,都不允許在foo.js裡修改,原因是因為引用的,具體檢視下面的注意點【1】。如果修改的話,編譯時會出錯的。
如果連續匯出剛宣告的變數,可以這麼幹:(不過我個人覺得這種寫法一點都不好看,並且他們的宣告方式要一致才行。)
export const myFirstName = 'Dong', myLastName = ''
// 相當於
export const myFirstName = 'Dong';
export const myLastName = 'Wang';
2、先聲明後匯出的寫法:
// bar.js
const myFirstName = 'Dong';
let myLastName = 'Wang';
export {myFirstName, myLastName}
// foo.js 和上面保持一致
被匯出的變數,用大括號包裹起來,注意有這種並不是es6物件的簡潔寫法,請不要混淆了。(具體參照下面的注意點【3】)
3、用其他名字匯出變數/常量/函式:
標準寫法是:
export { 模組內變數名1 as 匯出名字1, 模組內變數名2 as 匯出名字2 }
但經測試,我上面發的那個轉碼的外掛,是不能正確的轉碼這種方式的,但這種方式按照MDN和stack overflow上別人的回答,是正確的(見本節開始給的連結)。
因此,如果為了穩妥起見,建議規避這種方法。
// bar.js
const firstName = 'Dong';
let lastName = 'Wang';
export {firstName as myFirstName, lastName as myLastName}
4、匯出函式/類:
和匯出變數/常量的方法是一樣的,唯一需要注意的是,假如函式是宣告式的,那麼匯出他需要注意一下方法。
引入的檔案foo.js程式碼如下,如無特殊宣告,那麼只使用這個。
// foo.js
import {log} from './bar.js'
log()
被引入bar.js的程式碼,如下面幾種情況:
先上錯誤的示例:
// 錯誤的
function log() {
console.log('test')
}
export log // 原因是匯出變數不能直接匯出變數名
然後上正確的示例:
【正確1】:宣告時直接匯出,類似上面的【1】export let a = 1
export function log() {
console.log('test1')
}
【正確2】:先宣告再匯出,類似上面的【2】
let log = function () {
console.log('test2')
}
export {log}
【正確3】:同樣是先宣告再匯出,和【正確2】類似
function log() {
console.log('test3')
}
export {log}
5、匯出預設的(引入檔案不需要知道內部提供的介面的名字是什麼)
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
之前幾種匯出方法可以用,但實際使用中,用這種方式的比較多。因為最大的好處是不需要知道模組對外提供的介面的介面名是什麼。
這種匯出方式和上面不同之處在於:
- 是唯一的,一個模組內只能出現這一個;
- 和
export
不衝突,可以同時存在; - import的引入方式,與
export
方法匯出的引入方式不同; - 後面可以直接跟變數、函式、類或其他,而
export
後面跟的必須放在大括號裡;
以下程式碼可驗證:
// bar.js
function log() {
console.log('test4')
}
export default log // 如果是export,這裡就不能是log,而應該是{log}
export let a = 1 // 可以同時export和export default都存在
// foo.js
import log from './bar.js'
import {a} from './bar.js'
log() // test4
console.log(a) // 1
6、匯出繼承來的模組
6.1、匯出預設介面
需要注意的是,bar.js
不能直接用export default from './baz'
,轉碼的時候回報錯。
也不能用export {default} from './baz'
,轉碼後用現有foo.js
程式碼無法正常匯入。
總之,穩妥起見,請用以下方式來實現
// baz.js
export default 'baz'
// bar.js
import baz from './baz'
export default baz
// foo.js
import baz from './bar.js'
console.log(baz) // baz
6.2、匯出指定介面
由於轉碼問題,理論上一些合法的寫法不能寫,例如export * from './baz'
這樣可以匯出全部介面的,在轉碼後無法正常顯示資料,因此建議以以下這種方式來寫。(即匯入匯出分兩行程式碼來寫)
// baz.js
export let baz = 'baz', BAZ = 'BAZ'
// bar.js
import {baz} from './baz'
export {baz}
// foo.js
import {baz} from './bar.js'
console.log(baz) // baz
注意點:
1、export匯出的變數是按引用傳遞的,是模組內的實時值。
具體來說,如有以下程式碼:
export let myLastName = 'Wang'
setTimeout(() => {
myLastName = 'Zhang'
}, 1000)
import匯入myLastName後,直接console.log,會顯示值是Wang
。假如再等超過1秒後去列印這個值,會發現他變成了Zhang
。
另外,就算中間夾雜著其他模組,只要一路傳遞過來,沒做其他處理,依然是引用的。
2、對外的介面,必須和內部的變數/常量/函式是一一對應的關係。
你不能直接使用例如export 1
這樣的寫法,這是沒有辦法引入的。
而export default 'bar'
這樣的直接匯出字串的情況,雖然在我上面提供的轉碼工具的情況下是可以正常使用的,但MDN並沒有提供這樣的用法,因此也不推薦使用。
3、錯誤的寫法:
// 錯誤的寫法
export {myLastName:myFirstName, myLastName:'1'}
原因:export後面的大括號,並不是物件的縮寫。
4、export必須出現在模組的頂層,而不能是某個塊級作用域或者函式作用域之內。這個沒啥好說的,略。
2.3、import
上面分析export,其實也順便解釋了import。
用法:
1、引入預設模組:
// bar.js
export default 'bar'
// foo.js
import bar from './bar.js'
console.log(bar) // bar
import後面直接跟變數名即可,變數名即是匯出的變數/常量/函式/類
2、引入模組的多個/指定變數/常量/方法等:
// bar.js
export let bar = 'bar', BAR = 'BAR'
// foo.js
import {bar, BAR} from './bar.js'
console.log(bar, BAR) // bar BAR
3、引入整個模組暴露的介面;
這裡必須通過as重新命名
// bar.js
export let bar = 'bar', BAR = 'BAR'
// foo.js
import * as bar from './bar.js'
console.log(bar.bar, bar.BAR) // bar BAR
相當於讓整個bar.js
的對外介面被掛載了foo.js
的bar物件上。
注意,因為是按引用傳遞的,因此只能輸出值,或者執行方法,但是不能修改變數(比如bar.bar
指向的值)
4、在3的基礎上同時匯入預設介面:
無非是1和3的結合罷了,看完程式碼就秒懂。如果是1和2的組合,寫法是類似的,把* as bar
替換成2的寫法即可
// bar.js
export let bar = 'bar', BAR = 'BAR'
export default 'bar default'
// foo.js
import barDefault, * as bar from './bar.js'
console.log(bar.bar, bar.BAR, barDefault) // bar BAR bar default
5、修改匯入介面的名字:
import name as newName from ‘xxx’
import {name as newName, …} from ‘xxx’
例子不給了,簡單的說,上面那個是修改預設介面,下面的是修改暴露出來的介面。從上面看到這裡還看不懂的就太笨了。
沒了吧?應該沒了吧?匯入再匯出的參照export那邊的程式碼
注意點:
1、import
和export
不同,他會被提升到模組的頂部(而export
不會被提升,因此必須先宣告再匯出,或者宣告時立即匯出);
但依然推薦把import
寫在最上面,這是一個好習慣。另外,請不要寫在塊級作用域內。
2、模組名的.js
字尾可以省略,其他的不行;
3、模組名的路徑也可以省略,但你得配置告訴解析引擎取法,一般有固定的取法(比如先同級目錄,再去node_modules
目錄下找之類,具體看解析引擎的設定);
4、如果某個模組被多次匯入,裡面的程式碼只會被執行一次。
例如以下程式碼,bar裡的程式碼只會執行一次。
import 'bar'
import 'bar'
又例如:模組A匯入模組B和模組C,而模組B和模組C都匯入模組D,那麼模組D裡面的程式碼依然只會被執行一次。(證明方法略)
原因是因為模組是按引用傳遞的,因此如果執行多次,就不會是按引用生效的了。
2.4、export default
目的是讓使用者不用檢視有哪些介面也能用,畢竟我用你的方法,還要知道你提供哪些介面,實在是很麻煩的事情。要是引入進來,用我自己定的變數名直接使用,才夠人性化。
具體用法見2.2的【6.1】和2.3的【1】即可。
一般預設輸出一個物件、類、函式,偶爾會預設輸出變數。
因為很靈活,個人建議按標準寫法寫,即先宣告,再匯出,這樣看起來既美觀,也不容易出錯,還好維護。
2.5、import和export的混合使用
先給個阮一峰的文章連結,這些寫法理論上是正確的。
但還是那句話,轉碼工具有時候會給你搞事,所以還是穩妥起見,先匯入,再匯出。
有些程式碼,如以下,都是會無法正常執行的:
// 預設介面匯出
export { default } from 'foo';
// 預設轉具名匯出
export {default as bar} from 'baz';
更多都不列舉了。
但只要先匯入、再匯出,一般是不會出錯的。
3、模組的繼承
讀者們,你們知道我寫這篇部落格最痛苦的是什麼麼?
明明es6規定了這個特性,但卻由於瀏覽器本身不支援,外加轉碼工具的不支援這個特性,導致無法正常實現。
例如,以下程式碼應該是允許的:
// bar.js
export * from 'baz'
export let bar = 'bar'
這段程式碼的效果是將baz匯出的原封不動匯出,再順便再加匯出一個bar變數。
然而轉碼工具轉碼後會報錯。所以GG。
如果你像我一樣,想先獲取import * as baz from 'baz'
,然後遍歷baz再匯出來實現。對不起,請念一句話:export不能用在塊級作用域內
。
所以模組的繼承,如果你使用的是不支援這種特性的轉碼工具,那麼就沒法用。
另外,並不確認babel
是否支援。建議使用babel轉的人可以試試。
4、常量、配置
4.1、關於常量
阮一峰的博文提到,const的作用域只有宣告的時候作用域。
然而,由於模組的特性,因此const宣告的變數也可以被匯出,並且也是按引用傳遞的。
那麼其他模組能不能修改呢?這存在幾個問題:
1、有的轉碼工具,不允許直接修改引用變數的值(準確的說,是這兒變數名指向的記憶體地址),會報錯(原因是會按引用傳遞,如果修改導致出現很多問題,比如其他引入這個變數的值也會被修改,很麻煩),根本不能轉;
2、按照解耦和封裝的思想,也不應該修改。
直接修改獲取到的變數的值,是違反封裝的原則的,如果真需要修改,應該呼叫模組提供的方法來進行修改;
3、一般也沒必要改,如果要改,可以把引入的變數的值賦給新的一個變數,然後修改那個新的變數的值即可;
4、對於物件之類的按引用型別來說,一般不會讓他指向另外一個物件,而是給原有物件新增屬性,這個可以直接修改,並不影響,但我並不建議。原因參照第二條。
let同理。
4.2、模組和配置
我們一般在開發的時候,需要在多個元件或者頁面裡,實現相同的邏輯。
如果是比較初級的開發者,可能會通過複製貼上,將一段邏輯複製貼上到多個地方。
然而這造成兩個問題:
- 當變更需求的時候,需要一一去找使用這段邏輯的地方,很容易造成漏改;
- 程式碼亢餘,維護不方便。
對於有一定經驗的開發者來說,通常是將這樣的邏輯,寫在一個單獨的js檔案中。這樣在使用的時候,直接引入這個檔案,並執行裡面的方法即可。
有沒有發現,這不就是模組嘛,這種模組可以稱為配置模組。
作為配置的模組,匯出的內容不僅有函式,可以是常量、物件、類等。
例如:
我們需要做一個表單功能,他需要校驗一個欄位。而這個欄位會在多個表單中出現,我們就可以抽象出這個驗證方法來。放在一個js檔案中,通過export匯出這個校驗邏輯。
既然這個校驗邏輯可以放,那麼其他可能共用的校驗邏輯,自然也可以放入這個js檔案。
最後,這個js變成一個專門用於校驗表單模組了。
除了校驗邏輯外,還可以有很多其他的東西。
例如,同樣是表單,校驗成功和失敗後,是要有提示圖示的。如果每個頁面單獨放,那顯然是很麻煩的。
另外,萬一產品經理覺得這個圖示不好看,要改成其他圖示,你只有兩個辦法,同名替換檔案法,或者是在程式碼裡搜尋並修改。
同名替換檔案法是一個辦法,但萬一這個圖示還在其他地方使用,替換後導致本不應該被換的地方也被換了,豈不是坑。
所以可以在模組裡增加一個成功/失敗的圖示url,匯出這個url字串,並讓該url成為你使用的圖示的url即可。
如果需要修改,直接改url即可,所有使用這個url的地方都會自動用新的值。
就像下面這個示例程式碼一樣
// config.js
let successIconUrl = './success-icon.png'
export {successIconUrl}
// field.js
import {successIconUrl} from 'config'
icon.src = successIconUrl
// field2.js
import {successIconUrl} from 'config'
icon.src = successIconUrl
5、非同步載入
import()
別看了,提案而已,還不支援。
import和export瀏覽器都不支援,更別說這個了。
阻礙js發展的,就是瀏覽器的相容性的,以及可惡的IE6、7、8
→_→
阮一峰的博文連結點選檢視import(),我略了。