Module概述
- 歷史上,JavaScript一直沒有模塊系統,沒法將一個大程序拆分紅相互依賴的小文件,再用簡單的方法拼裝起來,這給前端工程化的開發帶來了很大的阻礙。
- 在ES6以前,前端社區指定了一些前端模塊加載方案,主要有
commonJS and AMD
,commonJS
用於服務器,在node.js中就是很好的體現,AMD主要用於瀏覽器,ES6在語言標準的層面上實現了模塊功能,並且實現的很是簡單,徹底能夠取代commonJS and AMD 成爲先後端通用的模塊解決方案
- 設計思想
- 儘可能的靜態化,在編譯的時候就能肯定各模塊之間的依賴關係
- 而
CommonJS and AMD
則都是在運行的時候肯定這些東西的
- 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,而後在從這個對象上讀取三個方法,這種加載方式稱爲運行時加載,由於只有在運行是才能獲得這個對象,致使的問題就是在編譯時徹底不能作靜態優化
- ES6模塊不是對象,而是經過export指定輸出的代碼,再經過import的方式引入
import {stat, exist, readFile} from 'fs'
這種方式是經過fs模塊加載三個方法,其餘的方法不加載,這種方式叫作「編譯時加載」,或者叫作運行時加載,即ES6是在編譯的時候加載模塊,這樣效率要比commonJS的效率高,
- ES6的模塊自動採用嚴格模式,無論咱們在文件最前面加不加
‘use strict’
默認就是嚴格模式
- 嚴格模式的限制:
- 變量必須聲明以後才能使用
- 函數的參數不能有同名的,不然報錯
- 不能使用with語句
- 不能對只讀屬性賦值,不然報錯
- 不能使用前綴0表示八進制,不然報錯
- 不能刪除不可刪除的屬性,不然報錯
- arguments不能被從新賦值
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局變量,特別注意的是頂層的this是指向undefined,而不是window
export命令
- export主要用於規定模塊的對外接口,import命令主要用於輸入其餘模塊提供的功能
- 一個模塊就是一個獨立的文件,文件內部的變量其餘外部的模塊是沒法使用的,若是模塊內部的某個變量須要外部讀取,這是咱們必須使用export命令
export const firstName = "GeekChenYi"
export const lastName = "hello world"
export const year = 2019
// 除了上面的這種寫法外,還能夠是下面的這種寫法
const firstName = "GeekChenYi"
const lastName = "hello world"
const year = 2019
export {firstName, lastName, year};// 優先使用這種方法,清晰明瞭
// 輸出函數的方法
export function show(){}
export () => {}
// 使用as重命名
const v1 = function(){};
const v2 = function(){};
const v3 = function(){};
export {
v1 as stream1,
v2 as stream2,
v3 as stream3
}
複製代碼
- 須要特別注意的是,export提供的是對外的接口,必須與模塊內部的變量創建一對一的關係,不然就會報錯
- export語句輸出的接口與其對應的值是動態綁定的關係,即經過該接口就能夠去到模塊內部的值
- commonJS則輸出的是值的緩存,不存在動態更新
import命令
- 使用export命令導出接口,其餘的js文件就能夠經過import命令加載這個模塊
// 注意這地方,其實也是一個解構賦值的寫法
import { firstName, lastName, year} from 'profile.js'
// 須要注意的是,大括號裏面的變量名必須和被導入模塊中的變量名保持一致
// 也能夠經過as 進行重命名
import {firstName as name} from 'profile.js'
複製代碼
- import命令輸入的變量是只讀的,若是直接修改會報錯
- import命令具備提高效果,會把變量提高到模塊的最前面
- 因爲import是靜態執行,所以不能使用表達式和變量
// 報錯
import { 'fo' + 'o'} from 'module_name'
// 報錯
let module = module_name;
import {foo} from module;
// 報錯,由於在編譯階段是不會執行if語句的
if(x > 1){
import { foo} from module1;
}else{
import {foo1} from module2;
}
複製代碼
import loadsh
這種方法只是加載模塊 不輸出任何值
- 模塊除了加載指定的方法,也能夠實現模塊的總體加載,
import * as cricle from cricle.js
export default命令
- 有上面的import命令咱們能夠知道,咱們在加載模塊的時候,必需要知道加載模塊名字,不然是沒法加載的,爲了使用方面,可使用export default function(){} 爲模塊指定默認的輸出,其餘模塊加載該模塊的時候,能夠經過匿名的方式實現加載,可是import的匿名是不能使用{}
- 一個模塊只能有一個默認輸出,所以只有一個
export default
,這也是爲何import後面的命令不須要加{}的緣由
// export 的本質區別
export const name = "Geek" // 以前的正確寫法
const name1 = "hello world"
export default name1;// 這種方法是正確的寫法
// 本質
export {name1 as default}
// 錯誤的寫法
export default const name2 = "hello world"
// 正確的寫法
export default 43
// 錯誤的寫法
export 43 // 它輸出的是接口
複製代碼
- export defalut 的本質就是將後面的值賦值給default變量
- export和import的複合寫法
export {foo, bar} from module_name;
// 等價於下面的寫法
import {foo, bar} from module_name;
export {foo, bar}
// 具名接口改寫爲default
export {foo as default} from module_name1
<==>
import {foo} from module_name1
export default foo
複製代碼
- 跨模塊常量,若是咱們使用的常量是很是的多,這是咱們能夠創建一個專門用來存放常量的js文件
// constants.js文件
export const A = 1;
export const B = 2;
export const C = 3;
export const D = 4;
// main.js文件
import * as constants from './constants.js'
console.log(constants.A);
console.log(constants.B);
// main1.js
import {A, B, C, D} from './constants.js'
console.log(A);
console.log(B);
console.log(C);
console.log(D);
複製代碼
- 因爲import不能實現動態的加載,也就是說只有在條件成立的時候,我纔開始加載,這也給import形成了一個很很差的效果,由於有時候咱們可能只在開發環境下導入某些模塊,在生產環境下咱們是不須要加入的,例如:mock.js。爲了解決這個問題,最新的提案是引入import()這個函數
// 這種判斷的方式是在有在開發環境下才引入咱們mock.js,而在生成環境下,咱們就不須要引入了
if(process.env.NODE_ENV == 'development'){
import('mock.js')
}
複製代碼
- import()函數返回的是一個promise對象,它能夠運行在任何的地方,由於它是在運行的時候加載。import()相似於Node.js的require(),他們之間的區別就是:import()返回的是一個promise對象,是異步的,而requre()是同步的
- import()使用場景:
- 按需加載:在須要的時候只加載某個模塊
- 條件加載:只有判斷條件成立的時候加載某個模塊
- 動態的模塊路徑:容許模塊路徑動態的生成
- import()使用注意點:
- import()加載成功以後會做爲一個對象,成爲then()放的的參數,所以可使用解構賦值的形式,獲取須要的接口
- 若是模塊有default輸出接口,這是可使用參數直接獲取
- 也可使用在async中
// import()作爲then()方法的參數
import('./module').then(({export1, export2}) => {
// ...
})
// 若是模塊有default輸出命令,
import('./module').then(myModule => {
// some code
})
// 同時加載多個模塊
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js')
]).then(([module1,module2, module3]) => {
// some code...
})
// async函數中的使用
async function main(){
const myModule1 = await import('./module.js');
const {export1, export2} = await import('./module.js');
const [myModule1, myModule2, myModule3] = await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js')
])
}
main()
複製代碼
Module的加載實現
- 傳統方法: 只是將js腳本經過script標籤引入的方式加載,可是這有一個問題就是若是存在多個腳本的時候,加載方式是同步加載,前一個加載未結束後面的腳本就不會加載,解決辦法就是實現異步加載
<script src='example1.js' async><script>
<script src='example1.js' defer><script>
// 它們二者之間的區別
async: 只要加載完畢就會當即執行
defer: 加載完畢等到整個頁面下載完畢纔會執行
若是出現多個加載腳本,defer會按照頁面中出現的順序執行的,可是async就不能確保了
複製代碼
- 加載規則
- 瀏覽器加載ES6模塊的時候也是使用script標籤,可是
type='module'
例如:<script type='module' src='example.js'></script>
這段代碼表示加載一個名字爲example.js的模塊
- 瀏覽器加載ES6模塊的時候,默認就是異步實現的,並且默認是defer的形式
- 對於外部的模塊腳本加載注意點:
- 代碼是在模塊做用域中運行的而不是在全局做用域中運行的,模塊內部的頂層變量外部是不可見的
- 模塊腳本自動採用嚴格模式
type='module'
的模塊之中可使用import
命令加載其餘的模塊,可是文件的後綴.js是不能省略的,路徑必須是相對路徑或者絕對路徑,也可使用export輸出對外的接口
- 在模塊中頂層的this關鍵字放回的undefined,而不是window
- 同一個模塊若是加載屢次將會只執行一次,特別注意
- CommonJS和ES6模塊之間的差別
- CommonJS模塊輸出的是一個值的複製,而ES6模塊輸出的是一個值的引用
- CommonJS模塊是運行時加載,ES6模塊是編譯時輸出對外的接口
- CommonJS只因此時運行時加載,由於它導出的是一個對象,經過
module.exports
該腳本只會在腳本運行結束的時候生成,而ES6的模塊是對外的接口是在編譯的時候就會生成
- Node加載
- Node有本身的commonJS格式,與ES6模塊的格式是不兼容的,目前的解決方案是將二者分開,ES6模塊和commonJS採用各自的加載方式。
- 目前掌握一種辦法就是在Node中繼續使用commonJS的規範,而在前端工程化中使用ES6的模塊方案
- CommonJS的加載原理:
- commonJS的一個模塊就是一個腳本文件,
require
命令第一次加載這個模塊的時候就會執行這個腳本,而後在內存中生成一個對象
- 因爲ES6輸入的是模塊變量只是一個「符號連接」,因此這個變量是隻讀的,對它進行從新的賦值會報錯
- ES6的模塊中,頂層的this指向undefined,CommonJS模塊的頂層this指向當前模塊,這是二者之間的重要差距
// 1.例如在main.js中加載foo.js模塊
const temp = require('./foo.js');
// commonJS的原理:內存中生成一個對象
{
id: foo.js, // id屬性是模塊的名字
exports: {
// 模塊輸出的各個接口都是存在這個對象中的
},
loaded: true, // 返回一個布爾值,表示該模塊的腳本是否執行完畢
... 還有一些不重要的屬性,這裏省略了
}
// 2.下面的這種方法會報錯
// lib.js
export let obj = {};
// main.js
import {obj} from './lib.js' // 後面的後綴是能夠省略的
obj.prop = 123; // OK
obj = {}; // 報錯
// 3.commonJS中模塊加載的尋找方式
1.const foo = require('./foo')
//依次尋找
// ./foo.js
// ./foo/package.json
// ./foo/index.js
2.const baz = require('baz')
// 依次尋找
// ./node_modules/baz.js
// ./node_modules/baz/package.json
// ./node_modules/bar/index.js
// 找不到開始尋找上一級目錄
// ../node_modules/baz.js
// ../node_modules/baz/package.json
// ../node_modules/baz/index.js
// 找不到再向上一級查找
複製代碼
- commonJS中exports和Module.exports的區別:
- exports是模塊對象Module.exports的一個屬性
- Module.exports纔是真正的接口,也就是說最終返回給調用者的是Module.exports而不是exports
- 全部的exports收集到的屬性和方法最終都是賦值給了Module.exports
- 若是賦值的方法和屬性Module.exports有,則會忽略exports的方法和屬性的