ES6經常使用但被忽略的方法(第九彈Module)

寫在開頭

  • ES6經常使用但被忽略的方法 系列文章,整理做者認爲一些平常開發可能會用到的一些方法、使用技巧和一些應用場景,細節深刻請查看相關內容鏈接,歡迎補充交流。

相關文章

Module

  • ES6-Module
  • CommonJSAMD 模塊,都只能在運行時肯定這些東西。 ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高,這種加載稱爲「編譯時加載」或者靜態加載。
  • 優點
    1. 能進一步拓寬 JavaScript 的語法,好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。
    2. 再也不須要UMD模塊格式。
    3. 未來瀏覽器的新 API 就能用模塊格式提供,再也不必須作成全局變量或者navigator對象的屬性。
    4. 再也不須要對象做爲命名空間(好比Math對象),將來這些功能能夠經過模塊提供。

嚴格模式

  • ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict"
  • 限制:
    1. 變量必須聲明後再使用
    2. 函數的參數不能有同名屬性,不然報錯
    3. 不能使用with語句
    4. 不能對只讀屬性賦值,不然報錯
    5. 不能使用前綴 0 表示八進制數,不然報錯
    6. 不能刪除不可刪除的屬性,不然報錯
    7. 不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]
    8. eval不會在它的外層做用域引入變量
    9. evalarguments不能被從新賦值
    10. arguments不會自動反映函數參數的變化
    11. 不能使用arguments.calleearguments.caller
    12. 禁止this指向全局對象
    13. 不能使用fn.callerfn.arguments獲取函數調用的堆棧
    14. 增長了保留字(好比protectedstaticinterface
  • 尤爲須要注意this的限制。ES6 模塊之中,頂層的this指向undefined,即不該該在頂層代碼使用this

export 命令

  • export命令用於規定模塊的對外接口。
  • 一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸出該變量。除了輸出變量,還能夠輸出函數或類(class)。
// index.js
export const name = 'detanx';
export const year = 1995;
export function multiply(x, y) {
  return x * y;
};

// 寫法二
const name = 'detanx';
const year = 1995;
function multiply(x, y) {
  return x * y;
};
export { name, year, multiply }
複製代碼
  • export輸出的變量就是原本的名字,可是可使用as關鍵字重命名。重命名後,能夠用不一樣的名字輸出屢次。
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
複製代碼
  • export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯
export 1;

var m = 1;
export m;

// 正確
export var m = 1;

var m = 1;
export {m};

var n = 1;
export {n as m};
複製代碼
  • export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。
  • export *命令會忽略模塊的default方法。
// 總體輸出
export * from 'my_module';
複製代碼

import 命令

  • 使用export命令定義了模塊的對外接口之後,其餘 JS 文件就能夠經過import命令加載這個模塊。想爲輸入的變量從新取一個名字,import命令要使用as關鍵字,將輸入的變量重命名。
import { name, year } from './index.js';
import { name as username } from './profile.js';
複製代碼
  • import命令輸入的變量都是隻讀的,由於它的本質是輸入接口。 也就是說,不容許在加載模塊的腳本里面,改寫接口。若是a是一個對象,改寫a的屬性是容許的。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操做
複製代碼
  • import後面的from指定模塊文件的位置,能夠是相對路徑,也能夠是絕對路徑,.js後綴能夠省略。若是隻是模塊名,不帶有路徑,那麼必須有配置文件(例如使用webpack配置路徑),告訴 JavaScript 引擎該模塊的位置。
import {myMethod} from 'util';
複製代碼
  • import命令具備提高效果,會提高到整個模塊的頭部,首先執行。
foo(); // 不會報錯
import { foo } from 'my_module';
複製代碼
  • import是靜態執行,因此不能使用表達式和變量,這些只有在運行時才能獲得結果的語法結構。
// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
複製代碼
  • 屢次重複執行同一句import語句,那麼只會執行一次,而不會執行屢次。
import 'lodash';
import 'lodash'; // 只會執行一次

import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';
複製代碼

模塊的總體加載

  • 除了指定加載某個輸出值,還可使用總體加載,即用星號(*)指定一個對象,全部輸出值都加載在這個對象上面。
import * as user from './index.js';
user.name; // 'detanx'
user.year; // 1995
複製代碼

export default 命令

  • export default命令,爲模塊指定默認輸出。其餘模塊加載該模塊時,import命令(import命令後面,不使用大括號)能夠爲該匿名函數指定任意名字。
// export-default.js
export default function () {
  console.log('detanx');
}

// import-default.js
import customName from './export-default';
customName(); // 'detanx'
複製代碼
  • 使用export default時,對應的import語句不須要使用大括號;使用export,對應的import語句須要使用大括號。 一個模塊只能有一個默認輸出,所以export default命令只能使用一次。
export default function crc32() {  ...}
import crc32 from 'crc32'; 

export function crc32() { ... };
import { crc32 } from 'crc32';
複製代碼

export 與 import 的複合寫法

  • 若是在一個模塊之中,先輸入後輸出同一個模塊, import語句能夠與export語句寫在一塊兒。寫成一行之後,foobar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foobar
export { foo, bar } from 'my_module';

// 能夠簡單理解爲
import { foo, bar } from 'my_module';
export { foo, bar };
複製代碼
  • 模塊的接口更名和總體輸出,也能夠採用這種寫法。
// 接口更名
export { foo as myFoo } from 'my_module';

// 總體輸出
export * from 'my_module';
複製代碼
  • 默認接口的寫法以下。
export { default } from 'foo';
複製代碼
  • 具名接口改成默認接口的寫法以下。
export { es6 as default } from './someModule';

// 等同於
import { es6 } from './someModule';
export default es6;
複製代碼
  • 一樣地,默認接口也能夠更名爲具名接口。
export { default as es6 } from './someModule';
ES2020 以前,有一種import語句,沒有對應的複合寫法。

import * as someIdentifier from "someModule";
複製代碼
  • ES2020補上了這個寫法。
export * as ns from "mod";

// 等同於
import * as ns from "mod";
export {ns};
複製代碼

應用

  1. 公共模塊
    • 例如項目有不少的公共方法放到一個constant的文件,咱們須要什麼就加載什麼。
    // constants.js 模塊
    export const A = 1;
    export const B = 3;
    export const C = 4;
    
    // use.js
    import {A, B} from './constants';
    複製代碼
  2. import()
    • import命令會被 JavaScript 引擎靜態分析,先於模塊內的其餘語句執行(import命令叫作「鏈接」 binding 其實更合適)。因此咱們只能在最頂層去使用。ES2020引入import()函數,支持動態加載模塊。
    • import()返回一個 Promise 對象。
    const main = document.querySelector('main');
    
    import(`./section-modules/${someVariable}.js`)
      .then(module => {
        module.loadPageInto(main);
      })
      .catch(err => {
        main.textContent = err.message;
      });
    複製代碼
    • import()函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也可使用。它是運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊。另外,import()函數與所加載的模塊沒有靜態鏈接關係,這點也是與import語句不相同。import()相似於 Noderequire方法,區別主要是前者是異步加載,後者是同步加載。
    • 適用場景按需加載、條件加載、動態的模塊路徑。
  3. 注意點
    • import()加載模塊成功之後,這個模塊會做爲一個對象,看成then方法的參數。所以,可使用對象解構賦值的語法,獲取輸出接口。
    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...·
    });
    複製代碼
    • 上面代碼中,export1export2都是myModule.js的輸出接口,能夠解構得到。
    • 若是模塊有default輸出接口,能夠用參數直接得到。
    import('./myModule.js')
    .then(myModule => {
      console.log(myModule.default);
    });
    複製代碼
    • 上面的代碼也可使用具名輸入的形式。
    import('./myModule.js')
    .then(({default: theDefault}) => {
      console.log(theDefault);
    });
    複製代碼
    • 若是想同時加載多個模塊,能夠採用下面的寫法。
    Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ])
    .then(([module1, module2, module3]) => {
       ···
    });
    複製代碼
    • import()也能夠用在 async 函數之中。
    async function main() {
      const myModule = await import('./myModule.js');
      const {export1, export2} = await import('./myModule.js');
      const [module1, module2, module3] =
        await Promise.all([
          import('./module1.js'),
          import('./module2.js'),
          import('./module3.js'),
        ]);
    }
    main();
    複製代碼

Module 加載實現

簡介

  1. 傳統加載
    • 默認狀況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標籤就會停下來,等到執行完腳本,再繼續向下渲染。爲了解決<script>標籤打開deferasync屬性,腳本就會異步加載。
    • deferasync的區別是:defer要等到整個頁面在內存中正常渲染結束(DOM 結構徹底生成,以及其餘腳本執行完成),纔會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本之後,再繼續渲染。一句話,defer是「渲染完再執行」,async是「下載完就執行」。另外,若是有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的。
  2. 加載規則
    • 瀏覽器加載 ES6 模塊,也使用<script>標籤,可是要加入type="module"屬性。等同於打開了<script>標籤的defer屬性。
    <script type="module" src="./foo.js"></script>
    
    <!-- 等同於 -->
    <script type="module" src="./foo.js" defer></script>
    複製代碼
    • 對於外部的模塊腳本,有幾點須要注意。
      1. 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
      2. 模塊腳本自動採用嚴格模式,無論有沒有聲明"use strict"
      3. 模塊之中,可使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也可使用export命令輸出對外接口。
      4. 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
      5. 同一個模塊若是加載屢次,將只執行一次。
      import utils from 'https://example.com/js/utils.js';
      const x = 1;
      
      console.log(x === window.x); //false
      console.log(this === undefined); // true
      複製代碼
    • 利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。
    const isNotModuleScript = this !== undefined;
    複製代碼

ES6 模塊與 CommonJS 模塊的差別

  • 討論 Node.js 加載 ES6 模塊以前,必須瞭解 ES6 模塊與 CommonJS 模塊徹底不一樣。
  • 它們有兩個重大差別。
    1. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
    2. 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

// 寫成函數
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

$ node main.js
3
4
複製代碼
  • ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。
// 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 輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯。
// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError
複製代碼
  • export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。
// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();
複製代碼
  • 上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。
// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';
複製代碼
  • 如今執行main.js,輸出的是 1
$ babel-node main.js
1
複製代碼
  • 證實了x.jsy.js加載的都是C的同一個實例。

Node.js 加載

  • Node.js 要求 ES6 模塊採用.mjs後綴文件名。Node.js 遇到.mjs文件,就認爲它是 ES6 模塊,默認啓用嚴格模式,沒必要在每一個模塊文件頂部指定"use strict"。 若是不但願將後綴名改爲.mjs,能夠在項目的package.json文件中,指定type字段爲module
{
   "type": "module"
}
複製代碼
  • 這時還要使用 CommonJS 模塊,那麼須要將 CommonJS 腳本的後綴名都改爲.cjs。若是沒有type字段,或者type字段爲commonjs,則.js腳本會被解釋成 CommonJS 模塊。node

  • 總結:.mjs文件老是以 ES6 模塊加載,.cjs文件老是以 CommonJS 模塊加載,.js文件的加載取決於package.json裏面type字段的設置。webpack

  • 注意,ES6 模塊與 CommonJS 模塊儘可能不要混用。require命令不能加載.mjs文件,會報錯,只有import命令才能夠加載.mjs文件。反過來,.mjs文件裏面也不能使用require命令,必須使用import。es6

  • Node.js 加載 主要是介紹ES6 模塊和 CommonJS 相互之間的支持,有興趣的能夠本身去看看。web

循環加載

  • 「循環加載」(circular dependency指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。「循環加載」表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現,但很難避免尤爲是特別複雜的項目。
相關文章
相關標籤/搜索