一、前言
目前主流的模塊規範:
1、UMD通用模塊
2、CommonJs
3、es6 module
二、UMD模塊(通用模塊)
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.libName = factory());
}(this, (function () { 'use strict';})));
如果你在js文件的頭部看到這樣的代碼,這個js文件使用的規範就是UMD規範;
什麼是UMD模塊規範?就是AMD+CommonJs+全局變量的組合規範。
這段代碼用來判斷當前的運行環境,如果是node環境,就會使用CommonJs規範,然後判斷是否是AMD環境,是的話就會使用AMD規範,最後導出全局變量
有了UMD後我們的代碼可以同時運行在node和瀏覽器上。現在前端多數的庫最後打包都是使用UMD規範。
二、CommonJs
nodejs的運行環境使用的模塊系統就是基於CommonJs規範實現的,我們現在所說的ComonJs規範大多是指的node的模塊系統。
2.1模塊導出
關鍵字:module.exports,exports
// foo.js
//一個一個 導出
module.exports.age = 1
module.exports.foo = function(){}
exports.a = 'hello'
//整體導出
module.exports = { age: 1, a: 'hello', foo:function(){} }
//整體導出不能用`exports` 用exports不能在導入的時候使用
exports = { age: 1, a: 'hello', foo:function(){} }
注意:使用exports導出不能被賦值,因爲賦值之後,exports失去了對module.exports的引用,成偉一個模塊內的局部變量。
2.2模塊導入
關鍵字:require
const foo = require('./foo.js');
console.log(foo.age); //1
2.2.1模塊導入規則
假設在目錄src/app/index.js的文件,調用require()。
./moduleA 相對路徑開頭
在沒有指定後綴名的情況下:
1、先去尋找同級目錄同級目錄:src/app/
2、同級目錄沒有moduleA文件會去找同級的moduleA目錄:src/app/moduleA
結束
/module/moduleA絕對路徑開頭
直接在/module/moduleA目錄中尋找,規則同上
注意:react沒有路徑開頭
沒有路徑開頭則視爲導入一個包,會首先判斷moduleA是否是一個核心模塊,例如path,http,優先導入核心模塊,不是核心模塊,會從當前文件的同級目錄下的node_modules尋找。
2.3require wrapper
node的模塊,實際上可以理解爲代碼被包裹在一個函數包裝器內
function wrapper (script) {
return '(function (exports, require, module, __filename, __dirname) {' +
script +
'\n})'
}
function require(id) {
var cachedModule = Module._cache[id];
if(cachedModule){
return cachedModule.exports;
}
const module = { exports: {} }
// 這裏先將引用加入緩存 後面循環引用會說到
Module._cache[id] = module
//當然不是eval這麼簡單
eval(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname')
return module.exports
}
也可以查看:node module 源碼
我們可以知道:
1、模塊只執行一次,之後調用獲取的module.exports都是在緩存中,哪怕這個js沒有執行完(因爲先加入緩存後加入模塊)。
2、模塊導出就是return 這個變量,其實跟賦值一樣,基本類型導出的是值,引用類型導出的是引用地址。
3、exports和module.exports持有相同的引用,因爲最後導出的是module.exports,所以對於exports進行賦值會導致exports操作的而不再是module.exports的引用。
2.4循環引用
// a.js
module.exports.a = 1
var b = require('./b')
console.log(b)
module.exports.a = 2
// b.js
module.exports.b = 11
var a = require('./a')
console.log(a)
module.exports.b = 22
//main.js
var a = require('./a')
console.log(a)
運行此段代碼結合上面的require demo,分析一下:
1、執行 node main.js -> 第一行 require(a.js),(node 執行也可以理解爲調用了require方法,我們省略require(main.js)內容);
2、進入 require(a)方法: 判斷緩存(無) -> 初始化一個 module -> 將 module 加入緩存 -> 執行模塊 a.js 內容,(需要注意 是先加入緩存, 後執行模塊內容)
3、a.js: 第一行導出 a = 1 -> 第二行 require(b.js)(a 只執行了第一行)
4、進入 require(b) 內 同 1 -> 執行模塊 b.js 內容
5、b.js: 第一行 b = 11 -> 第二行 require(a.js)
6、require(a) 此時 a.js 是第二次調用 require -> 判斷緩存(有)-> cachedModule.exports -> 回到 b.js(因爲js對象引用問題 此時的 cachedModule.exports = { a: 1 })
7、b.js:第三行 輸出 { a: 1 } -> 第四行 修改 b = 22 -> 執行完畢回到 a.js
8、a.js:第二行 require 完畢 獲取到 b -> 第三行 輸出 { b: 22 } -> 第四行 導出 a = 2 -> 執行完畢回到 main.js
9、main.js:獲取 a -> 第二行 輸出 { a: 2 } -> 執行完畢
以上就是node
的module
模塊解析和運行的大致規則
三、es6 module
ES6 之前 javascript 一直沒有屬於自己的模塊規範,所以社區制定了 CommonJs規範, Node 從 Commonjs 規範中借鑑了思想於是有了 Node 的 module,而 AMD 異步模塊 也同樣脫胎於 Commonjs 規範,之後有了運行在瀏覽器上的 require.js
es6 module 基本語法:
3.1 export
export * from 'module'; //重定向導出 不包括 module內的default
export { name1, name2, ..., nameN } from 'module'; // 重定向命名導出
export { import1 as name1, import2 as name2, ..., nameN } from 'module'; // 重定向重命名導出
export { name1, name2, …, nameN }; // 與之前聲明的變量名綁定 命名導出
export { variable1 as name1, variable2 as name2, …, nameN }; // 重命名導出
export let name1 = 'name1'; // 聲明命名導出 或者 var, const,function, function*, class
export default expression; // 默認導出
export default function () { ... } // 或者 function*, class
export default function name1() { ... } // 或者 function*, class
export { name1 as default, ... }; // 重命名爲默認導出
export規則:
1、export * from '' 或者 export {} from '',重定向導出,重定向的命名並不能在本模塊使用,只是搭建一個橋樑,例如:這個a並不能在本模塊內使用
2、export {}, 與變量名綁定,命名導出
3、export Declaration,聲明的同時,命名導出, Declaration就是: var, let, const, function, function*, class 這一類的聲明語句
4、export default AssignmentExpression,默認導出, AssignmentExpression的 範圍很廣,可以大致理解 爲除了聲明Declaration(其實兩者是有交叉的),a=2,i++,i/4,a===b,obj[name],name in obj,func(),new P(),[1,2,3],function(){}等等很多
3.2 import
// 命名導出 module.js
let a = 1,b = 2
export { a, b }
export let c = 3
// 命名導入 main.js
import { a, b, c } from 'module'; // a: 1 b: 2 c: 3
import { a as newA, b, c as newC } from 'module'; // newA: 1 b: 2 newC: 3
// 默認導出 module.js
export default 1
// 默認導入 main.js
import defaultExport from 'module'; // defaultExport: 1
// 混合導出 module.js
let a = 1
export { a }
const b = 2
export { b }
export let c = 3
export default [1, 2, 3]
// 混合導入 main.js
import defaultExport, { a, b, c as newC} from 'module'; //defaultExport: [1, 2, 3] a: 1 b: 2 newC: 3
import defaultExport, * as name from 'module'; //defaultExport: [1, 2, 3] name: { a: 1, b: 2, c: 3 }
import * as name from 'module'; // name: { a: 1, b: 2, c: 3, default: [1, 2, 3] }
// module.js
Array.prototype.remove = function(){}
//副作用 只運行一個模塊
import 'module'; // 執行module 不導出值 多次調用module.js只運行一次
//動態導入(異步導入)
var promise = import('module');
import 規則:
1、import { } from 'module', 導入module.js的命名導出
2、import defaultExport from 'module', 導入module.js的默認導出
3、import * as name from 'module', 將module.js的的所有導出合併爲name的對象,key爲導出的命名,默認導出的key爲default
4、import 'module',副作用,只是運行module,不爲了導出內容例如 polyfill,多次調用次語句只能執行一次
5、import('module'),動態導入返回一個 Promise,TC39的stage-3階段被提出 tc39 import
3.3 es6 module 特點
3.3.1 es6 module語法是靜態的
import
會自動提升到代碼的頂層。
export
和 import
只能出現在代碼的頂層,下面這段語法是錯誤的。
//if for while 等都無法使用
{
export let a = 1
import defaultExport from 'module'
}
true || export let a = 1
import
的導入名不能爲字符串或在判斷語句,下面代碼是錯誤的:
import 'defaultExport' from 'module'
let name = 'Export'
import 'default' + name from 'module'
靜態的語法意味着可以在編譯時確定導入和導出,更加快速的查找依賴,可以使用lint
工具對模塊依賴進行檢查,可以對導入導出加上類型信息進行靜態的類型檢查
3.3.2 es6 module的導出是綁定的
使用 import
被導入的模塊運行在嚴格模式下。
使用 import
被導入的變量是只讀的,可以理解默認爲 const
裝飾,無法被賦值。
使用 import
被導入的變量是與原變量綁定/引用的,可以理解爲 import
導入的變量無論是否爲基本類型都是引用傳遞。
// js中 基礎類型是值傳遞
let a = 1
let b = a
b = 2
console.log(a,b) //1 2
// js中 引用類型是引用傳遞
let obj = {name:'obj'}
let obj2 = obj
obj2.name = 'obj2'
console.log(obj.name, obj2.name) // obj2 obj2
// es6 module 中基本類型也按引用傳遞
// foo.js
export let a = 1
export function count(){
a++
}
// main.js
import { a, count } from './foo'
console.log(a) //1
count()
console.log(a) //2
上面這段代碼就是 CommonJs
導出變量 和 ES6
導出變量的區別
3.4 es6 module 循環引用
// bar.js
import { foo } from './foo'
console.log(foo);
export let bar = 'bar'
// foo.js
import { bar } from './bar'
console.log(bar);
export let foo = 'foo'
// main.js
import { bar } from './bar'
console.log(bar)
分析:
1、執行 main.js -> 導入 bar.js;
2、bar.js -> 導入 foo.js;
3、foo.js -> 導入 bar.js -> bar.js 已經執行過直接返回 -> 輸出 bar -> bar is not defined, bar 未定義報錯
我們可以使用function
的方式解決:
// bar.js
import { foo } from './foo'
console.log(foo());
export function bar(){
return 'bar'
}
// foo.js
import { bar } from './bar'
console.log(bar());
export function foo(){
return 'foo'
}
// main.js
import { bar } from './bar'
console.log(bar)
因爲函數聲明會提示到文件頂部,所以就可以直接在 foo.js
調用還沒執行完畢的bar.js
的 bar
方法
四、CommonJs與es6 module的區別
從上面能夠知道一些區別:
1、CommonJs導出的是變量的一份拷貝,ES6 Module導出的是變量的綁定(引用);
2、CommonJs是單個值導出,ES6 Module可以導出多個;
3、CommonJs是動態語法可以寫在判斷裏,ES6 Module靜態語法只能寫在頂層;
4、CommonJs的 this 是當前模塊,ES6 Module的 this 是 undefined。
五、易混淆點
5.1模塊語法與解構
module語法
與解構語法
很容易混淆,例如:
import { a } from 'module'
const { a } = require('module')
儘管看上去很像,但是不是同一個東西,這是兩種完全不一樣的語法與作用,ps:兩個人撞衫了,穿一樣的衣服你不能說這倆人就是同一個人1、module
的語法: 上面有寫 import/export { a } / { a, b } / { a as c} FromClause
2、解構
的語法:
let { a } = { a: 1 }
let { a = 2 } = { }
let { a: b } = { a: 1 }
let { a: b = 2, ...res } = { name:'a' }
let { a: b, obj: { name } } = { a: 1, obj: { name: '1' } }
function foo({a: []}) {}
他們是差別非常大的兩個東西,一個是模塊導入導出,一個是獲取對象的語法糖