您好,登錄后才能下訂單哦!
這篇文章主要講解了“es6模塊的原理是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“es6模塊的原理是什么”吧!
ES6模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。ES6模塊不是對象,而是通過export命令顯式指定輸出的代碼,再通過import命令輸入。由于ES6模塊是編譯時加載,使得靜態分析成為可能;有了它就能進一步拓寬JS的語法,比如引入宏和類型檢驗這些只能靠靜態分析實現的功能。
歷史上,JavaScript 一直沒有模塊
(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如 Ruby 的 require 、Python 的 import ,甚至就連 CSS 都有 @import ,但是 JavaScript 任何這方面的支持都沒有,這對開發大型的、復雜的項目形成了巨大障礙。
在 ES6 之前,社區制定了一些模塊加載方案,最主要的有CommonJS
和AMD
兩種。前者用于服務器
,后者用于瀏覽器
。ES6 在語言標準的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規范,成為瀏覽器和服務器通用的模塊解決方案。
ES6 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時確定這些東西。比如,CommonJS 模塊就是對象,輸入時必須查找對象屬性。
// CommonJS模塊
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代碼的實質是整體加載 fs 模塊(即加載 fs 的所有方法),生成一個對象( _fs ),然后再從這個對象上面讀取 3 個方法。這種加載稱為“運行時加載”,因為只有運行時才能得到這個對象,導致完全沒辦法在編譯時做“靜態優化”。
ES6 模塊不是對象,而是通過 export 命令顯式指定輸出的代碼,再通過 import 命令輸入。
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼的實質是從 fs 模塊加載 3 個方法,其他方法不加載。這種加載稱為“編譯時加載”
或者靜態加載
,即 ES6 可以在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。當然,這也導致了沒法引用 ES6 模塊本身,因為它不是對象。
由于 ES6 模塊是編譯時加載,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。
除了靜態加載帶來的各種好處,ES6 模塊還有以下好處。
不再需要 UMD 模塊格式了,將來服務器和瀏覽器都會支持 ES6 模塊格式。目前,通過各種工具庫,其實已經做到了這一點。
將來瀏覽器的新 API 就能用模塊格式提供,不再必須做成全局變量或者 navigator 對象的屬性。
不再需要對象作為命名空間(比如 Math 對象),未來這些功能可以通過模塊提供。
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 )
上面這些限制,模塊都必須遵守。由于嚴格模式是 ES5 引入的,不屬于 ES6,所以請參閱相關 ES5 書籍,本書不再詳細介紹了。
其中,尤其需要注意 this 的限制。ES6 模塊之中,頂層的 this 指向 undefined ,即不應該在頂層代碼使用 this 。
模塊
功能主要由兩個命令構成:export
和import
。 export 命令用于規定模塊的對外接口, import 命令用于輸入其他模塊提供的功能。
一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果你希望外部能夠讀取模塊內部的某個變量,就必須使用 export 關鍵字輸出該變量。下面是一個 JS 文件,里面使用 export 命令輸出變量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面代碼是 profile.js 文件,保存了用戶信息。ES6 將其視為一個模塊,里面用 export 命令對外部輸出了三個變量。
export 的寫法,除了像上面這樣,還有另外一種。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
上面代碼在 export 命令后面,使用大括號指定所要輸出的一組變量。它與前一種寫法(直接放置在 var 語句前)是等價的,但是應該優先考慮使用這種寫法。因為這樣就可以在腳本尾部,一眼看清楚輸出了哪些變量。
export 命令除了輸出變量,還可以輸出函數或類(class)。
export function multiply(x, y) {
return x * y;
};
上面代碼對外輸出一個函數 multiply 。
通常情況下, export 輸出的變量就是本來的名字,但是可以使用 as 關鍵字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
上面代碼使用 as 關鍵字,重命名了函數 v1 和 v2 的對外接口。重命名后, v2 可以用不同的名字輸出兩次。
需要特別注意的是, export 命令規定的是對外的接口,必須與模塊內部的變量建立一一對應關系。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
上面兩種寫法都會報錯,因為沒有提供對外的接口。第一種寫法直接輸出 1,第二種寫法通過變量 m ,還是直接輸出 1。 1 只是一個值,不是接口。正確的寫法是下面這樣。
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
上面三種寫法都是正確的,規定了對外的接口 m 。其他腳本可以通過這個接口,取到值 1 。它們的實質是,在接口名與模塊內部變量之間,建立了一一對應的關系。
同樣的, function 和 class 的輸出,也必須遵守這樣的寫法。
// 報錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f};
另外, export 語句輸出的接口,與其對應的值是動態綁定關系,即通過該接口,可以取到模塊內部實時的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代碼輸出變量 foo ,值為 bar ,500 毫秒之后變成 baz 。
這一點與 CommonJS 規范完全不同。CommonJS 模塊輸出的是值的緩存,不存在動態更新。
最后, export 命令可以出現在模塊的任何位置,只要處于模塊頂層就可以。如果處于塊級作用域內,就會報錯,下一節的 import 命令也是如此。這是因為處于條件代碼塊之中,就沒法做靜態優化了,違背了 ES6 模塊的設計初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
上面代碼中, export 語句放在函數之中,結果報錯。
使用export
命令定義了模塊的對外接口以后,其他JS
文件就可以通過import
命令加載這個模塊。
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
上面代碼的 import 命令,用于加載 profile.js 文件,并從中輸入變量。 import 命令接受一對大括號,里面指定要從其他模塊導入的變量名。大括號里面的變量名,必須與被導入模塊( profile.js )對外接口的名稱相同。
如果想為輸入的變量重新取一個名字, 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 是一個對象,改寫 a 的屬性是允許的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
上面代碼中, a 的屬性可以成功改寫,并且其他模塊也可以讀到改寫后的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都當作完全只讀,不要輕易改變它的屬性。
import 后面的 from 指定模塊文件的位置,可以是相對路徑,也可以是絕對路徑, .js 后綴可以省略。如果只是模塊名,不帶有路徑,那么必須有配置文件,告訴 JavaScript 引擎該模塊的位置。
import {myMethod} from 'util';
上面代碼中, util 是模塊文件名,由于不帶有路徑,必須通過配置,告訴引擎怎么取到這個模塊。
注意, import 命令具有提升效果,會提升到整個模塊的頭部,首先執行。
foo();
import { foo } from 'my_module';
上面的代碼不會報錯,因為 import 的執行早于 foo 的調用。這種行為的本質是, import 命令是編譯階段執行的,在代碼運行之前。
由于 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';
}
上面三種寫法都會報錯,因為它們用到了表達式、變量和 if 結構。在靜態分析階段,這些語法都是沒法得到值的。
最后, import 語句會執行所加載的模塊,因此可以有下面的寫法。
import 'lodash';
上面代碼僅僅執行 lodash 模塊,但是不輸入任何值。
如果多次重復執行同一句 import 語句,那么只會執行一次,而不會執行多次。
import 'lodash';
import 'lodash';
上面代碼加載了兩次 lodash ,但是只會執行一次。
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
上面代碼中,雖然 foo 和 bar 在兩個語句中加載,但是它們對應的是同一個 my_module 實例。也就是說, import 語句是 Singleton 模式。
目前階段,通過 Babel 轉碼,CommonJS 模塊的 require 命令和 ES6 模塊的 import 命令,可以寫在同一個模塊里面,但是最好不要這樣做。因為 import 在靜態解析階段執行,所以它是一個模塊之中最早執行的。下面的代碼可能不會得到預期結果。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
除了指定加載某個輸出值,還可以使用整體加載,即用星號
( * )指定一個對象,所有輸出值都加載在這個對象上面。
下面是一個 circle.js 文件,它輸出兩個方法 area 和 circumference 。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
現在,加載這個模塊。
// main.js
import { area, circumference } from './circle';
console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
上面寫法是逐一指定要加載的方法,整體加載的寫法如下。
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
注意,模塊整體加載所在的那個對象(上例是 circle ),應該是可以靜態分析的,所以不允許運行時改變。下面的寫法都是不允許的。
import * as circle from './circle';
// 下面兩行都是不允許的
circle.foo = 'hello';
circle.area = function () {};
從前面的例子可以看出,使用 import 命令的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。但是,用戶肯定希望快速上手,未必愿意閱讀文檔,去了解模塊有哪些屬性和方法。
為了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到 export default 命令,為模塊指定默認輸出。
// export-default.js
export default function () {
console.log('foo');
}
上面代碼是一個模塊文件 export-default.js ,它的默認輸出是一個函數。
其他模塊加載該模塊時, import 命令可以為該匿名函數指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代碼的 import 命令,可以用任意名稱指向 export-default.js 輸出的方法,這時就不需要知道原模塊輸出的函數名。需要注意的是,這時 import 命令后面,不使用大括號。
export default 命令用在非匿名函數前,也是可以的。
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者寫成
function foo() {
console.log('foo');
}
export default foo;
上面代碼中, foo 函數的函數名 foo ,在模塊外部是無效的。加載的時候,視同匿名函數加載。
下面比較一下默認輸出和正常輸出。
// 第一組
export default function crc32() { // 輸出
// ...
}
import crc32 from 'crc32'; // 輸入
// 第二組
export function crc32() { // 輸出
// ...
};
import {crc32} from 'crc32'; // 輸入
上面代碼的兩組寫法,第一組是使用 export default 時,對應的 import 語句不需要使用大括號;第二組是不使用 export default 時,對應的 import 語句需要使用大括號。
export default 命令用于指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,因此 export default 命令只能使用一次。所以,import命令后面才不用加大括號,因為只可能唯一對應 export default 命令。
本質上, export default 就是輸出一個叫做 default 的變量或方法,然后系統允許你為它取任意名字。所以,下面的寫法是有效的。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
正是因為 export default 命令其實只是輸出一個叫做 default 的變量,所以它后面不能跟變量聲明語句。
// 正確
export var a = 1;
// 正確
var a = 1;
export default a;
// 錯誤
export default var a = 1;
上面代碼中, export default a 的含義是將變量 a 的值賦給變量 default 。所以,最后一種寫法會報錯。
同樣地,因為 export default 命令的本質是將后面的值,賦給 default 變量,所以可以直接將一個值寫在 export default 之后。
// 正確
export default 42;
// 報錯
export 42;
上面代碼中,后一句報錯是因為沒有指定對外的接口,而前一句指定對外接口為 default 。
有了 export default 命令,輸入模塊時就非常直觀了,以輸入 lodash 模塊為例。
import _ from 'lodash';
如果想在一條 import 語句中,同時輸入默認方法和其他接口,可以寫成下面這樣。
import _, { each, forEach } from 'lodash';
對應上面代碼的 export 語句如下。
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
上面代碼的最后一行的意思是,暴露出 forEach 接口,默認指向 each 接口,即 forEach 和 each 指向同一個方法。
export default 也可以用來輸出類。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
如果在一個模塊之中,先輸入后輸出同一個模塊, import
語句可以與export
語句寫在一起。
export { foo, bar } from 'my_module';
// 可以簡單理解為
import { foo, bar } from 'my_module';
export { foo, bar };
上面代碼中, export 和 import 語句可以結合在一起,寫成一行。但需要注意的是,寫成一行以后, foo 和 bar 實際上并沒有被導入當前模塊,只是相當于對外轉發了這兩個接口,導致當前模塊不能直接使用 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};
模塊
之間也可以繼承
。
假設有一個 circleplus 模塊,繼承了 circle 模塊。
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
上面代碼中的 export ,表示再輸出 circle 模塊的所有屬性和方法。注意, export 命令會忽略 circle 模塊的 default 方法。然后,上面代碼又輸出了自定義的 e 變量和默認方法。
這時,也可以將 circle 的屬性或方法,改名后再輸出。
// circleplus.js
export { area as circleArea } from 'circle';
上面代碼表示,只輸出 circle 模塊的 area 方法,且將其改名為 circleArea 。
加載上面模塊的寫法如下。
// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
上面代碼中的 import exp 表示,將 circleplus 模塊的默認方法加載為 exp 方法。
本書介紹 const 命令的時候說過, const 聲明的常量只在當前代碼塊有效。如果想設置跨模塊的常量(即跨多個文件),或者說一個值要被多個模塊共享,可以采用下面的寫法。
// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
如果要使用的常量非常多,可以建一個專門的 constants 目錄,將各種常量寫在不同的文件里面,保存在該目錄下。
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
然后,將這些文件輸出的常量,合并在 index.js 里面。
// constants/index.js
export {db} from './db';
export {users} from './users';
使用的時候,直接加載 index.js 就可以了。
// script.js
import {db, users} from './constants/index';
前面介紹過,import
命令會被JavaScript
引擎靜態分析,先于模塊內的其他語句執行( import 命令叫做“連接” binding 其實更合適)。所以,下面的代碼會報錯。
// 報錯
if (x === 2) {
import MyModual from './myModual';
}
上面代碼中,引擎處理 import 語句是在編譯時,這時不會去分析或執行 if 語句,所以 import 語句放在 if 代碼塊之中毫無意義,因此會報句法錯誤,而不是執行時錯誤。也就是說, import 和 export 命令只能在模塊的頂層,不能在代碼塊之中(比如,在 if 代碼塊之中,或在函數之中)。
這樣的設計,固然有利于編譯器提高效率,但也導致無法在運行時加載模塊。在語法上,條件加載就不可能實現。如果 import 命令要取代 Node 的 require 方法,這就形成了一個障礙。因為 require 是運行時加載模塊, import 命令無法取代 require 的動態加載功能。
const path = './' + fileName;
const myModual = require(path);
上面的語句就是動態加載, require 到底加載哪一個模塊,只有運行時才知道。 import 命令做不到這一點。
ES2020提案 引入 import() 函數,支持動態加載模塊。
import(specifier)
上面代碼中, import 函數的參數 specifier ,指定所要加載的模塊的位置。 import 命令能夠接受什么參數, 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() 類似于 Node 的 require 方法,區別主要是前者是異步加載,后者是同步加載。
下面是 import() 的一些適用場合。
(1)按需加載。
import() 可以在需要的時候,再加載某個模塊。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
上面代碼中, import() 方法放在 click 事件的監聽函數之中,只有用戶點擊了按鈕,才會加載這個模塊。
(2)條件加載
import() 可以放在 if 代碼塊,根據不同的情況,加載不同的模塊。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
上面代碼中,如果滿足條件,就加載模塊 A,否則加載模塊 B。
(3)動態的模塊路徑
import() 允許模塊路徑動態生成。
import(f())
.then(...);
上面代碼中,根據函數 f 的返回結果,加載不同的模塊。
import() 加載模塊成功以后,這個模塊會作為一個對象,當作 then 方法的參數。因此,可以使用對象解構賦值的語法,獲取輸出接口。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
上面代碼中, export1 和 export2 都是 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();
感謝各位的閱讀,以上就是“es6模塊的原理是什么”的內容了,經過本文的學習后,相信大家對es6模塊的原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。