【JavaScript系列】深入javascript的主流的模塊規範

一、前言

目前主流的模塊規範:

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 } -> 執行完畢

以上就是nodemodule模塊解析和運行的大致規則

 

三、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: []}) {}

他們是差別非常大的兩個東西,一個是模塊導入導出,一個是獲取對象的語法糖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章