1. 程式人生 > >從零開始學_JavaScript_系列(67)——es6的import和export(chrome61版本後可用)

從零開始學_JavaScript_系列(67)——es6的import和export(chrome61版本後可用)

1、概述

1.1、module是什麼

俗稱【模組】。

簡單來說,假如我寫了三個功能,每個功能1000行程式碼。

如果放在一個js檔案裡,那就是3000行程式碼;

如果每個功能放在一個js檔案裡,那就是各1000行程式碼。

在往常情況,我們可能需要在html檔案裡,通過script標籤來載入這三個js檔案。但這個帶來幾個問題:

  1. 必須依賴html檔案;
  2. 沒有辦法做到按需載入(除非你用例如document.write或者建立新的DOM標籤,之類的方法寫入標籤);
  3. 不能做到js檔案對js檔案的依賴關係;
  4. 不能做到只暴露指定介面(可以通過立即執行函式返回一個物件,來變相實現,但是很麻煩);
  5. 會汙染全域性變數;
  6. 不方便;

為了解決以上問題,有一些通行的規範,比如CommonJS(被Node使用,一般用於伺服器端)、AMD規範等(一般用於瀏覽器端)。而ES6也推出了自己的規範。

1.2、AMD、CMD、CommonJS

我將之前原本寫在這裡的內容單獨截取出來了,參照上面。

1.3、es6的模組化

es6的模組化設計思想有以下特點:

  1. 靜態化,載入哪些,可以在編譯的時候就確定,而不是隻有當執行的時候才確定;
  2. 編譯的時候就能確認依賴關係,這樣好優化(缺點是某些模組不確定需不需要用,非同步載入需要另外實現,很麻煩);
  3. 通過export顯式的指定匯出的內容,避免全域性汙染,例如讓模組內部的一些東西影響到其他模組;
  4. 模組內部的作用域是獨立的,不同模組即使有同名變數,只要該變數不影響外部,那麼也是互不干擾的;

另外,es6的模組內部,自動啟用【嚴格模式】

2、import和export

2.1、準備工作

由於瀏覽器還不支援import和export(Chrome61版本和之後支援),本文成文時間是61版本之前,因此需要轉碼。

然後從我的github上down下來這個資料夾下的東西:

  1. 先全域性安裝轉碼工具;
  2. 然後再修改入口檔案foo.js,以及他引用的檔案的程式碼;
  3. 並執行npm run test完成轉碼;
  4. 開啟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, … };

之前幾種匯出方法可以用,但實際使用中,用這種方式的比較多。因為最大的好處是不需要知道模組對外提供的介面的介面名是什麼。

這種匯出方式和上面不同之處在於:

  1. 是唯一的,一個模組內只能出現這一個;
  2. export不衝突,可以同時存在;
  3. import的引入方式,與export方法匯出的引入方式不同;
  4. 後面可以直接跟變數、函式、類或其他,而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、importexport不同,他會被提升到模組的頂部(而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、模組和配置

我們一般在開發的時候,需要在多個元件或者頁面裡,實現相同的邏輯。

如果是比較初級的開發者,可能會通過複製貼上,將一段邏輯複製貼上到多個地方。

然而這造成兩個問題:

  1. 當變更需求的時候,需要一一去找使用這段邏輯的地方,很容易造成漏改;
  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(),我略了。