沒有網(wǎng)站限制的瀏覽器臺(tái)州網(wǎng)站建設(shè)
😁 作者簡(jiǎn)介:一名大四的學(xué)生,致力學(xué)習(xí)前端開發(fā)技術(shù)
??個(gè)人主頁:夜宵餑餑的主頁
? 系列專欄:JavaScript進(jìn)階指南
👐學(xué)習(xí)格言:成功不是終點(diǎn),失敗也并非末日,最重要的是繼續(xù)前進(jìn)的勇氣
?🔥?前言:
本篇是關(guān)于js中最常用的模塊語法,import和export命令的使用細(xì)節(jié),暴露和導(dǎo)出js語法時(shí)應(yīng)該注意什么,這非常重要,了解到這些細(xì)節(jié),會(huì)讓js語法代碼更加的嚴(yán)謹(jǐn)和健壯,希望可以幫助到大家,歡迎大家的補(bǔ)充和糾正
🌻如果有想要了解模塊加載機(jī)制中具體的實(shí)現(xiàn)可以看我的博客:JavaScript中的模塊Module的加載實(shí)現(xiàn):循環(huán)加載和Node加載
文章目錄
- 第22章 Module語法
- 22.1 概述
- 22.2 嚴(yán)格模式
- 22.3 export命令
- 22.4 import命令
- 22.5 模塊的整體加載
- 22.6 export defaule命令
- 22.7 export與import的復(fù)合寫法
- 22.8 模塊的繼承
- 22.9 跨模塊常量
- 22.10 import()
- 22.10.1 簡(jiǎn)介
- 22.10.2 適用場(chǎng)合
- 22.10.3 注意點(diǎn)
第22章 Module語法
22.1 概述
? 在ES6之前,社區(qū)制定了一些模塊加載方案,最主要的有CommonJS和AMD兩種,前者用于服務(wù)器,后者用于瀏覽器。ES6在語言規(guī)格的層面上實(shí)現(xiàn)了模塊功能,而且實(shí)現(xiàn)的相當(dāng)簡(jiǎn)單,完全可以取代現(xiàn)有的CommonJS和AMD規(guī)范,稱為瀏覽器和服務(wù)器通用的模塊解決方案
背景:
? JavaScript一直沒有模塊體系,無法將大程序拆分成互相依賴的小文件,再用簡(jiǎn)單的方法將它們拼裝起來,其他語言都有這項(xiàng)功能,比如Runby的require,Python的import,甚至連CSS都有@import,但是JavaScript沒有任何對(duì)這方面的支持,這對(duì)于開發(fā)大型,復(fù)雜的項(xiàng)目而言是一個(gè)巨大的障礙
? ES6模塊設(shè)計(jì)思想是想盡量靜態(tài)化,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量,CommonJS和AMD模塊都只能在運(yùn)行是確定這些東西,比如CommonJS模塊就是對(duì)象,輸入時(shí)必須查找對(duì)象屬性
//CommonJS模塊
let {stat,exists,readFile} = require('fs')//等同于
let _fs=require('fs')
let stat=_fs.stat
let exists=_fs.exists
let readfile=_fs.readfile
? 上面代碼的實(shí)質(zhì)是整體加載fs模塊(即加載fs所有的方法),生成一個(gè)對(duì)象(_fs),然后再從這個(gè)對(duì)象上讀取3個(gè)方法,這種加載稱為“運(yùn)行時(shí)加載”,因?yàn)橹挥羞\(yùn)行時(shí)才能得到這個(gè)對(duì)象,導(dǎo)致完全沒辦法在編譯時(shí)進(jìn)行“靜態(tài)優(yōu)化”
? ES6模塊不是對(duì)象,而是通過export命令顯式指定輸出的代碼,再通過import命令輸入
//ES6模塊
import {stat,exists,readFile} from 'fs'
? 上面的代碼的實(shí)質(zhì)是從fs模塊加載3個(gè)方法,而不加載其他方法,這種加載稱為“編譯時(shí)加載”或者靜態(tài)加載,即ES6可以再編譯時(shí)就完成模塊加載,效率比CommonJS模塊的加載方式高,當(dāng)然,這也導(dǎo)致ES6模塊本身無法被引用,因?yàn)樗皇菍?duì)象
? 由于ES6模塊時(shí)編譯時(shí)加載,使得靜態(tài)分析成為可能,有了它就能進(jìn)一步拓展JavaScript的語法,比如引入宏和類型檢驗(yàn)這些只能靠靜態(tài)分析實(shí)現(xiàn)的功能。
? 除了靜態(tài)加載帶來的各種好處,ES6模塊還有以下的好處
- 不再需要UMD模塊格式,將來服務(wù)器和瀏覽器都會(huì)支持ES6模塊格式
- 將來瀏覽器的新API可以用模塊格式提供,不再需要做成全局變量
- 不再需要對(duì)象作為命名空間(Math對(duì)象),未來這些功能可以通過加載模塊來提供
22.2 嚴(yán)格模式
ES的模式自動(dòng)采用嚴(yán)格模式,不過有沒有在模塊頭部加上use strict
嚴(yán)格模式主要有以下限制
- 變量必須聲明后再使用
- 函數(shù)的參數(shù)不能有同名屬性,否則報(bào)錯(cuò)
- 不能使用with語句
- 不能對(duì)只讀屬性賦值,否則報(bào)錯(cuò)
- 不能使用前綴0表示八進(jìn)制,否則報(bào)錯(cuò)
- 不能刪除不可刪除的屬性,否則報(bào)錯(cuò)
- 不能刪除變量 delete prop,會(huì)報(bào)錯(cuò),只能刪除樹叢delete global[prop]
- eval不會(huì)再它的外層作用域引入變量
- eval和arguments不能被重新賦值
- arguments不會(huì)自動(dòng)反映函數(shù)參數(shù)的變化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局對(duì)象
- 不能使用fn.caller和fn.arguments獲取函數(shù)調(diào)用的堆棧
- 增加保留字(比如protected,static 和 interface)
?? 注意:尤其需要注意this限制,在ES6模塊之中。頂層的this指向undefined,即不應(yīng)該在頂層代碼中使用this
22.3 export命令
1 語法
模塊功能主要有兩個(gè)命令組成:export和import。export命令用于規(guī)定模塊的對(duì)外接口,import命令用于輸入其他模塊提供的功能
一個(gè)模塊就是一個(gè)獨(dú)立文件,該文件內(nèi)部的所有變量,外部無法獲取,如果希望外部能夠讀取模塊內(nèi)部的某個(gè)變量,就必須使用export關(guān)鍵字輸出該變量
-
單個(gè)語句,逐條暴露
//profile.js//輸出變量 export var firstName='Michael' export var lastName='Jackson' export var year=1958//輸出函數(shù)或者類 export fuction multiply(x,y){return x*y }
-
多條語句,統(tǒng)一暴露(?? 推薦這種寫法)
//profile.jsvar firstName='Michael'var lastName='Jackson'var year=1958export {firstName.lastName,year}
-
通常情況下,export輸出的變量就是本來的名字,但是可以使用as關(guān)鍵字重命名
function v1(){} function v2(){}export {v1 as streamV1,v2 as streamV2,v2 as streamLatestVersion }
上面的代碼使用as關(guān)鍵字重命名了函數(shù)v1和v2的對(duì)外接口,重命名后,v2可以用不同的名字輸出兩次
2. 注意點(diǎn):
-
export命令規(guī)定的是對(duì)外的接口,必須與模塊內(nèi)部的變量建立一一對(duì)應(yīng)關(guān)系
//報(bào)錯(cuò) export 1;//報(bào)錯(cuò) var m =1 export m;//正確的寫法//寫法一 export var m=1//寫法二 var m=1 export {m}//寫法三 var m=1 export{n as m}
上面兩種寫法都會(huì)報(bào)錯(cuò),因?yàn)闆]有提供對(duì)外的接口,第一種寫法直接輸出1,第二種寫法通過變量m依然直接輸出1,1只是一個(gè)值,不是接口
上面3中寫法都是正確,規(guī)定了對(duì)外的接口m
-
export語句輸出的接口與其對(duì)應(yīng)的值是動(dòng)態(tài)綁定關(guān)系,即通過該接口可以取到模塊內(nèi)部實(shí)時(shí)的值
export var foo='bar'; setTimeout(()=>foo='baz',500)
上面的代碼輸出變量foo,值為bar,500ms之后變成baz
-
export命令可以出現(xiàn)在模塊的任何位置,只要處于模塊頂層就可以,如果處理塊級(jí)作用域內(nèi),就會(huì)報(bào)錯(cuò),下一節(jié)的import命令也是如此,這是因?yàn)樘幱跅l件代碼塊之中,就沒法做靜態(tài)優(yōu)化,違背模塊設(shè)計(jì)初衷
function foo(){export default 'bar' //SyntaxError }foo()
22.4 import命令
1. 語法
使用export命令定義了模塊的對(duì)外接口以后,其他的JS文件就可以通過import命令加載這個(gè)模塊了
//main.js
import {firstName,lastName,year} from './profile'\function setName(element){element.textContent=firstName + ' '+lastName
}
上面的import命令用于加載profile.js文件,并從中輸入變量,import命令接受一個(gè)對(duì)象(用大括號(hào)表示),里面指定要從其他模塊導(dǎo)入的變量名,大括號(hào)中的變量名必須與被導(dǎo)入模塊(profile.js)對(duì)外接口名稱相同
如果想為輸入變量重新去個(gè)名字,要在import命令中使用as關(guān)鍵字,將輸入的變量重命名
import {lastName as surname} from './profile'
如果執(zhí)行所加載的模塊,可以這么寫
import 'lodash'
上面的代碼僅僅執(zhí)行l(wèi)odash模塊,但是不會(huì)輸入任何值
2. 注意點(diǎn):
-
import 后面的from 指定模塊文件的位置,可以是相對(duì)路徑,也可以是絕對(duì)路徑,.js后綴可以省略,如果只是模塊名,不帶有路徑,那么必須有配置文件告訴JavaScript引擎該模塊的位置
-
import命令具有提升效果,會(huì)提示到整個(gè)模塊的頭部首先執(zhí)行
foo()import {foo} from 'my_module'
-
由于import是靜態(tài)執(zhí)行,所以不能使用表達(dá)式和變量,只有在運(yùn)行是才能得到結(jié)果的語法結(jié)構(gòu)
//報(bào)錯(cuò) import {'f'+'oo'} from 'my_module'//報(bào)錯(cuò) let module='my_module' import {foo} from module//報(bào)錯(cuò) if(x === 1{import {foo} from 'module1 } else{import {foo} from 'module2' }
-
如果多次重復(fù)執(zhí)行同一句import語句,那么只會(huì)執(zhí)行一次,而不會(huì)執(zhí)行多次
22.5 模塊的整體加載
處理指定加載某個(gè)輸出值,還可以使用整體加載(即星號(hào)*)來指定一個(gè)對(duì)象,所有輸出值都加載在這個(gè)對(duì)象上
//circle.jsexport function area(radius){return Math.PI*radius*radius
}export function circumference(radius){return 2*Math.PI*radius
}//加載模塊,逐一加載
import {area,circumference} from './circle'console.log('圓面積:'+area(4))
console.log('圓周長:'+circumference(14))//統(tǒng)一加載
import * as circle from './circle'console.log('圓面積:'+circle.area(4))
console.log('圓周長:'+circle.circumference(14))
?? 注意:模塊的整體加載所在對(duì)象(上例是circle)應(yīng)該是可以靜態(tài)分析的,所以不允許運(yùn)行時(shí)改變。
import * as circle from './circle'//下面兩行都是不允許的
circle.foo='hello'
circle.area=function(){}
22.6 export defaule命令
? 從前面的例子可以看出來,使用import命令時(shí)用戶需要知道所要加載的變量名或函數(shù)名,否則無法加載,但是,用戶肯定希望快速上手,未必由于閱讀文檔去了解模塊有哪些屬性和方法
? 為了方便用戶,使其不用閱讀文檔就能加載模塊,可以使用export default命令為模塊指定默認(rèn)輸出。
export default function(){console.log('foo')
}import customName from './export-default'
customName() //'foo'
上的代碼中,就不需要知道原模塊輸出的函數(shù)名,需要注意的是,這時(shí)import命令后面不使用大括號(hào),而關(guān)于輸出語句的寫法可以有下面兩種方式
export default function foo(){console.log('foo')
}//或者寫成function foo(){console.log('foo')
}export default foo
上面的代碼中的foo函數(shù)的函數(shù)名foo在模塊外部是無效的,加載時(shí)視為匿名函數(shù)
📝 使用細(xì)節(jié):
-
export default命令用于指定模塊的默認(rèn)輸出,顯然,一個(gè)模塊只能有一個(gè)默認(rèn)輸出,因此,export default命令只能使用一次,所以import命令后面的才不用加大括號(hào),因?yàn)橹豢赡軐?duì)應(yīng)一個(gè)方法
-
如果想在一條import語句中同時(shí)輸入默認(rèn)方法和其他接口,可以寫成下面這樣:
import _,{each,each as forEach} from 'lodsh'//對(duì)應(yīng)的代碼語句 export default function(obj){//... }export function each(obj,iterator,context){//... }export {each as forEach}
?? export default語句的本質(zhì)
本質(zhì)上,export default就是輸出一個(gè)叫作default的變量或者方法,然后系統(tǒng)允許我們?yōu)樗∪我饷?#xff0c;所以,下面的寫法是有效的
//modules.js
function add(x,y){return x*y
}
export {add as default}//等同于
//export default add//app.js
import{ default as xxx } from 'modules'
//等同于
import xxx from 'modules'
正是因?yàn)閑xport default命令其實(shí)只是輸出一個(gè)叫作default的變量,所以它后面不能跟變量聲明語句
//正確
export var a=1//正確
var a=1
export default a;//錯(cuò)誤
export default var a=1
上面的代碼中,export default a的含義是將變量a的值賦給變量default,所以最后一種寫法會(huì)報(bào)錯(cuò)
同樣,因?yàn)閑xport default本質(zhì)是將該命令后面的值賦給default變量以后再默認(rèn),所以直接將一個(gè)值寫在export default之后
//正確
export default 42//報(bào)錯(cuò)
export 42
22.7 export與import的復(fù)合寫法
如果模式之中先輸入后輸出同一個(gè)模塊,import語句可以與export語句寫在一起
export {foo,bar} from 'my_module'//等同于
import{foo,bar} from 'my_module'
export {foo,bar}
上面的代碼中,export和import可以結(jié)合寫在一起寫成一行
模塊的接口改名和整體輸出也可以采用這種寫法
//接口改名
export {foo as myFoo} from 'my_module'//整體輸出
export * from 'my_module'
📝 使用細(xì)節(jié):
-
默認(rèn)接口的寫法如下:
export {default} from 'foo'
-
具名接口改為默認(rèn)接口的寫法如下
export {es6 as default} from './someModule'//等同于 import {es6} from './someModule' export default es6
-
默認(rèn)接口也可以改為具名接口
export {default as es6} from './someModule'
-
有三種import語句沒有對(duì)應(yīng)的復(fù)合寫法
import * as someIdentifier from 'someModule' import someIdentifier from 'someModule' import someIdentifier,{ namedIentifier } from 'someModule'
22.8 模塊的繼承
模塊之間也可以繼承
假設(shè)有一個(gè)circleplus模塊繼承了circle模塊
//circleplus.js
export * from 'circle'
export var e=2.718
export default function(x){return Math.exp(x)
}
上面的export * 表示輸出circle模塊的所有屬性和方法
?? 注意:export命令會(huì)忽略circle模塊的default方法,之后,又輸出了自定義的e變量和默認(rèn)方法
加載上面模塊的寫法
import * as math from 'circleplus'
import exp from 'circleplus'
console.log(exp(math.e))
上面代碼中的import exp表示,將circleplus模塊的默認(rèn)方法加載為exp方法
22.9 跨模塊常量
前面介紹const 命令的時(shí)候說過,const聲明的常量只能在當(dāng)前代碼塊內(nèi)有效,如果想設(shè)置跨模塊的常量(即跨多個(gè)文件),或者說一個(gè)值、要被多個(gè)模塊共享,可以采用下面的寫法
//constants.js模塊
export const A=1
export const B=2
export const C=3//test1.js模塊
import * as constants from './constants'
console.log(constants.A) //1
console.log(constants.B) //2//test2.js模塊
import {A,B} from './constants'
console.log(A) //1
console.log(C) //3
🌻 小建議:如果要使用的常量非常多,可以建立一個(gè)專門的constants目錄,將各種常量寫在不同的文件里面并保存在該目錄下
export const db={url:'https://www.csdn.net/',admin_username:'夜宵餑餑',admin_password:'xxxxxxxx'
}//constants/user.jd
export const users=['root','admin','staff','cex','chief']
然后將這些文件輸出的常量合并在index.js中
//constants/index.js
export {db} from './db'
export {user} from './users'
使用的時(shí)候,直接加載index.js即可
//script.js
import {db,users} from './constants'
22.10 import()
22.10.1 簡(jiǎn)介
import命令會(huì)被JavaScript引擎靜態(tài)分析,先于模塊內(nèi)的其他模塊執(zhí)行(稱為連接更合適),所以下面的代碼會(huì)報(bào)錯(cuò)
//報(bào)錯(cuò)
if(x === 2){import MyModule from './myModule'
}
上面的代碼中,引擎處理import語句是在編譯時(shí),這時(shí)不會(huì)分析或執(zhí)行if語句,所以import語句放在if代碼塊之中毫無意義,因此會(huì)報(bào)句法錯(cuò)誤,而不是執(zhí)行時(shí)錯(cuò)誤
這樣的設(shè)計(jì)固然有利于編譯器提高效率,但也導(dǎo)致無法在運(yùn)行時(shí)加載模塊。在語法上,條件加載不可能實(shí)現(xiàn)。
因此,有一個(gè)提案(現(xiàn)已經(jīng)實(shí)現(xiàn)),建議引入import()函數(shù),完成動(dòng)態(tài)加載。
import(specifier)
import函數(shù)的參數(shù)specifier指定要加載的模塊的位置,import命令能夠接受什么參數(shù),import()函數(shù)就可以接受什么參數(shù),后者為動(dòng)態(tài)加載。
import()函數(shù)返回一個(gè)Promise對(duì)象,下面是一個(gè)例子
const main=document.querySelector('main')import('./section-module/${someVariable}.js')
.then(module => {module.loadPageInto(main)
})
.catch(err => {main.textContent=err.message
})
import()函數(shù)可以用在任何地方,不僅僅是模塊,非模塊的腳本也可以使用,它是運(yùn)行時(shí)執(zhí)行,也就是說,運(yùn)行到這一句便會(huì)加載模塊。另外,import()函數(shù)所加載的模塊沒有靜態(tài)連接關(guān)系,這點(diǎn)也與import語句不相同
22.10.2 適用場(chǎng)合
-
按需加載
button.addEventListener('click',event=>{import('./dialogBox.js').then(dialogBox => {dialogBox.open()}).catch(error => {//Error}) })
-
條件加載
if(condition){import('moduleA').then(...) }else{import('moduleB').then(...) }
-
動(dòng)態(tài)加載模塊路徑
import(f()).then(...)
22.10.3 注意點(diǎn)
-
import()加載模塊成功以后,這個(gè)模塊會(huì)作為一個(gè)對(duì)象當(dāng)作then方法的參數(shù),因此可以使用對(duì)象結(jié)構(gòu)賦值的語法,獲取輸出接口
-
如果模塊有default輸出接口,可以用參數(shù)直接獲得
import('./myModule.js').then(myModule => {console.log(myModule.default) })//也可以使用具名形式輸入 import('./myModule.js').then(({default:thenDefault}) => {console.log(thenDefault) })
-
如果想同時(shí)加載多個(gè)模塊,可以采用Promise.all寫法
Promise.all([import('./amo.js'),import('./bmo.js'),import('./cmo.js') ]) .then(([amo,bmo,cmo])=>{})
-
import也可以用在async函數(shù)中