轉(zhuǎn)載自 JS模塊的導(dǎo)出和引入
在js編程中經(jīng)常會(huì)有模塊的導(dǎo)出導(dǎo)入,涉及到一些導(dǎo)入導(dǎo)出關(guān)鍵字
- 導(dǎo)出關(guān)鍵字
- module.exports
- exports
- export
- export default
- 導(dǎo)入關(guān)鍵字
- require
- const xxx = require("模塊名")
- import
- import { xxx } from "模塊名"
- import xxx from "模塊名"
- import xxx1, {xxx2, xxx3,...} from "模塊名"
- import * from "模塊名"
- require
因?yàn)樵趯?shí)際開發(fā)中經(jīng)常會(huì)混淆這些用法,所以想要弄清楚這些的區(qū)別,讓自己明白自己到底在寫什么。本文作為學(xué)習(xí)筆記輸出。
模塊規(guī)范
JS模塊化編程分了兩種規(guī)范:CommonJS模塊規(guī)范和ES6模塊規(guī)范。
- CommonJS模塊規(guī)范 —— CommonJS規(guī)范中,以module.exports導(dǎo)出接口,以require引入模塊
- ES6模塊規(guī)范 —— ES6標(biāo)準(zhǔn)規(guī)范中,以export指令導(dǎo)出接口,以import引入模塊
在Node.js編程中,Node模塊系統(tǒng)遵循的是CommonJS規(guī)范。
CommonJS模塊規(guī)范
CommonJS規(guī)范規(guī)定: 每個(gè)js文件就是一個(gè)模塊,有自己的作用域。
在一個(gè)文件里面定義的變量、函數(shù)、類,都是私有的,對(duì)其他文件不可見。
如果要暴露給其他程序,需要以module.exports導(dǎo)出接口,以require引入模塊。
module.exports 和 exports
module.exports / exports: 只有 Node 支持的導(dǎo)出
模塊導(dǎo)出的時(shí)候,導(dǎo)出的是module.exports,不是exports
module.exports可以導(dǎo)出所有的類型。對(duì)象,函數(shù),字符串、數(shù)值等。
每一個(gè)js文件通過node執(zhí)行時(shí),都自動(dòng)創(chuàng)建一個(gè)module變量和一個(gè)exports變量。
module變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對(duì)象,同時(shí),module對(duì)象會(huì)創(chuàng)建一個(gè)叫exports的屬性(即module.exports),該屬性初始化的值是 {},是對(duì)外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的module.exports屬性。
// test1.js
console.log(module)
復(fù)制代碼
執(zhí)行node test1.js的打印結(jié)果:
Module {
id: '.',
path: '/Users/xxxjuan/學(xué)習(xí)資料/Nodejs學(xué)習(xí)/demo-模塊編程',
exports: {},
parent: null,
filename: '/Users/xxxjuan/學(xué)習(xí)資料/Nodejs學(xué)習(xí)/demo-模塊編程/test1.js',
loaded: false,
children: [],
paths: [
'/Users/xxxjuan/學(xué)習(xí)資料/Nodejs學(xué)習(xí)/demo-模塊編程/node_modules',
'/Users/xxxjuan/學(xué)習(xí)資料/Nodejs學(xué)習(xí)/node_modules',
'/Users/xxxjuan/學(xué)習(xí)資料/node_modules',
'/Users/xxxjuan/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
// module.exports 表示模塊對(duì)外輸出的值。
// module.filename 模塊的文件名,帶有絕對(duì)路徑。
// module.loaded 返回一個(gè)布爾值,表示模塊是否已經(jīng)完成加載。
// module.children 返回一個(gè)數(shù)組,表示該模塊要用到的其他模塊。
// module.parent 返回一個(gè)對(duì)象,表示調(diào)用該模塊的模塊。
復(fù)制代碼
默認(rèn)exports變量是對(duì)module.exports的引用,即exports和module.exports指向同一個(gè)內(nèi)存塊。 這等同在每個(gè)模塊頭部,有一行這樣的命令。
var exports = module.exports;
復(fù)制代碼
- 當(dāng)通過exports去改變內(nèi)存塊里內(nèi)容時(shí),module.exports的值也會(huì)改變
- 當(dāng)通過module.exports去改變內(nèi)存塊里內(nèi)容時(shí),exports的值也會(huì)改變
- 當(dāng)module.exports被改變的時(shí)候,exports不會(huì)被改變
- 當(dāng)exports被改變的時(shí)候,module.exports不會(huì)被改變
所以,exports屬性的出現(xiàn)應(yīng)該可以直接向exports對(duì)象添加方法,從而方便對(duì)外輸出模塊接口。不過當(dāng)module.exports改變時(shí),exports與module.exports也就斷開了鏈接,所以最好不要采用這種方式,統(tǒng)一采用module.exports方式。
// module_export_demo.js
module.exports.a = 100
console.log("log1: " + exports.a) // log1: 100
exports.a = 200;
console.log("log2: " + module.exports.a) // log2: 200
module.exports = "hello"
console.log("log3: " + JSON.stringify(exports)) // log3: {"a":200}
復(fù)制代碼
module.exports可以導(dǎo)出所有的類型。對(duì)象,函數(shù),字符串、數(shù)值等。 語法示例:
// module_export_demo2.js
var x = 5
var str = "hello"
var addX = function (value) {
return value + x
};
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// module.exports.x = x
// module.exports.str = str
// module.exports.addX = addX
// module.exports.Point = Point
module.exports = {
x: x,
str: str,
addX: addX,
Point: Point
}
// require時(shí)對(duì)比下兩種方式x的值到底取哪個(gè)
exports = {
x: 10,
}
復(fù)制代碼
require
requirer用于加載模塊,是node的一個(gè)全局方法,使用非常簡單
const xxx = require("模塊名")
復(fù)制代碼
讀入并執(zhí)行一個(gè)JavaScript文件,返回模塊的exports對(duì)象。如果沒有發(fā)現(xiàn)指定模塊,會(huì)報(bào)錯(cuò)。
require方法接受以下幾種參數(shù)的傳遞:
- 原生模塊,如http、fs、path等
- 相對(duì)路徑的文件模塊,如./mod或../mod
- 絕對(duì)路徑的文件模塊,如 /pathtomodule/mod
- 第三方模塊,如koa等
在模塊目錄中通常有一個(gè)package.json文件,并且將入口文件寫入main字段
// package.json
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
復(fù)制代碼
require發(fā)現(xiàn)參數(shù)字符串指向一個(gè)目錄以后,會(huì)自動(dòng)查看該目錄的package.json文件,然后加載main字段指定的入口文件。
如果package.json文件沒有main字段,或者根本就沒有package.json文件,則會(huì)加載該目錄下的index.js文件或index.node文件
因?yàn)槟K導(dǎo)出的實(shí)際是module.exports,所以require只能看到通過 module.exports 導(dǎo)出的內(nèi)容,看不到通過exports導(dǎo)出的內(nèi)容。它相當(dāng)于module.exports的傳送門,module.exports后面的內(nèi)容是什么,require的結(jié)果就是什么,對(duì)象、數(shù)字、字符串、函數(shù)...再把require的結(jié)果賦值給某個(gè)變量。 針對(duì)上面的 module_export_domo2.js 文件,引入模塊示例
// node require_demo2.js
const demo2 = require("./module_export_demo2")
console.log(demo2.x) // 5 也證明了引入的是module.export的內(nèi)容
console.log(demo2.str) // hello
console.log(demo2.addX(15)) // 20 = 5 + 15
let point = new demo2.Point(3, 4)
console.log(point.toString()) // (3, 4)
復(fù)制代碼
require 是運(yùn)行時(shí)的,其參數(shù)可以是表達(dá)式
let value = 2
const demo2 = require("./module_export" + "_demo" + value)
復(fù)制代碼
require函數(shù)加載模塊
require函數(shù)加載模塊順序按照其在代碼中出現(xiàn)的順序
require函數(shù)加載模塊是同步的,只有加載完成,才能執(zhí)行后面的操作
-
require函數(shù)加載的模塊是被輸出的值的拷貝,不會(huì)受到模塊內(nèi)容變化影響
// module_export_demo3.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; 復(fù)制代碼const demo3 = require("./module_export_demo3") console.log(demo3.counter); // 3 demo3.incCounter(); console.log(demo3.counter); // 3 復(fù)制代碼counter輸出結(jié)果說明module_export_demo3模塊內(nèi)部的變化就影響不到counter了
-
模塊第一次被加載時(shí)會(huì)執(zhí)行一次,后續(xù)被加載時(shí)不會(huì)再執(zhí)行,都是從緩存中獲取的
// module_export_demo3.js console.log("hello") module.exports = "wrold" 復(fù)制代碼const demo3 = require("./module_export_demo3") const demo3_1 = require("./module_export_demo3") const demo3_2 = require("./module_export_demo3") const demo3_3 = require("./module_export_demo3") const demo3_4 = require("./module_export_demo3") const demo3_5 = require("./module_export_demo3") console.log(demo3) console.log(demo3_1) // hello // wrold // wrold 復(fù)制代碼hello只會(huì)被打印一次說明console.log("hello")語句只執(zhí)行了一次,module_export_demo3.js只被加載了一次。
ES6模塊規(guī)范
ES6發(fā)布的module并沒有直接采用CommonJS,甚至連require都沒有采用,也就是說require仍然只是node的一個(gè)私有的全局方法,module.exports也只是node私有的一個(gè)全局變量屬性,跟標(biāo)準(zhǔn)什么關(guān)系都沒有。
ES6模塊規(guī)范是,在創(chuàng)建JS模塊時(shí),export 語句用于從模塊中導(dǎo)出函數(shù)、對(duì)象或原始值,以便其他程序可以通過 import 語句使用它們。
export 和 export default
ES6模塊導(dǎo)出有兩種方式:export(命名導(dǎo)出) 和 export default(默認(rèn)導(dǎo)出)。 在導(dǎo)出多個(gè)值時(shí),命名導(dǎo)出非常有用。在導(dǎo)入時(shí),必須使用相應(yīng)對(duì)象的相同名稱。但是,可以使用任何名稱導(dǎo)入默認(rèn)導(dǎo)出。
// 導(dǎo)出單個(gè)特性
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function FunctionName(){...}
export class ClassName {...}
// 導(dǎo)出列表
export { name1, name2, …, nameN };
// 重命名導(dǎo)出
export { variable1 as name1, variable2 as name2, …, nameN };
// 默認(rèn)導(dǎo)出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// 導(dǎo)出"引入模塊的導(dǎo)出值"
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export * from …; // 導(dǎo)出"引入模塊的所有導(dǎo)出值",不包括模塊的默認(rèn)導(dǎo)出值
export { default } from …; // 導(dǎo)出"引入模塊的默認(rèn)導(dǎo)出值"
復(fù)制代碼
- export與export default均可用于導(dǎo)出常量、函數(shù)、文件、模塊等
- 通過export方式導(dǎo)出,在導(dǎo)入時(shí)要加{ },export default則不需要
- 在一個(gè)文件中,export可以有多個(gè),export default僅有一個(gè)
- 大部分風(fēng)格建議,模塊中最好在末尾用一個(gè)export導(dǎo)出所有的接口
export 1 // 這種寫法錯(cuò)誤
// 正確的寫法
const value = 1
export { value }
// 或者
export const value = 1
// 或者
const value = 1
export default value
// 或者
export default 1
復(fù)制代碼
export default是別名的語法糖,這個(gè)語法糖的好處在是
- import的時(shí)候,可以省去花括號(hào){}。
- import的時(shí)候, 可以起任何變量名表示引入變量
所以如果import的時(shí)候,你發(fā)現(xiàn)某個(gè)變量沒有花括號(hào)括起來(除了* 號(hào)),是因?yàn)樵撟兞渴峭ㄟ^export default 導(dǎo)出的。
// d.js
// 導(dǎo)出函數(shù)
export default function() {}
// 等效于:
// function a() {};
// export {a as default};
復(fù)制代碼
import a from "d.js" // a 是 {defalut as a}的替代寫法。
復(fù)制代碼
所以使用export default命令,為模塊指定默認(rèn)輸出,這樣就不需要知道所要加載模塊的變量名。
// a.js
let sex = "boy";
export default sex //sex不能加大括號(hào) 等價(jià)于 export {sex as default}
復(fù)制代碼
本質(zhì)上,a.js文件的export default輸出一個(gè)叫做default的變量,然后系統(tǒng)允許你為它取任意名字。 自然default只能有一個(gè)值,所以一個(gè)文件內(nèi)不能有多個(gè)export default。
// b.js
import any from "./a.js"
import any12 from "./a.js"
console.log(any, any12) // boy,boy
復(fù)制代碼
import
require 和 import是完全不同的兩種概念。require是賦值過程,import是解構(gòu)過程 const xxx = require("模塊名") import { xxx } from "模塊名"
- import是編譯時(shí)的, 必須放在文件開頭,否則會(huì)報(bào)錯(cuò)
- import后面跟上花括號(hào)的形式是最基本的用法,花括號(hào)里面的變量與export后面的變量一一對(duì)應(yīng)。
import {a} from ..
復(fù)制代碼
- 支持給變量取別名。因?yàn)橛械臅r(shí)候不同的兩個(gè)模塊可能有相同的接口,可以給這個(gè)變量取一個(gè)別名,方便在當(dāng)前的文件里面使用。
import {a as a_a} from ..
復(fù)制代碼
import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name"); // 動(dòng)態(tài)模塊加載,返回的是一個(gè)promise對(duì)象
復(fù)制代碼
Node為何支持export / import
我們經(jīng)常會(huì)看到在node中也會(huì)用export / import,這是什么呢? 我們?cè)趎ode中使用babel支持ES6,僅僅是將ES6轉(zhuǎn)碼為ES5再執(zhí)行,import語法會(huì)被轉(zhuǎn)碼為require。因?yàn)槟壳八械囊娑歼€沒有實(shí)現(xiàn)export / import。
如何讓Node.js支持ES6的語法具體參考在node環(huán)境中支持ES6代碼
// ES6語法
import {a} from "./demo.js"
// 轉(zhuǎn)碼ES5后
var _demo = require("./demo.js")
復(fù)制代碼
這也是為什么在使用module.exports模塊導(dǎo)出時(shí),在引入模塊時(shí)使用import仍然起效,因?yàn)楸举|(zhì)上,import會(huì)被轉(zhuǎn)碼為require去執(zhí)行。
總結(jié)
CommonJS規(guī)范中,建議盡量都用 module.exports 導(dǎo)出,然后用require導(dǎo)入 ES6規(guī)則中,大部分風(fēng)格建議,模塊中最好在末尾用一個(gè)export導(dǎo)出所有的接口
- module.exports / exports: 只有 Node 支持的導(dǎo)出
- require: Node 和 ES6 都支持的引入
- export / import : 只有ES6 支持的導(dǎo)出引入