ECMA Script 6_模塊加載方案 ES6 Module 模塊語法_import_export

1. 模塊加載方案 commonJSnode

背景:es6

歷史上,JavaScript 一直沒有模塊(module)體系,json

沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。promise

其餘語言都有這項功能: 瀏覽器

Ruby 的require緩存

Python 的import服務器

甚至就連 CSS 都有@import異步

可是 JavaScript 任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙async

在 ES6 以前,社區制定了一些模塊加載方案,最主要的有:函數

CommonJS     用於服務器

AMD    用於瀏覽器

ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代 CommonJS 和 AMD 規

範,成爲瀏覽器和服務器通用的模塊解決方案

ES6 模塊的設計思想: 儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入輸出的變量

CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。

好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。

運行時加載:實質是總體加載fs模塊(即加載fs的全部方法),生成一個對象(_fs),而後再從這個對象上面讀取 3 個方法

  • let { stat, exists, readFile } = require('fs');    // CommonJS模塊
    
    
    // 等同於
    let _fs = require('fs');
    let stat = _fs.stat;
    let exists = _fs.exists;
    let readfile = _fs.readfile;

ES6 模塊 不是對象,而是經過 export 命令顯式指定 輸出的代碼,再經過 import 命令輸入

編譯時加載: 實質是從fs模塊加載 3 個方法,其餘方法不加載。

  • import { stat, exists, readFile } from 'fs';    // ES6模塊

ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。

固然,這也致使了無法引用 ES6 模塊自己,由於它不是對象

  • ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";
  • 限制
  • 變量必須聲明後再使用 函數的參數不能有同名屬性,不然報錯 不能使用 with 語句 不能對只讀屬性賦值,不然報錯 不能使用前綴 0 表示八進制數,不然報錯 不能刪除不可刪除的屬性,不然報錯 不能刪除變量 delete prop,會報錯,只能刪除屬性 delete global[prop] eval 不會在它的外層做用域引入變量 eval 和 arguments 不能被從新賦值 arguments不會自動反映函數參數的變化 不能使用 arguments.callee 不能使用 arguments.caller 禁止 this 指向全局對象 不能使用 fn.caller 和 fn.arguments 獲取函數調用的堆棧 增長了保留字(好比 protected、static 和 interface)

2. 模塊功能主要由兩個命令構成:export 和 import

export、import 命令 能夠出如今模塊的任何位置,

只要處於模塊頂層就能夠,

不能處於塊級做用域內,不然就會報錯

export

用於輸出模塊的對外接口

一個模塊就是一個獨立的文件。

 

注意1. export語句輸出的接口,與其對應的值是動態綁定關係,

即經過該接口,能夠取到模塊內部實時的值

  • export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // 輸出變量 foo,值爲bar,500 毫秒以後變成baz

不一樣於CommonJS 模塊輸出的是值的緩存,不存在動態更新

注意2. export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。

  • // 報錯
    export 1;
    
    // 報錯
    var m = 1;
    export m;

      // 報錯
      function f() {}
      export f;

    /**** 正確寫法 ****/ // 寫法一 export var m = 1; // 寫法二 var m = 1; export {m}; // 寫法三 var n = 1; export {n as m};

      // 正確
      export function f() {};

     
       

      // 正確
      function f() {}
      export {f};

  • export 命令輸出變量

模塊文件內部的全部變量,外部沒法獲取。

若是你但願外部可以讀取模塊內部的某個變量,就必須使用 export 關鍵字輸出該變量

  • // profile.js
    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;

優先考慮如下寫法。由於這樣就能夠在腳本尾部,一眼看清楚輸出了哪些變量。

  • // profile.js
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    
    export {firstName, lastName, year};
  • export 命令輸出函數或類(class)
  • export function multiply(x, y) {
        return x * y;
    };
  • 可使用 export { ...as...} 關鍵字重命名
  • function v1() { ... }
    function v2() { ... }
    
    export {
        v1 as streamV1,
        v2 as streamV2,
        v2 as streamLatestVersion    // v2 能夠用不一樣的名字輸出兩次。
    }; 

 

import

用於輸入其餘模塊提供的功能

其餘 JS 文件就能夠經過 import 命令加載這個模塊

  • // main.js
    import {firstName, lastName, year} from './profile.js'; function setName(element) {
        element.textContent = firstName + ' ' + lastName;
    }
  • import 命令要使用 as 關鍵字,將輸入的變量重命名
  • import { lastName as surname } from './profile.js';
  • import 命令輸入的變量都是隻讀的

由於它的本質是輸入接口。

也就是說,不容許在加載模塊的腳本里面,改寫接口

  • import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only;

     

     // 若是a是一個對象,改寫a的屬性是容許的
     a.foo = 'hello'; // 合法操做

  • import 命令具備提高效果,會提高到整個模塊的頭部,首先執行

本質是,import命令是編譯階段執行的,在代碼運行以前就輸入完成了。

  • 因爲import是靜態執行,因此不能使用表達式和變量,這些只有在運行時才能獲得結果的語法結構
  • 僅僅執行模塊,可是不輸入任何值
  • import 'lodash';
    import 'lodash';    // 屢次重複執行同一句 import 語句,那麼只會執行一次,而不會執行屢次
  • CommonJS 模塊的require命令  和  ES6 模塊的import命令,能夠寫在同一個模塊裏面,可是最好不要這樣作
  • 由於import在靜態解析階段執行,因此它是一個模塊之中最先執行的。下面的代碼可能不會獲得預期結果。
  • require('core-js/modules/es6.symbol');
    require('core-js/modules/es6.promise');
    import React from 'React';

模塊的總體加載

  • 現有模塊 circle.js
  • // circle.js
    export function area(radius) { return Math.PI * radius * radius; }; export function circumference(radius) { return 2 * Math.PI * radius; };
  • index.js 總體加載
  • // index.js
    import * as circle from './circle';
    
    console.log('圓面積:' + circle.area(4));
    console.log('圓周長:' + circle.circumference(14));

export default 模塊指定默認輸出

使用import命令的時候,用戶須要知道所要加載的變量名或函數名,不然沒法加載。

可是,用戶確定但願快速上手,未必願意閱讀文檔,去了解模塊有哪些屬性和方法

爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到 export default 命令,爲模塊指定默認輸出

一個模塊只能有一個默認輸出, 所以 export default 命令只能使用一次

使用 export default 時,對應的 import 語句不須要使用大括號

  • // export-default.js
    export default function foo() {
        console.log('foo');
    };
    
    // 或者寫成
    function foo() {
        console.log('foo');
    };
    
    export default foo;
  • 若是想在一條 import 語句中,同時輸入默認方法和其餘接口,能夠寫成下面這樣
  • export default function (obj) {
      // ···
    }
    
    export function each(obj, iterator, context) {
      // ···
    }
    
    export { each as forEach };
    
    
    /**** 導入 ****/
    import _, { each, forEach } from 'lodash';

跨模塊常量 const

引入import()函數,完成動態加載

import函數的參數specifier,指定所要加載的模塊的位置。

import 命令可以接受什麼參數,import()函數就能接受什麼參數,二者區別主要是後者爲動態加載。

import()相似於 Node 的require方法,區別主要是前者是異步加載,後者是同步加載

import()返回一個 Promise 對象

  • const main = document.querySelector('main');
    
    import(`./section-modules/${someVariable}.js`)
        .then(module => {
            module.loadPageInto(main);
        })
        .catch(err => {
            main.textContent = err.message;
        });

 

3. 瀏覽器加載

默認狀況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標籤就會停下

來,等到執行完腳本,再繼續向下渲染。若是是外部腳本,還必須加入腳本下載的時間

 

若是腳本體積很大,下載和執行的時間就會很長,所以形成瀏覽器堵塞,用戶會感受到瀏覽器「卡死」

了,沒有任何響應。這顯然是很很差的體驗,因此瀏覽器容許腳本異步加載,

下面就是兩種異步加載的語法

  • <script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>
  • <script> 標籤打開 defer 或 async 屬性,腳本就會異步加載。

渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。

defer 與 async 的區別是:

defer 要等到整個頁面在內存中正常渲染結束

(DOM 結構徹底生成,以及其餘腳本執行完成),纔會執行

async 一旦下載完,渲染引擎就會中斷渲染,

執行這個腳本之後,再繼續渲染

  • 瀏覽器加載 ES6 模塊,也使用<script>標籤,可是要加入type="module"屬性
  • <script type="module" src="./foo.js"></script>

瀏覽器對於帶有 type="module"的 <script>,都是異步加載,不會形成堵塞瀏覽器,

即等到整個頁面渲染完,再執行模塊腳本,等同於打開了 <script> 標籤的 defer 屬性。

ES6 模塊也容許內嵌在網頁中,語法行爲與加載外部腳本徹底一致

  • <script type="module"> import utils from "./utils.js"; // other code </script>

注意:

            • 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
            • 模塊腳本自動採用嚴格模式,無論有沒有聲明 use strict
            • 模塊之中,可使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也可使用export命令輸出對外接口。
            • 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
            • 同一個模塊若是加載屢次,將只執行一次。
  • 利用頂層的 this 等於 undefined 這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。
  • const isNotModuleScript = this !== undefined

4. ES6 模塊與 CommonJS 模塊徹底不一樣。

  • CommonJS 模塊輸出的是值的拷貝            ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載            ES6 模塊是編譯時輸出接口。

 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成

 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成

  • CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值
  • /**** 定義接口 lib.js ****/ var counter = 3; function incCounter() { counter++; }; module.exports = { counter: counter, incCounter: incCounter, }; /**** 導入 main.js ****/ var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); // 改變的是模塊文件中的值,而當前文件的值不受影響 console.log(mod.counter); // 3
  • ES6 模塊的運行機制與 CommonJS 不同。

JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。

等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值

  • // lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 第一次取值 incCounter(); console.log(counter); // 4 再取值,發現值變了
  • 惟一要注意的是: ES6 輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯

5. Node 對 ES6 模塊的處理比較麻煩,由於它有本身的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。

目前的解決方案是,將二者分開,ES6 模塊 和 CommonJS 採用各自的加載方案

  • 爲了與瀏覽器的 import 加載規則相同,Node 的.mjs文件支持 URL 路徑。
  • import './foo?query=1'; // 加載 ./foo 傳入參數 ?query=1
  • 只要文件名中含有:%#?等特殊字符,最好對這些字符進行轉義。

由於 Node 會按 URL 規則解讀

  • Node 的 import 命令只支持加載本地模塊(file:協議),不支持加載遠程模塊
  • 若是模塊名不含路徑,那麼 import 命令會去 node_modules 目錄尋找這個模塊
  • 若是腳本文件省略了後綴名,

好比import './foo',Node 會依次嘗試四個後綴名

./foo.mjs

./foo.js

./foo.json

./foo.node

若是這些腳本文件都不存在,Node 就會去加載 ./foo/package.json 的 main 字段指定的腳本。

若是 ./foo/package.json 不存在 或者 沒有 main 字段,那麼就會拋出錯誤。

6. ES6 模塊加載 CommonJS 模塊 

CommonJS 模塊的輸出 都定義在 module.exports 這個屬性上面

  • // a.js module.exports = { foo: 'hello', bar: 'world' }; // 等同於 export default { foo: 'hello', bar: 'world' }; /**** export 指向 modeule.exports, 即 exports 變量 是對 module 的 exports 屬性的引用 所以 ****/ module.exports = func; // 正確 export = func; // 錯誤

    module.exports會被視爲默認輸出,即import命令實際上輸入的是這樣一個對象{ default: module.exports }

  • 經過 import 一共有三種寫法,能夠拿到 CommonJS 模塊的module.exports
  • // 寫法一 import baz from './a'; // baz = {foo: 'hello', bar: 'world'}; // 寫法二 import {default as baz} from './a'; // baz = {foo: 'hello', bar: 'world'}; // 寫法三 import * as baz from './a'; // baz = { // get default() {return module.exports;}, // get foo() {return this.default.foo}.bind(baz), // get bar() {return this.default.bar}.bind(baz) // }
  • CommonJS 的一個文件,就是一個模塊
  • 每一個模塊文件都默認包裹一層函數:console.log(arguments.callee.toString());
  • 能夠經過將變量和函數設置爲  module.exports / exports 的屬性來暴露模塊內容(變量和函數)
  • require 命令第一次導入加載模塊內容,就會執行整個腳本,而後在內存生成一個對象
  • function(exports, require, module, __filename, __dirname){}

正是由於有了這層看不見的函數,因此一個模塊就是一個函數做用域,與其餘模塊做用域互相獨立

相關文章
相關標籤/搜索