天天看點

ES6學習系列——Module

ES6 子產品(Module)總覽:

在ES6 之前,JS中并沒有子產品體系,但是程式猿還是搞出了替代的子產品加載方案:用戶端用AMD,而服務端用commonJS。在ES6 中正式提出了子產品(Module),相較于前面的替代方案,它的靜态優化更好,是以效率更加高,前展也更為可觀,為JS 的文法拓展可以提供條件。

為什麼說它的靜态優化更好呢?因為ES6 的子產品在編譯的過程中就可以确定子產品間的依賴關系以及要輸出和輸入的變量。更直覺地說,就是ES6 子產品在編譯的時候就可以完成子產品的加載。

反觀AMD 和 commonJS,在這類子產品加載方案中,隻有在運作當中用到相關的子產品,才會對這個子產品進行整體的加載,然後生成一個對象,在這個對象中去查找相關屬性,效率自然就低一點。

再來說說ES6 子產品的一些特點:

  • 自動采用嚴格模式,不需要再加上”use strict”;
  • 尤其需要注意的是,在子產品的頂層代碼(也就是子產品中非塊級作用域{ }内的代碼)中,不要使用this,因為在嚴格模式中禁止this 指向全局對象(此時this 為 undefined);

1、export 指令

export 指令作用是向外輸出變量,而關鍵就在于提供對外接口,規定要與子產品内部的變量對應起來;而且export 指令必須要在子產品的頂層中,不能寫在塊級作用域中,否則就給你報錯;

是以往往是這樣輸出的

export { };

let a = ;
let b = function () { console.log("hello world");};

//可以使用as 來重命名多次, 用不同的名字來調用
export {
    a,
    b as renamedB1,
    b as renamedB2
};

//函數和類還可以這樣輸出
export function c () {
    console.log("welcome to NewYork");
}

//接口和對應的值是動态綁定的,輸出的foo的值是test1,在一秒後悔變成test2
export let foo = 'test1';
setTimeoUt( ()=>{foo = 'test2'}, );

           

2、import 指令

使用import 指令可以加載某個已經輸出的子產品;

文法一般像這樣:

import { } from url ;

放在module檔案夾中,要輸出的模快

profile.js

let name = "Troyes";
let age = ;
function printMsg (name, age) {
    console.log('name is:'+ name+ ' ,and age is:'+ age);
}
export { name, age, printMsg };
           
  • { }

    中可以寫從其他子產品導入的對應變量名(也就是說這些變量要與export 的對外接口同名);
import {name, age, printMsg as outPut} from 'module/profile';
outPut(); //name is:Troyes ,and age is:
           
  • 可以在

    {}

    使用

    as

    來為輸入的變量重新命名;
  • import

    指令具有提升效果,寫在檔案末尾也會提升到頭部首先執行;
  • url

    是輸出子產品所在的位置,可以是相對或者絕對路徑,倘若url 隻是子產品的名字而非路徑,那就要另外寫配置檔案來給 JS 引擎指明該子產品的位置;
  • 由于

    import

    是靜态執行,是以在

    import

    中,不能使用隻有在運作中才能得到結果的文法結構,譬如表達式和變量;
  • 同一個

    import

    語句如果同時出現,那麼隻會執行一次;
  • 加載同一個子產品中的多個變量,

    import

    也是隻執行一次(在編譯的時候将這些要輸入的變量都搜集起來,然後一次性都加載進來)。
  • 現階段最好不要混用

    Module

    import

    commomJS

    require

    ,容易出bug;

如何加載整個子產品?

整體加載某個子產品,隻需要用

*

來指定一個新的對象便可,隻不過這個新對象在加載完之後是不允許在運作的時候再去改變(譬如添加屬性之類的操作)。要輸出的子產品還是上面的

proflie.js

,具體操作如下:

import * as person from 'module/profile';

person.printMsg(); //name is:Troyes ,and age is:
           

3、export 和 import 的配合使用

  • 倘若在一個子產品當中,要先後輸入輸出同一個子產品,有簡寫的方式,操作如下:
import {name, age} from 'module/profile';
export {name, age};

//可以簡寫為:
export {name, age} from 'module/profile';
           
  • 整個子產品輸出:
export * from 'module/profile';
           

4、往前看:import() 函數

import 是靜态分析,也就是在編譯階段就處理完成了,是以無法在運作的時候動态加載;就有提案建議搞個

import()

函數,來支援動态加載;

下面來兩個連結(對應中英文版):

Native ECMAScript modules: dynamic import()

原生 ECMAScript 子產品:動态import()

5、ES6 Module 加載實作探讨

這裡先讨論在浏覽器中Module的用法,用戶端的node 我還沒動手去學習。

在ES6 之前,浏覽器加載腳本都是 用的

<script>

标簽, 而浏覽器對js 腳本的加載采取的是同步方式,也就是說頁面渲染的時候遇到js 腳本,必須要先把腳本執行完(如果是外部腳本還要先下載下傳),才會繼續渲染。

//内部腳本
<script type='application/javascript'>
    let a = ;
</script>

//外部腳本(也就加上src)
//注意:如果是外部腳本,标簽裡面寫的内容将會被忽略
<script type='application/javascript' src=url></script>

//浏覽器的預設腳本語言就是js,是以type可以省略
<script>
    let a = ; 
</script>

//js腳本異步加載處理:添加defer 或者 async 屬性
//二者有何差別:雖然都是異步加載,defer 是先下載下傳,等整個頁面渲染完才會去執行;
//而async 就比較厲害了,下載下傳完之後頁面渲染就要停下來,等此腳本執行完才繼續渲染;
//另外,加defer 才能保證加載順序,加async 是不能保證的;
<script src=url defer> </script>
<script src=url async> </script>
           
(1)浏覽器如何加載ES6 子產品?

加載也是利用

<script>

标簽,隻是要加上

type="module"

再者它預設加上

defer

屬性,是以如果想要改變它的執行時機,就要另外加上

async

屬性。

另外一個需要注意的點是:子產品中

import

的時候,

url

中 其子產品的檔案名字尾

.js

不能省略;而且子產品中,頂層的

this

傳回的是 undefined,而不是

window

(2)循環加載:兩個腳本互相依賴,循環加載就會出現

說到循環加載,就必須探讨加載原理:

對于ES6 子產品的加載原理,上面已經探讨過啦:在編譯階段就已經将子產品的依賴關系和要輸出輸入的變量确定下來;

上執行個體:

// a.js
import {bar} from './b';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a';
console.log('b.js');
console.log(foo);
export let bar = 'bar';
           

a 和 b 互相依賴,構成循環加載。我們來看a 的執行結果:

b.js
undefined
a.js
bar
           

再來看具體的執行過程:

a.js 第一行就加載 b.js,是以先執行 b.js,而 b.js 的第一行又是執行 a.js , 而 a.js 已經在執行了,是以就繼續執行 b.js , 是以第一行就是輸出 “b.js” ;

接下來到

console.log(foo);

,而這時候 在 a.js 中

foo

還沒有定義,是以第二行輸出的是”undefined”;

b.js 接下來正常執行完,就會繼續執行 a.js ,接下來輸出的自然就是 “a.js”, “bar”;

接下來就看看 commonJS 子產品是如何加載的:

在commonJS 中,一個子產品就是單獨的一個腳本檔案,一旦用 reqiure 指令去加載腳本,這個腳本就會整個執行,然後生成一個對象,放在記憶體中;接下來如果再一次用reqiure 加載這個子產品,引擎不會再去執行整個腳本,而是去記憶體中取第一次加載生成的那個對象;更重要的是,commonJS 遇上循環加載,傳回的是已經執行部分的值,後面還沒執行的部分的值影響不到它。

上執行個體:

//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');

//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
           

直接來看執行過程:

a.js 先執行,輸出 done 變量 ,然後加載 b.js ,去執行 b.js,a.js 就停在了第二行,等待b.js 執行完;

b.js 執行到第二行的時候,去加載 a.js ,這時就出現循環加載;b.js 就去記憶體中查找 a.js 已經執行部分所對應的對象的屬性,得到了 a.js 輸出的done 變量,是以第三行列印:

在 b.js 之中,a.done = false

。知道b.js 執行完畢,a.js 才會繼續執行;

(3)ES6 轉碼:

本渣喜歡用babel 來将ES6 代碼 編譯成 ES5;

babel 基于nodeJS,安裝和使用如下(用的是windows的 cmd指令行):

//建立初始化檔案
npm init 

//安裝babel-cli, 加-D 是為了誤删node_module檔案夾之後,可以通過'npm install' 指令行再下回來
npm install babel-cli -D

//修改package.json 檔案,往"script" 添加啟動和建構指令簡寫
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    //新添run 和 build 指令
    "run": "node server.js --port=80 -s -O=a.log",
    //src是待編譯檔案所在檔案夾的名字, 而lib(預設幫你建立)則是編譯後的檔案所在檔案夾的名字
    "build": "babel src -d lib"
  }

//安裝 babel-preset-env,下面的 --save-dev 可以簡寫為 -D
npm install --save-dev babel-preset-env

//安裝babel-polyfill,這東西用來轉碼ES6 新增的API,Babel預設隻轉碼文法。用的時候隻要在腳本的頭部加上 import 'babel-polyfill';
npm i babel-polyfill -D

//給babel-preset-env 寫配置檔案".babelrc",内容如下(更深入的的配置自己去翻文檔):
{
    "presets": ["env"]
}
//啟動babel 編譯
npm run start
           

另外一個工具是 ES6 module transpiler (沒用過),npm 文檔建議這樣安裝和使用:

The transpiler can be used directly from the command line:

$ npm install -g es6-module-transpiler
$ compile-modules convert foo.js
           

Here is the basic usage:

compile-modules convert -I lib -o out FILE [FILE…]