一坞生、理解 JavaScript 的作用域、作用域鏈和內(nèi)部原理
作用域
javascript 擁有一套設(shè)計(jì)良好的規(guī)則來(lái)存儲(chǔ)變量,并且之后可以方便地找到這些變量式矫,這套規(guī)則被稱為作用域狸驳。
作用域就是代碼的執(zhí)行環(huán)境预明,全局執(zhí)行環(huán)境就是全局作用域,函數(shù)的執(zhí)行環(huán)境就是私有作用域耙箍,它們都是棧內(nèi)存撰糠。
作用域鏈
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行時(shí),會(huì)創(chuàng)建變量對(duì)象的一個(gè)作用域鏈(作用域形成的鏈條),由于變量的查找是沿著作用域鏈來(lái)實(shí)現(xiàn)的辩昆,所以也稱作用域鏈為變量查找的機(jī)制阅酪。
- 作用域鏈的前端,始終都是當(dāng)前執(zhí)行的代碼所在環(huán)境的變量對(duì)象
- 作用域鏈中的下一個(gè)對(duì)象來(lái)自于外部環(huán)境汁针,而在下一個(gè)變量對(duì)象則來(lái)自下一個(gè)外部環(huán)境术辐,一直到全局執(zhí)行環(huán)境
- 全局執(zhí)行環(huán)境的變量對(duì)象始終都是作用域鏈上的最后一個(gè)對(duì)象
內(nèi)部環(huán)境可以通過(guò)作用域鏈訪問(wèn)所有外部環(huán)境,但外部環(huán)境不能訪問(wèn)內(nèi)部環(huán)境的任何變量和函數(shù)施无。
內(nèi)部原理
-
編譯
以 var a = 2;為例辉词,說(shuō)明 javascript 的內(nèi)部編譯過(guò)程,主要包括以下三步:
-
分詞(tokenizing)
把由字符組成的字符串分解成有意義的代碼塊猾骡,這些代碼塊被稱為詞法單元(token)
var a = 2;被分解成為下面這些詞法單元:var瑞躺、a、=兴想、2幢哨、;。這些詞法單元組成了一個(gè)詞法單元流數(shù)組
[ "var": "keyword", "a": "identifier", "=": "assignment", "2": "integer", ";": "eos" (end of statement) ]
-
解析(parsing)
把詞法單元流數(shù)組轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表程序語(yǔ)法結(jié)構(gòu)的樹(shù)襟企,這個(gè)樹(shù)被稱為“抽象語(yǔ)法樹(shù)” (Abstract Syntax Tree, AST)
var a = 2;的抽象語(yǔ)法樹(shù)中有一個(gè)叫 VariableDeclaration 的頂級(jí)節(jié)點(diǎn)嘱么,接下來(lái)是一個(gè)叫 Identifier(它的值是 a)的子節(jié)點(diǎn),以及一個(gè)叫 AssignmentExpression 的子節(jié)點(diǎn),且該節(jié)點(diǎn)有一個(gè)叫 Numericliteral(它的值是 2)的子節(jié)點(diǎn)
{ operation: "=", left: { keyword: "var", right: "a" } right: "2" }
-
代碼生成
將 AST 轉(zhuǎn)換為可執(zhí)行代碼的過(guò)程被稱為代碼生成
var a=2;的抽象語(yǔ)法樹(shù)轉(zhuǎn)為一組機(jī)器指令曼振,用來(lái)創(chuàng)建一個(gè)叫作 a 的變量(包括分配內(nèi)存等)几迄,并將值 2 儲(chǔ)存在 a 中
實(shí)際上,javascript 引擎的編譯過(guò)程要復(fù)雜得多冰评,包括大量?jī)?yōu)化操作映胁,上面的三個(gè)步驟是編譯過(guò)程的基本概述
任何代碼片段在執(zhí)行前都要進(jìn)行編譯,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒甲雅。javascript 編譯器首先會(huì)對(duì) var a=2;這段程序進(jìn)行編譯解孙,然后做好執(zhí)行它的準(zhǔn)備,并且通常馬上就會(huì)執(zhí)行它
-
-
執(zhí)行
簡(jiǎn)而言之抛人,編譯過(guò)程就是編譯器把程序分解成詞法單元(token)弛姜,然后把詞法單元解析成語(yǔ)法樹(shù)(AST),再把語(yǔ)法樹(shù)變成機(jī)器指令等待執(zhí)行的過(guò)程
實(shí)際上妖枚,代碼進(jìn)行編譯廷臼,還要執(zhí)行。下面仍然以 var a = 2;為例绝页,深入說(shuō)明編譯和執(zhí)行過(guò)程
-
編譯
編譯器查找作用域是否已經(jīng)有一個(gè)名稱為 a 的變量存在于同一個(gè)作用域的集合中荠商。如果是,編譯器會(huì)忽略該聲明续誉,繼續(xù)進(jìn)行編譯莱没;否則它會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為 a
編譯器將 var a = 2;這個(gè)代碼片段編譯成用于執(zhí)行的機(jī)器指令
依據(jù)編譯器的編譯原理酷鸦,javascript 中的重復(fù)聲明是合法的
// test在作用域中首次出現(xiàn)饰躲,所以聲明新變量,并將20賦值給test var test = 20 // test在作用域中已經(jīng)存在臼隔,直接使用属铁,將20的賦值替換成30 var test = 30
-
執(zhí)行
引擎運(yùn)行時(shí)會(huì)首先查詢作用域,在當(dāng)前的作用域集合中是否存在一個(gè)叫作 a 的變量躬翁。如果是,引擎就會(huì)使用這個(gè)變量盯拱;如果否盒发,引擎會(huì)繼續(xù)查找該變量
如果引擎最終找到了變量 a,就會(huì)將 2 賦值給它狡逢。否則引擎會(huì)拋出一個(gè)異常
-
-
查詢
在引擎執(zhí)行的第一步操作中宁舰,對(duì)變量 a 進(jìn)行了查詢,這種查詢叫做 LHS 查詢奢浑。實(shí)際上蛮艰,引擎查詢共分為兩種:LHS 查詢和 RHS 查詢
從字面意思去理解,當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行 LHS 查詢雀彼,出現(xiàn)在右側(cè)時(shí)進(jìn)行 RHS 查詢
更準(zhǔn)確地講壤蚜,RHS 查詢與簡(jiǎn)單地查找某個(gè)變量的值沒(méi)什么區(qū)別即寡,而 LHS 查詢則是試圖找到變量的容器本身,從而可以對(duì)其賦值
function foo(a) { console.log(a) // 2 } foo(2)
這段代碼中袜刷,總共包括 4 個(gè)查詢聪富,分別是:
1、foo(...)對(duì) foo 進(jìn)行了 RHS 引用
2著蟹、函數(shù)傳參 a = 2 對(duì) a 進(jìn)行了 LHS 引用
3墩蔓、console.log(...)對(duì) console 對(duì)象進(jìn)行了 RHS 引用,并檢查其是否有一個(gè) log 的方法
4萧豆、console.log(a)對(duì) a 進(jìn)行了 RHS 引用奸披,并把得到的值傳給了 console.log(...)
-
嵌套
在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí),引擎就會(huì)在外層嵌套的作用域中繼續(xù)查找涮雷,直到找到該變量阵面,或抵達(dá)最外層的作用域(也就是全局作用域)為止
function foo(a) { console.log(a + b) } var b = 2 foo(2) // 4
行 RHS 引用,沒(méi)有找到份殿;接著膜钓,引擎在全局作用域中查找 b,成功找到后卿嘲,對(duì)其進(jìn)行 RHS 引用颂斜,將 2 賦值給 b
-
異常
為什么區(qū)分 LHS 和 RHS 是一件重要的事情?因?yàn)樵谧兞窟€沒(méi)有聲明(在任何作用域中都無(wú)法找到變量)的情況下拾枣,這兩種查詢的行為不一樣
-
RHS
- 如果 RHS 查詢失敗沃疮,引擎會(huì)拋出 ReferenceError(引用錯(cuò)誤)異常
// 對(duì)b進(jìn)行RHS查詢時(shí),無(wú)法找到該變量梅肤。也就是說(shuō)司蔬,這是一個(gè)“未聲明”的變量 function foo(a) { a = b } foo() // ReferenceError: b is not defined
- 如果 RHS 查詢找到了一個(gè)變量,但嘗試對(duì)變量的值進(jìn)行不合理操作姨蝴,比如對(duì)一個(gè)非函數(shù)類型值進(jìn)行函數(shù)調(diào)用俊啼,或者引用 null 或 undefined 中的屬性,引擎會(huì)拋出另外一種類型異常:TypeError(類型錯(cuò)誤)異常
function foo() { var b = 0 b() } foo() // TypeError: b is not a function
-
LHS
- 當(dāng)引擎執(zhí)行 LHS 查詢時(shí)左医,如果無(wú)法找到變量授帕,全局作用域會(huì)創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎
function foo() { a = 1 } foo() console.log(a) // 1
- 如果在嚴(yán)格模式中 LHS 查詢失敗時(shí)浮梢,并不會(huì)創(chuàng)建并返回一個(gè)全局變量跛十,引擎會(huì)拋出同 RHS 查詢失敗時(shí)類似的 ReferenceError 異常
function foo() { 'use strict' a = 1 } foo() console.log(a) // ReferenceError: a is not defined
-
-
原理
function foo(a) { console.log(a) } foo(2)
以上面這個(gè)代碼片段來(lái)說(shuō)明作用域的內(nèi)部原理,分為以下幾步:
【1】引擎需要為 foo(...)函數(shù)進(jìn)行 RHS 引用秕硝,在全局作用域中查找 foo芥映。成功找到并執(zhí)行
【2】引擎需要進(jìn)行 foo 函數(shù)的傳參 a=2,為 a 進(jìn)行 LHS 引用,在 foo 函數(shù)作用域中查找 a奈偏。成功找到坞嘀,并把 2 賦值給 a
【3】引擎需要執(zhí)行 console.log(...),為 console 對(duì)象進(jìn)行 RHS 引用霎苗,在 foo 函數(shù)作用域中查找 console 對(duì)象姆吭。由于 console 是個(gè)內(nèi)置對(duì)象,被成功找到
【4】引擎在 console 對(duì)象中查找 log(...)方法唁盏,成功找到
【5】引擎需要執(zhí)行 console.log(a)内狸,對(duì) a 進(jìn)行 RHS 引用,在 foo 函數(shù)作用域中查找 a厘擂,成功找到并執(zhí)行
【6】于是昆淡,引擎把 a 的值,也就是 2 傳到 console.log(...)中
【7】最終刽严,控制臺(tái)輸出 2
二昂灵、理解詞法作用域和動(dòng)態(tài)作用域
詞法作用域
編譯器的第一個(gè)工作階段叫作分詞,就是把由字符組成的字符串分解成詞法單元舞萄。這個(gè)概念是理解詞法作用域的基礎(chǔ)
簡(jiǎn)單地說(shuō)眨补,詞法作用域就是定義在詞法階段的作用域,是由寫(xiě)代碼時(shí)將變量和塊作用域?qū)懺谀睦飦?lái)決定的倒脓,因此當(dāng)詞法分析器處理代碼時(shí)會(huì)保持作用域不變
- 關(guān)系
無(wú)論函數(shù)在哪里被調(diào)用撑螺,也無(wú)論它如何被調(diào)用,它的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定
function foo(a) {
var b = a * 2
function bar(c) {
console.log(a, b, c)
}
bar(b * 3)
}
foo(2) // 2 4 12
在這個(gè)例子中有三個(gè)逐級(jí)嵌套的作用域崎弃。為了幫助理解甘晤,可以將它們想象成幾個(gè)逐級(jí)包含的氣泡
作用域氣泡由其對(duì)應(yīng)的作用域塊代碼寫(xiě)在哪里決定,它們是逐級(jí)包含的
氣泡 1 包含著整個(gè)全局作用域饲做,其中只有一個(gè)標(biāo)識(shí)符:foo
氣泡 2 包含著 foo 所創(chuàng)建的作用域线婚,其中有三個(gè)標(biāo)識(shí)符:a、bar 和 b
氣泡 3 包含著 bar 所創(chuàng)建的作用域盆均,其中只有一個(gè)標(biāo)識(shí)符:c
- 查找
作用域氣泡的結(jié)構(gòu)和互相之間的位置關(guān)系給引擎提供了足夠的位置信息塞弊,引擎用這些信息來(lái)查找標(biāo)識(shí)符的位置
在代碼片段中,引擎執(zhí)行 console.log(...)聲明泪姨,并查找 a居砖、b 和 c 三個(gè)變量的引用。它首先從最內(nèi)部的作用域驴娃,也就是 bar(...)函數(shù)的作用域開(kāi)始查找。引擎無(wú)法在這里找到 a循集,因此會(huì)去上一級(jí)到所嵌套的 foo(...)的作用域中繼續(xù)查找唇敞。在這里找到了 a,因此引擎使用了這個(gè)引用。對(duì) b 來(lái)講也一樣疆柔。而對(duì) c 來(lái)說(shuō)咒精,引擎在 bar(...)中找到了它
[注意]詞法作用域查找只會(huì)查找一級(jí)標(biāo)識(shí)符,如果代碼引用了 foo.bar.baz旷档,詞法作用域查找只會(huì)試圖查找 foo 標(biāo)識(shí)符模叙,找到這個(gè)變量后,對(duì)象屬性訪問(wèn)規(guī)則分別接管對(duì) bar 和 baz 屬性的訪問(wèn)
foo = {
bar: {
baz: 1
}
}
console.log(foo.bar.baz) // 1
- 遮蔽
作用域查找從運(yùn)行時(shí)所處的最內(nèi)部作用域開(kāi)始鞋屈,逐級(jí)向外或者說(shuō)向上進(jìn)行范咨,直到遇見(jiàn)第一個(gè)匹配的標(biāo)識(shí)符為止
在多層的嵌套作用域中可以定義同名的標(biāo)識(shí)符,這叫作“遮蔽效應(yīng)”厂庇,內(nèi)部的標(biāo)識(shí)符“遮蔽”了外部的標(biāo)識(shí)符
var a = 0
function test() {
var a = 1
console.log(a) // 1
}
test()
全局變量會(huì)自動(dòng)為全局對(duì)象的屬性渠啊,因此可以不直接通過(guò)全局對(duì)象的詞法名稱,而是間接地通過(guò)對(duì)全局對(duì)象屬性的引用來(lái)對(duì)其進(jìn)行訪問(wèn)
var a = 0
function test() {
var a = 1
console.log(window.a) //0
}
test()
通過(guò)這種技術(shù)可以訪問(wèn)那些被同名變量所遮蔽的全局變量权旷。但非全局的變量如果被遮蔽了替蛉,無(wú)論如何都無(wú)法被訪問(wèn)到
動(dòng)態(tài)作用域
javascript 使用的是詞法作用域,它最重要的特征是它的定義過(guò)程發(fā)生在代碼的書(shū)寫(xiě)階段
那為什么要介紹動(dòng)態(tài)作用域呢拄氯?實(shí)際上動(dòng)態(tài)作用域是 javascript 另一個(gè)重要機(jī)制 this 的表親躲查。作用域混亂多數(shù)是因?yàn)樵~法作用域和 this 機(jī)制相混淆,傻傻分不清楚
動(dòng)態(tài)作用域并不關(guān)心函數(shù)和作用域是如何聲明以及在任何處聲明的译柏,只關(guān)心它們從何處調(diào)用镣煮。換句話說(shuō),作用域鏈?zhǔn)腔谡{(diào)用棧的艇纺,而不是代碼中的作用域嵌套
var a = 2
function foo() {
console.log(a)
}
function bar() {
var a = 3
foo()
}
bar()
【1】如果處于詞法作用域怎静,也就是現(xiàn)在的 javascript 環(huán)境。變量 a 首先在 foo()函數(shù)中查找黔衡,沒(méi)有找到蚓聘。于是順著作用域鏈到全局作用域中查找,找到并賦值為 2盟劫。所以控制臺(tái)輸出 2
【2】如果處于動(dòng)態(tài)作用域夜牡,同樣地,變量 a 首先在 foo()中查找侣签,沒(méi)有找到塘装。這里會(huì)順著調(diào)用棧在調(diào)用 foo()函數(shù)的地方,也就是 bar()函數(shù)中查找影所,找到并賦值為 3蹦肴。所以控制臺(tái)輸出 3
兩種作用域的區(qū)別,簡(jiǎn)而言之猴娩,詞法作用域是在定義時(shí)確定的阴幌,而動(dòng)態(tài)作用域是在運(yùn)行時(shí)確定的
三勺阐、理解 JavaScript 的執(zhí)行上下文棧,可以應(yīng)用堆棧信息快速定位問(wèn)題
執(zhí)行上下文
- 全局執(zhí)行上下文: 這是默認(rèn)的矛双、最基礎(chǔ)的執(zhí)行上下文渊抽。不在任何函數(shù)中的代碼都位于全局執(zhí)行上下文中。它做了兩件事:1. 創(chuàng)建一個(gè)全局對(duì)象议忽,在瀏覽器中這個(gè)全局對(duì)象就是 window 對(duì)象懒闷。2. 將 this 指針指向這個(gè)全局對(duì)象。一個(gè)程序中只能存在一個(gè)全局執(zhí)行上下文栈幸。
- 函數(shù)執(zhí)行上下文: 每次調(diào)用函數(shù)時(shí)愤估,都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文。每個(gè)函數(shù)都擁有自己的執(zhí)行上下文侦镇,但是只有在函數(shù)被調(diào)用的時(shí)候才會(huì)被創(chuàng)建灵疮。一個(gè)程序中可以存在任意數(shù)量的函數(shù)執(zhí)行上下文。每當(dāng)一個(gè)新的執(zhí)行上下文被創(chuàng)建壳繁,它都會(huì)按照特定的順序執(zhí)行一系列步驟震捣,具體過(guò)程將在本文后面討論。
- Eval 函數(shù)執(zhí)行上下文: 運(yùn)行在 eval 函數(shù)中的代碼也獲得了自己的執(zhí)行上下文闹炉,但由于 Javascript 開(kāi)發(fā)人員不常用 eval 函數(shù)蒿赢,所以在這里不再討論。
執(zhí)行棧
執(zhí)行棧渣触,在其他編程語(yǔ)言中也被叫做調(diào)用棧羡棵,具有 LIFO(后進(jìn)先出)結(jié)構(gòu),用于存儲(chǔ)在代碼執(zhí)行期間創(chuàng)建的所有執(zhí)行上下文嗅钻。
當(dāng) JavaScript 引擎首次讀取你的腳本時(shí)皂冰,它會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并將其推入當(dāng)前的執(zhí)行棧。每當(dāng)發(fā)生一個(gè)函數(shù)調(diào)用养篓,引擎都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并將其推到當(dāng)前執(zhí)行棧的頂端秃流。
引擎會(huì)運(yùn)行執(zhí)行上下文在執(zhí)行棧頂端的函數(shù),當(dāng)此函數(shù)運(yùn)行完成后柳弄,其對(duì)應(yīng)的執(zhí)行上下文將會(huì)從執(zhí)行棧中彈出舶胀,上下文控制權(quán)將移到當(dāng)前執(zhí)行棧的下一個(gè)執(zhí)行上下文。
讓我們通過(guò)下面的代碼示例來(lái)理解這一點(diǎn):
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
當(dāng)上述代碼在瀏覽器中加載時(shí)碧注,JavaScript 引擎會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并且將它推入當(dāng)前的執(zhí)行棧嚣伐。當(dāng)調(diào)用 first()
函數(shù)時(shí),JavaScript 引擎為該函數(shù)創(chuàng)建了一個(gè)新的執(zhí)行上下文并將其推到當(dāng)前執(zhí)行棧的頂端萍丐。
當(dāng)在 first()
函數(shù)中調(diào)用 second()
函數(shù)時(shí)轩端,Javascript 引擎為該函數(shù)創(chuàng)建了一個(gè)新的執(zhí)行上下文并將其推到當(dāng)前執(zhí)行棧的頂端。當(dāng) second()
函數(shù)執(zhí)行完成后逝变,它的執(zhí)行上下文從當(dāng)前執(zhí)行棧中彈出基茵,上下文控制權(quán)將移到當(dāng)前執(zhí)行棧的下一個(gè)執(zhí)行上下文刻撒,即 first()
函數(shù)的執(zhí)行上下文。
當(dāng) first()
函數(shù)執(zhí)行完成后耿导,它的執(zhí)行上下文從當(dāng)前執(zhí)行棧中彈出,上下文控制權(quán)將移到全局執(zhí)行上下文态贤。一旦所有代碼執(zhí)行完畢舱呻,Javascript 引擎把全局執(zhí)行上下文從執(zhí)行棧中移除。
執(zhí)行上下文是如何被創(chuàng)建的
到目前為止悠汽,我們已經(jīng)看到了 JavaScript 引擎如何管理執(zhí)行上下文箱吕,現(xiàn)在就讓我們來(lái)理解 JavaScript 引擎是如何創(chuàng)建執(zhí)行上下文的。
執(zhí)行上下文分兩個(gè)階段創(chuàng)建: 1)創(chuàng)建階段柿冲; 2)執(zhí)行階段
創(chuàng)建階段
在任意的 JavaScript 代碼被執(zhí)行前茬高,執(zhí)行上下文處于創(chuàng)建階段。在創(chuàng)建階段中總共發(fā)生了三件事情:
- 確定 this 的值假抄,也被稱為 This Binding 怎栽。
- LexicalEnvironment(詞法環(huán)境) 組件被創(chuàng)建。
- VariableEnvironment(變量環(huán)境) 組件被創(chuàng)建宿饱。
因此熏瞄,執(zhí)行上下文可以在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
This Binding:
在全局執(zhí)行上下文中, this
的值指向全局對(duì)象谬以,在瀏覽器中强饮, this
的值指向 window 對(duì)象。
在函數(shù)執(zhí)行上下文中为黎, this
的值取決于函數(shù)的調(diào)用方式邮丰。如果它被一個(gè)對(duì)象引用調(diào)用,那么 this
的值被設(shè)置為該對(duì)象铭乾,否則 this
的值被設(shè)置為全局對(duì)象或 undefined
(嚴(yán)格模式下)剪廉。例如:
let person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// 'this' 指向 'person', 因?yàn)?'calcAge' 是被 'person' 對(duì)象引用調(diào)用的。
let calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局 window 對(duì)象,因?yàn)闆](méi)有給出任何對(duì)象引用
詞法環(huán)境(Lexical Environment)
官方 ES6 文檔將詞法環(huán)境定義為:
詞法環(huán)境是一種規(guī)范類型片橡,基于 ECMAScript 代碼的詞法嵌套結(jié)構(gòu)來(lái)定義標(biāo)識(shí)符與特定變量和函數(shù)的關(guān)聯(lián)關(guān)系妈经。詞法環(huán)境由環(huán)境記錄(environment record)和可能為空引用(null)的外部詞法環(huán)境組成。
簡(jiǎn)而言之捧书,詞法環(huán)境是一個(gè)包含 標(biāo)識(shí)符變量映射 的結(jié)構(gòu)吹泡。(這里的 標(biāo)識(shí)符 表示變量/函數(shù)的名稱, 變量 是對(duì)實(shí)際對(duì)象【包括函數(shù)類型對(duì)象】或原始值的引用)
在詞法環(huán)境中经瓷,有兩個(gè)組成部分:(1) 環(huán)境記錄(environment record) (2) 對(duì)外部環(huán)境的引用
- 環(huán)境記錄 是存儲(chǔ)變量和函數(shù)聲明的實(shí)際位置爆哑。
- 對(duì)外部環(huán)境的引用 意味著它可以訪問(wèn)其外部詞法環(huán)境。
詞法環(huán)境有兩種類型:
- 全局環(huán)境(在全局執(zhí)行上下文中)是一個(gè)沒(méi)有外部環(huán)境的詞法環(huán)境舆吮。全局環(huán)境的外部環(huán)境引用為 null 揭朝。它擁有一個(gè)全局對(duì)象(window 對(duì)象)及其關(guān)聯(lián)的方法和屬性(例如數(shù)組方法)以及任何用戶自定義的全局變量队贱,
this
的值指向這個(gè)全局對(duì)象。 - 函數(shù)環(huán)境潭袱,用戶在函數(shù)中定義的變量被存儲(chǔ)在 環(huán)境記錄 中柱嫌。對(duì)外部環(huán)境的引用可以是全局環(huán)境,也可以是包含內(nèi)部函數(shù)的外部函數(shù)環(huán)境屯换。
注意:對(duì)于 函數(shù)環(huán)境 而言编丘, 環(huán)境記錄 還包含了一個(gè) arguments
對(duì)象,該對(duì)象包含了索引和傳遞給函數(shù)的參數(shù)之間的映射以及傳遞給函數(shù)的參數(shù)的 長(zhǎng)度(數(shù)量) 彤悔。例如嘉抓,下面函數(shù)的 arguments
對(duì)象如下所示:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// arguments 對(duì)象
Arguments: {0: 2, 1: 3, length: 2},
環(huán)境記錄同樣有兩種類型(如下所示):
- 聲明性環(huán)境記錄 存儲(chǔ)變量、函數(shù)和參數(shù)晕窑。一個(gè)函數(shù)環(huán)境包含聲明性環(huán)境記錄抑片。
- 對(duì)象環(huán)境記錄 用于定義在全局執(zhí)行上下文中出現(xiàn)的變量和函數(shù)的關(guān)聯(lián)。全局環(huán)境包含對(duì)象環(huán)境記錄杨赤。
抽象地說(shuō)敞斋,詞法環(huán)境在偽代碼中看起來(lái)像這樣:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標(biāo)識(shí)符綁定在這里
outer: <null>
}
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標(biāo)識(shí)符綁定在這里
outer: <Global or outer function environment reference>
}
}
}
變量環(huán)境:
它也是一個(gè)詞法環(huán)境,其 EnvironmentRecord
包含了由 VariableStatements 在此執(zhí)行上下文創(chuàng)建的綁定望拖。
如上所述渺尘,變量環(huán)境也是一個(gè)詞法環(huán)境,因此它具有上面定義的詞法環(huán)境的所有屬性说敏。
在 ES6 中鸥跟, LexicalEnvironment 組件和 VariableEnvironment 組件的區(qū)別在于前者用于存儲(chǔ)函數(shù)聲明和變量( let
和 const
)綁定,而后者僅用于存儲(chǔ)變量( var
)綁定盔沫。
讓我們結(jié)合一些代碼示例來(lái)理解上述概念:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e *f *g;
}
c = multiply(20, 30);
執(zhí)行上下文如下所示:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標(biāo)識(shí)符綁定在這里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標(biāo)識(shí)符綁定在這里
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標(biāo)識(shí)符綁定在這里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標(biāo)識(shí)符綁定在這里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意:只有在遇到函數(shù) multiply
的調(diào)用時(shí)才會(huì)創(chuàng)建函數(shù)執(zhí)行上下文医咨。
你可能已經(jīng)注意到了 let
和 const
定義的變量沒(méi)有任何與之關(guān)聯(lián)的值,但 var
定義的變量設(shè)置為 undefined
架诞。
這是因?yàn)樵趧?chuàng)建階段拟淮,代碼會(huì)被掃描并解析變量和函數(shù)聲明,其中函數(shù)聲明存儲(chǔ)在環(huán)境中谴忧,而變量會(huì)被設(shè)置為 undefined
(在 var
的情況下)或保持未初始化(在 let
和 const
的情況下)很泊。
這就是為什么你可以在聲明之前訪問(wèn) var
定義的變量(盡管是 undefined
),但如果在聲明之前訪問(wèn) let
和 const
定義的變量就會(huì)提示引用錯(cuò)誤的原因沾谓。
這就是我們所謂的變量提升委造。
執(zhí)行階段
這是整篇文章中最簡(jiǎn)單的部分。在此階段均驶,完成對(duì)所有變量的分配昏兆,最后執(zhí)行代碼。
注:在執(zhí)行階段妇穴,如果 Javascript 引擎在源代碼中聲明的實(shí)際位置找不到 let
變量的值爬虱,那么將為其分配 undefined
值隶债。
錯(cuò)誤堆棧的裁剪
Node.js 才支持這個(gè)特性,通過(guò) Error.captureStackTrace 來(lái)實(shí)現(xiàn)跑筝,Error.captureStackTrace 接收一個(gè) object 作為第 1 個(gè)參數(shù)死讹,以及可選的 function 作為第 2 個(gè)參數(shù)。其作用是捕獲當(dāng)前的調(diào)用棧并對(duì)其進(jìn)行裁剪曲梗,捕獲到的調(diào)用棧會(huì)記錄在第 1 個(gè)參數(shù)的 stack 屬性上回俐,裁剪的參照點(diǎn)是第 2 個(gè)參數(shù),也就是說(shuō)稀并,此函數(shù)之前的調(diào)用會(huì)被記錄到調(diào)用棧上面,而之后的不會(huì)单默。
讓我們用代碼來(lái)說(shuō)明碘举,首先,把當(dāng)前的調(diào)用棧捕獲并放到 myObj 上:
const myObj = {};
function c() {}
function b() {
// 把當(dāng)前調(diào)用棧寫(xiě)到 myObj 上
Error.captureStackTrace(myObj);
c();
}
function a() {
b();
}
// 調(diào)用函數(shù) a
a();
// 打印 myObj.stack
console.log(myObj.stack);
// 輸出會(huì)是這樣
// at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack
// at a (repl:2:1)
// at repl:1:1 <-- Node internals below this line
// at realRunInThisContextScript (vm.js:22:35)
// at sigintHandlersWrap (vm.js:98:12)
// at ContextifyScript.Script.runInThisContext (vm.js:24:12)
// at REPLServer.defaultEval (repl.js:313:29)
// at bound (domain.js:280:14)
// at REPLServer.runBound [as eval] (domain.js:293:12)
// at REPLServer.onLine (repl.js:513:10)
上面的調(diào)用棧中只有 a -> b搁廓,因?yàn)槲覀冊(cè)?b 調(diào)用 c 之前就捕獲了調(diào)用椧保。現(xiàn)在對(duì)上面的代碼稍作修改境蜕,然后看看會(huì)發(fā)生什么:
const myObj = {};
function d() {
// 我們把當(dāng)前調(diào)用棧存儲(chǔ)到 myObj 上蝙场,但是會(huì)去掉 b 和 b 之后的部分
Error.captureStackTrace(myObj, b);
}
function c() {
d();
}
function b() {
c();
}
function a() {
b();
}
// 執(zhí)行代碼
a();
// 打印 myObj.stack
console.log(myObj.stack);
// 輸出如下
// at a (repl:2:1) <-- As you can see here we only get frames before b was called
// at repl:1:1 <-- Node internals below this line
// at realRunInThisContextScript (vm.js:22:35)
// at sigintHandlersWrap (vm.js:98:12)
// at ContextifyScript.Script.runInThisContext (vm.js:24:12)
// at REPLServer.defaultEval (repl.js:313:29)
// at bound (domain.js:280:14)
// at REPLServer.runBound [as eval] (domain.js:293:12)
// at REPLServer.onLine (repl.js:513:10)
// at emitOne (events.js:101:20)
在這段代碼里面,因?yàn)槲覀冊(cè)谡{(diào)用 Error.captureStackTrace 的時(shí)候傳入了 b粱年,這樣 b 之后的調(diào)用棧都會(huì)被隱藏售滤。
現(xiàn)在你可能會(huì)問(wèn),知道這些到底有啥用台诗?如果你想對(duì)用戶隱藏跟他業(yè)務(wù)無(wú)關(guān)的錯(cuò)誤堆棧(比如某個(gè)庫(kù)的內(nèi)部實(shí)現(xiàn))就可以試用這個(gè)技巧完箩。
錯(cuò)誤調(diào)試
1.Error對(duì)象和錯(cuò)誤處理
當(dāng)程序運(yùn)行出現(xiàn)錯(cuò)誤時(shí), 通常會(huì)拋出一個(gè) Error 對(duì)象. Error 對(duì)象可以作為用戶自定義錯(cuò)誤對(duì)象繼承的原型.
Error.prototype 對(duì)象包含如下屬性:
constructor–指向?qū)嵗臉?gòu)造函數(shù)
message–錯(cuò)誤信息
name–錯(cuò)誤的名字(類型)
上述是 Error.prototype 的標(biāo)準(zhǔn)屬性, 此外, 不同的運(yùn)行環(huán)境都有其特定的屬性. 在例如 Node, Firefox, Chrome, Edge, IE 10+, Opera 以及 Safari 6+
這樣的環(huán)境中, Error 對(duì)象具備 stack 屬性, 該屬性包含了錯(cuò)誤的堆棧軌跡. 一個(gè)錯(cuò)誤實(shí)例的堆棧軌跡包含了自構(gòu)造函數(shù)之后的所有堆棧結(jié)構(gòu).
2.如何查看調(diào)用棧
只查看調(diào)用棧:console.trace
a()
function a() {
b()
}
function b() {
c()
}
function c() {
let aa = 1
}
console.trace()
3.debugger打斷點(diǎn)形式
四、this 的原理以及幾種不同使用場(chǎng)景的取值
作為對(duì)象方法調(diào)用
在 JavaScript 中拉队,函數(shù)也是對(duì)象弊知,因此函數(shù)可以作為一個(gè)對(duì)象的屬性,此時(shí)該函數(shù)被稱為該對(duì)象的方法粱快,在使用這種調(diào)用方式時(shí)秩彤,this 被自然綁定到該對(duì)象
var test = {
a:0,
b:0,
get:function(){
return this.a;
}
}
作為函數(shù)調(diào)用
函數(shù)也可以直接被調(diào)用,此時(shí) this 綁定到全局對(duì)象事哭。在瀏覽器中漫雷,window 就是該全局對(duì)象。比如下面的例子:函數(shù)被調(diào)用時(shí)慷蠕,this 被綁定到全局對(duì)象珊拼,
接下來(lái)執(zhí)行賦值語(yǔ)句,相當(dāng)于隱式的聲明了一個(gè)全局變量流炕,這顯然不是調(diào)用者希望的澎现。
function makeNoSense(x) {
this.x = x;
}
作為構(gòu)造函數(shù)調(diào)用
javaScript 支持面向?qū)ο笫骄幊探霭c主流的面向?qū)ο笫骄幊陶Z(yǔ)言不同,JavaScript 并沒(méi)有類(class)的概念剑辫,而是使用基于原型(prototype)的繼承方式干旧。
相應(yīng)的,JavaScript 中的構(gòu)造函數(shù)也很特殊妹蔽,如果不使用 new 調(diào)用椎眯,則和普通函數(shù)一樣。作為又一項(xiàng)約定俗成的準(zhǔn)則胳岂,構(gòu)造函數(shù)以大寫(xiě)字母開(kāi)頭编整,
提醒調(diào)用者使用正確的方式調(diào)用。如果調(diào)用正確乳丰,this 綁定到新創(chuàng)建的對(duì)象上掌测。
function Point(x, y){
this.x = x;
this.y = y;
}
在call或者apply,bind中調(diào)用
讓我們?cè)僖淮沃厣瓴埃?JavaScript 中函數(shù)也是對(duì)象汞斧,對(duì)象則有方法,apply 和 call 就是函數(shù)對(duì)象的方法什燕。
這兩個(gè)方法異常強(qiáng)大粘勒,他們?cè)试S切換函數(shù)執(zhí)行的上下文環(huán)境(context),即 this 綁定的對(duì)象屎即。
很多 JavaScript 中的技巧以及類庫(kù)都用到了該方法庙睡。讓我們看一個(gè)具體的例子:
function Point(x, y){
this.x = x;
this.y = y;
this.moveTo = function(x, y){
this.x = x;
this.y = y;
}
}
var p1 = new Point(0, 0);
var p2 = {x: 0, y: 0};
p1.moveTo(1, 1);
p1.moveTo.apply(p2, [10, 10])
五、閉包的實(shí)現(xiàn)原理和作用技俐,可以列舉幾個(gè)開(kāi)發(fā)中閉包的實(shí)際應(yīng)用
閉包的概念
- 指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)埃撵,一般情況就是在一個(gè)函數(shù)中包含另一個(gè)函數(shù)。
閉包的作用
- 訪問(wèn)函數(shù)內(nèi)部變量虽另、保持函數(shù)在環(huán)境中一直存在暂刘,不會(huì)被垃圾回收機(jī)制處理
因?yàn)楹瘮?shù)內(nèi)部聲明 的變量是局部的,只能在函數(shù)內(nèi)部訪問(wèn)到捂刺,但是函數(shù)外部的變量是對(duì)函數(shù)內(nèi)部可見(jiàn)的谣拣,這就是作用域鏈的特點(diǎn)了。
子級(jí)可以向父級(jí)查找變量族展,逐級(jí)查找森缠,找到為止
因此我們可以在函數(shù)內(nèi)部再創(chuàng)建一個(gè)函數(shù),這樣對(duì)內(nèi)部的函數(shù)來(lái)說(shuō)仪缸,外層函數(shù)的變量都是可見(jiàn)的贵涵,然后我們就可以訪問(wèn)到他的變量了。
function bar(){
//外層函數(shù)聲明的變量
var value=1;
function foo(){
console.log(value);
}
return foo();
};
var bar2=bar;
//實(shí)際上bar()函數(shù)并沒(méi)有因?yàn)閳?zhí)行完就被垃圾回收機(jī)制處理掉
//這就是閉包的作用,調(diào)用bar()函數(shù)宾茂,就會(huì)執(zhí)行里面的foo函數(shù)瓷马,foo這時(shí)就會(huì)訪問(wèn)到外層的變量
bar2();
foo()包含bar()內(nèi)部作用域的閉包,使得該作用域能夠一直存活跨晴,不會(huì)被垃圾回收機(jī)制處理掉欧聘,這就是閉包的作用,以供foo()在任何時(shí)間進(jìn)行引用端盆。
閉包的優(yōu)點(diǎn)
- 方便調(diào)用上下文中聲明的局部變量
- 邏輯緊密怀骤,可以在一個(gè)函數(shù)中再創(chuàng)建個(gè)函數(shù),避免了傳參的問(wèn)題
閉包的缺點(diǎn)
- 因?yàn)槭褂瞄]包焕妙,可以使函數(shù)在執(zhí)行完后不被銷毀蒋伦,保留在內(nèi)存中,如果大量使用閉包就會(huì)造成內(nèi)存泄露焚鹊,內(nèi)存消耗很大
閉包在實(shí)際中的應(yīng)用
function addFn(a,b){
return(function(){
console.log(a+"+"+b);
})
}
var test =addFn(a,b);
setTimeout(test,3000);
一般setTimeout的第一個(gè)參數(shù)是個(gè)函數(shù)凉敲,但是不能傳值。如果想傳值進(jìn)去寺旺,可以調(diào)用一個(gè)函數(shù)返回一個(gè)內(nèi)部函數(shù)的調(diào)用,將內(nèi)部函數(shù)的調(diào)用傳給setTimeout势决。內(nèi)部函數(shù)執(zhí)行所需的參數(shù)阻塑,外部函數(shù)傳給他,在setTimeout函數(shù)中也可以訪問(wèn)到外部函數(shù)果复。
六陈莽、理解堆棧溢出和內(nèi)存泄漏的原理,如何防止
內(nèi)存泄露
- 申請(qǐng)的內(nèi)存執(zhí)行完后沒(méi)有及時(shí)的清理或者銷毀虽抄,占用空閑內(nèi)存走搁,內(nèi)存泄露過(guò)多的話,就會(huì)導(dǎo)致后面的程序申請(qǐng)不到內(nèi)存迈窟。因此內(nèi)存泄露會(huì)導(dǎo)致內(nèi)部?jī)?nèi)存溢出
堆棧溢出
- 內(nèi)存空間已經(jīng)被申請(qǐng)完私植,沒(méi)有足夠的內(nèi)存提供了
標(biāo)記清除法
在一些編程軟件中,比如c語(yǔ)言中车酣,需要使用malloc來(lái)申請(qǐng)內(nèi)存空間曲稼,再使用free釋放掉,需要手動(dòng)清除湖员。而js中是有自己的垃圾回收機(jī)制的贫悄,一般常用的垃圾收集方法就是標(biāo)記清除。
標(biāo)記清除法:在一個(gè)變量進(jìn)入執(zhí)行環(huán)境后就給它添加一個(gè)標(biāo)記:進(jìn)入環(huán)境娘摔,進(jìn)入環(huán)境的變量不會(huì)被釋放窄坦,因?yàn)橹灰獔?zhí)行流進(jìn)入響應(yīng)的環(huán)境,就可能用到他們。當(dāng)變量離開(kāi)環(huán)境后鸭津,則將其標(biāo)記為“離開(kāi)環(huán)境”彤侍。
常見(jiàn)的內(nèi)存泄露的原因
- 全局變量引起的內(nèi)存泄露
- 閉包
- 沒(méi)有被清除的計(jì)時(shí)器
解決方法
- 減少不必要的全局變量
- 減少閉包的使用(因?yàn)殚]包會(huì)導(dǎo)致內(nèi)存泄露)
- 避免死循環(huán)的發(fā)生
七、如何處理循環(huán)的異步操作
使用自執(zhí)行函數(shù)
1曙博、當(dāng)自執(zhí)行函數(shù)在循環(huán)當(dāng)中使用時(shí)拥刻,自執(zhí)行函數(shù)會(huì)在循環(huán)結(jié)束之后才會(huì)運(yùn)行。比如你在自執(zhí)行函數(shù)外面定義一個(gè)數(shù)組,在自執(zhí)行函數(shù)當(dāng)中給這個(gè)數(shù)組追加內(nèi)容旅赢,你在自執(zhí)行函數(shù)之外輸出時(shí)燥爷,會(huì)發(fā)現(xiàn)這個(gè)數(shù)組當(dāng)中什么都沒(méi)有,這就是因?yàn)樽詧?zhí)行函數(shù)會(huì)在循環(huán)運(yùn)行完后才會(huì)執(zhí)行蒸眠。
2、當(dāng)自執(zhí)行函數(shù)在循環(huán)當(dāng)中使用時(shí)杆融,要是自執(zhí)行函數(shù)當(dāng)中嵌套ajax楞卡,那么循環(huán)當(dāng)中的下標(biāo)i就不會(huì)傳進(jìn)ajax當(dāng)中,需要在ajax外面把下標(biāo)i賦值給一個(gè)變量脾歇,在ajax中直接調(diào)用這個(gè)變量就可以了蒋腮。
例子:
$.ajax({
type: "GET",
dataType: "json",
url: "***",
success: function(data) {
//console.log(data);
for (var i = 0; i < data.length; i++) {
(function(i, abbreviation) {
$.ajax({
type: "GET",
url: "/api/faults?abbreviation=" + encodeURI(abbreviation),
dataType: "json",
success: function(result) {
//獲取數(shù)據(jù)后做的事情
}
})
})(i, data[i].abbreviation);
}
}
});
使用遞歸函數(shù)
所謂的遞歸函數(shù)就是在函數(shù)體內(nèi)調(diào)用本函數(shù)。使用遞歸函數(shù)一定要注意藕各,處理不當(dāng)就會(huì)進(jìn)入死循環(huán)池摧。
const asyncDeal = (i) = > {
if (i < 3) {
$.get('/api/changeParts/change_part_standard?part=' + data[i].change_part_name, function(res) {
//獲取數(shù)據(jù)后做的事情
i++;
asyncDeal(i);
})
} else {
//異步完成后做的事情
}
};
asyncDeal(0);
使用async/await
- async/await特點(diǎn)
async/await更加語(yǔ)義化,async 是“異步”的簡(jiǎn)寫(xiě)激况,async function 用于申明一個(gè) function 是異步的作彤; await,可以認(rèn)為是async wait的簡(jiǎn)寫(xiě)乌逐, 用于等待一個(gè)異步方法執(zhí)行完成竭讳;
async/await是一個(gè)用同步思維解決異步問(wèn)題的方案(等結(jié)果出來(lái)之后,代碼才會(huì)繼續(xù)往下執(zhí)行)
可以通過(guò)多層 async function 的同步寫(xiě)法代替?zhèn)鹘y(tǒng)的callback嵌套
- async function語(yǔ)法
自動(dòng)將常規(guī)函數(shù)轉(zhuǎn)換成Promise浙踢,返回值也是一個(gè)Promise對(duì)象
只有async函數(shù)內(nèi)部的異步操作執(zhí)行完绢慢,才會(huì)執(zhí)行then方法指定的回調(diào)函數(shù)
異步函數(shù)內(nèi)部可以使用await
- await語(yǔ)法
await 放置在Promise調(diào)用之前,await 強(qiáng)制后面點(diǎn)代碼等待洛波,直到Promise對(duì)象resolve呐芥,得到resolve的值作為await表達(dá)式的運(yùn)算結(jié)果
await只能在async函數(shù)內(nèi)部使用,用在普通函數(shù)里就會(huì)報(bào)錯(cuò)
const asyncFunc = function(i) {
return new Promise(function(resolve) {
$.get(url, function(res) {
resolve(res);
})
});
}
const asyncDeal = async function() {
for (let i = 0; i < data.length; i++) {
let res = await asyncFunc(i);
//獲取數(shù)據(jù)后做的事情
}
}
asyncDeal();
八、理解模塊化解決的實(shí)際問(wèn)題奋岁,可列舉幾個(gè)模塊化方案并理解其中原理
CommonJS規(guī)范(同步加載模塊)
允許模塊通過(guò)require方法來(lái)同步加載所要依賴的其他模塊思瘟,然后通過(guò)exports或module.exports來(lái)導(dǎo)出需要暴露的接口。
使用方式:
// 導(dǎo)入
require("module");
require("../app.js");
// 導(dǎo)出
exports.getStoreInfo = function() {};
module.exports = someValue;
優(yōu)點(diǎn):
- 簡(jiǎn)單容易使用
- 服務(wù)器端模塊便于復(fù)用
缺點(diǎn):
- 同步加載方式不適合在瀏覽器環(huán)境中使用闻伶,同步意味著阻塞加載滨攻,瀏覽器資源是異步加載的
- 不能非阻塞的并行加載多個(gè)模塊
為什么瀏覽器不能使用同步加載,服務(wù)端可以?
- 因?yàn)槟K都放在服務(wù)器端光绕,對(duì)于服務(wù)端來(lái)說(shuō)模塊加載時(shí)
- 而對(duì)于瀏覽器端女嘲,因?yàn)槟K都放在服務(wù)器端,加載的時(shí)間還取決于網(wǎng)速的快慢等因素诞帐,如果需要等很長(zhǎng)時(shí)間欣尼,整個(gè)應(yīng)用就會(huì)被阻塞。
- 因此停蕉,瀏覽器端的模塊愕鼓,不能采用"同步加載"(CommonJs),只能采用"異步加載"(AMD)慧起。
參照CommonJs模塊代表node.js的模塊系統(tǒng)
AMD(異步加載模塊)
采用異步方式加載模塊菇晃,模塊的加載不影響后面語(yǔ)句的運(yùn)行。所有依賴模塊的語(yǔ)句蚓挤,都定義在一個(gè)回調(diào)函數(shù)中磺送,等到加載完成之后,回調(diào)函數(shù)才執(zhí)行灿意。
使用實(shí)例:
// 定義
define("module", ["dep1", "dep2"], function(d1, d2) {...});
// 加載模塊
require(["module", "../app"], function(module, app) {...});
加載模塊require([module], callback);第一個(gè)參數(shù)[module]估灿,是一個(gè)數(shù)組,里面的成員就是要加載的模塊缤剧;第二個(gè)參數(shù)callback是加載成功之后的回調(diào)函數(shù)馅袁。
優(yōu)點(diǎn):
- 適合在瀏覽器環(huán)境中異步加載模塊
- 可以并行加載多個(gè)模塊
缺點(diǎn):
- 提高了開(kāi)發(fā)成本,代碼的閱讀和書(shū)寫(xiě)比較困難鞭执,模塊定義方式的語(yǔ)義不順暢
- 不符合通用的模塊化思維方式,是一種妥協(xié)的實(shí)現(xiàn)
實(shí)現(xiàn)AMD規(guī)范代表require.js
RequireJS對(duì)模塊的態(tài)度是預(yù)執(zhí)行芒粹。由于 RequireJS 是執(zhí)行的 AMD 規(guī)范, 因此所有的依賴模塊都是先執(zhí)行;即RequireJS是預(yù)先把依賴的模塊執(zhí)行兄纺,相當(dāng)于把require提前了
RequireJS執(zhí)行流程:
- require函數(shù)檢查依賴的模塊,根據(jù)配置文件化漆,獲取js文件的實(shí)際路徑
- 根據(jù)js文件實(shí)際路徑估脆,在dom中插入script節(jié)點(diǎn),并綁定onload事件來(lái)獲取該模塊加載完成的通知座云。
- 依賴script全部加載完成后疙赠,調(diào)用回調(diào)函數(shù)
CMD規(guī)范(異步加載模塊)
CMD規(guī)范和AMD很相似,簡(jiǎn)單朦拖,并與CommonJS和Node.js的 Modules 規(guī)范保持了很大的兼容性圃阳;在CMD規(guī)范中,一個(gè)模塊就是一個(gè)文件璧帝。
定義模塊使用全局函數(shù)define捍岳,其接收 factory 參數(shù),factory 可以是一個(gè)函數(shù),也可以是一個(gè)對(duì)象或字符串锣夹;
factory 是一個(gè)函數(shù)页徐,有三個(gè)參數(shù),function(require, exports, module):
- require 是一個(gè)方法银萍,接受模塊標(biāo)識(shí)作為唯一參數(shù)变勇,用來(lái)獲取其他模塊提供的接口:require(id)
- exports 是一個(gè)對(duì)象,用來(lái)向外提供模塊接口
- module 是一個(gè)對(duì)象贴唇,上面存儲(chǔ)了與當(dāng)前模塊相關(guān)聯(lián)的一些屬性和方法
實(shí)例:
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();
// 依賴就近書(shū)寫(xiě)搀绣,什么時(shí)候用到什么時(shí)候引入
var b = require('./b');
b.doSomething();
});
優(yōu)點(diǎn):
- 依賴就近,延遲執(zhí)行
- 可以很容易在 Node.js 中運(yùn)行
缺點(diǎn):
- 依賴 SPM 打包滤蝠,模塊的加載邏輯偏重
- 實(shí)現(xiàn)代表庫(kù)sea.js:SeaJS對(duì)模塊的態(tài)度是懶執(zhí)行, SeaJS只會(huì)在真正需要使用(依賴)模塊時(shí)才執(zhí)行該模塊
AMD 與 CMD 的區(qū)別
- 對(duì)于依賴的模塊豌熄,AMD 是提前執(zhí)行,CMD 是延遲執(zhí)行物咳。不過(guò) RequireJS 從2.0開(kāi)始锣险,也改成了可以延遲執(zhí)行(根據(jù)寫(xiě)法不同,處理方式不同)览闰。CMD 推崇 as lazy as possible.
- AMD推崇依賴前置芯肤;CMD推崇依賴就近,只有在用到某個(gè)模塊的時(shí)候再去require压鉴。
// AMD
define(['./a', './b'], function(a, b) { // 依賴必須一開(kāi)始就寫(xiě)好
a.doSomething()
// 此處略去 100 行
b.doSomething()
...
});
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此處略去 100 行
var b = require('./b')
// 依賴可以就近書(shū)寫(xiě)
b.doSomething()
// ...
});
UMD
- UMD是AMD和CommonJS的糅合
- AMD 以瀏覽器第一原則發(fā)展異步加載模塊崖咨。
- CommonJS 模塊以服務(wù)器第一原則發(fā)展,選擇同步加載油吭,它的模塊無(wú)需包裝击蹲。
- UMD先判斷是否支持Node.js的模塊(exports)是否存在,存在則使用Node.js模塊模式婉宰;在判斷是否支持AMD(define是否存在)歌豺,存在則使用AMD方式加載模塊。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
ES6模塊化
- ES6 在語(yǔ)言標(biāo)準(zhǔn)的層面上心包,實(shí)現(xiàn)了模塊功能类咧,而且實(shí)現(xiàn)得相當(dāng)簡(jiǎn)單,完全可以取代 CommonJS 和 AMD 規(guī)范蟹腾,成為瀏覽器和服務(wù)器通用的模塊解決方案痕惋。
- ES6 模塊設(shè)計(jì)思想:盡量的靜態(tài)化、使得編譯時(shí)就能確定模塊的依賴關(guān)系娃殖,以及輸入和輸出的變量(CommonJS和AMD模塊值戳,都只能在運(yùn)行時(shí)確定這些東西)。
使用方式:
// 導(dǎo)入
import "/app";
import React from “react”;
import { Component } from “react”;
// 導(dǎo)出
export function multiply() {...};
export var year = 2018;
export default ...
...
優(yōu)點(diǎn):
- 容易進(jìn)行靜態(tài)分析
- 面向未來(lái)的 EcmaScript 標(biāo)準(zhǔn)
缺點(diǎn): - 原生瀏覽器端還沒(méi)有實(shí)現(xiàn)該標(biāo)準(zhǔn)
- 全新的命令字炉爆,新版的 Node.js才支持述寡。
回到問(wèn)題“require與import的區(qū)別”
require使用與CommonJs規(guī)范柿隙,import使用于Es6模塊規(guī)范;所以兩者的區(qū)別實(shí)質(zhì)是兩種規(guī)范的區(qū)別鲫凶;
CommonJS:
- 對(duì)于基本數(shù)據(jù)類型禀崖,屬于復(fù)制。即會(huì)被模塊緩存螟炫;同時(shí)波附,在另一個(gè)模塊可以對(duì)該模塊輸出的變量重新賦值。
- 對(duì)于復(fù)雜數(shù)據(jù)類型昼钻,屬于淺拷貝掸屡。由于兩個(gè)模塊引用的對(duì)象指向同一個(gè)內(nèi)存空間,因此對(duì)該模塊的值做修改時(shí)會(huì)影響另一個(gè)模塊然评。
- 當(dāng)使用require命令加載某個(gè)模塊時(shí)仅财,就會(huì)運(yùn)行整個(gè)模塊的代碼。
- 當(dāng)使用require命令加載同一個(gè)模塊時(shí)碗淌,不會(huì)再執(zhí)行該模塊盏求,而是取到緩存之中的值。也就是說(shuō)亿眠,CommonJS模塊無(wú)論加載多少次碎罚,都只會(huì)在第一次加載時(shí)運(yùn)行一次,以后再加載纳像,就返回第一次運(yùn)行的結(jié)果荆烈,除非手動(dòng)清除系統(tǒng)緩存。
- 循環(huán)加載時(shí)竟趾,屬于加載時(shí)執(zhí)行憔购。即腳本代碼在require的時(shí)候,就會(huì)全部執(zhí)行岔帽。一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載"玫鸟,就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會(huì)輸出山卦。
ES6模塊
- ES6模塊中的值屬于【動(dòng)態(tài)只讀引用】鞋邑。
- 對(duì)于只讀來(lái)說(shuō)诵次,即不允許修改引入變量的值账蓉,import的變量是只讀的,不論是基本數(shù)據(jù)類型還是復(fù)雜數(shù)據(jù)類型逾一。當(dāng)模塊遇到import命令時(shí)铸本,就會(huì)生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí)遵堵,再根據(jù)這個(gè)只讀引用箱玷,到被加載的那個(gè)模塊里面去取值怨规。
- 對(duì)于動(dòng)態(tài)來(lái)說(shuō),原始值發(fā)生變化锡足,import加載的值也會(huì)發(fā)生變化波丰。不論是基本數(shù)據(jù)類型還是復(fù)雜數(shù)據(jù)類型。
- 循環(huán)加載時(shí)舶得,ES6模塊是動(dòng)態(tài)引用掰烟。只要兩個(gè)模塊之間存在某個(gè)引用,代碼就能夠執(zhí)行沐批。
最后:require/exports 是必要通用且必須的纫骑;因?yàn)槭聦?shí)上,目前你編寫(xiě)的 import/export 最終都是編譯為 require/exports 來(lái)執(zhí)行的九孩。