免費(fèi)視頻在B站
輕松應(yīng)對大廠面試-徹底理解js作用域和閉包_嗶哩嗶哩 (゜-゜)つロ 干杯~-bilibili
一. 預(yù)編譯
1 概念
1) 什么是預(yù)編譯
首先, 我們要知道Javascript是解釋性語言
- 解釋性: 逐行解析, 逐行執(zhí)行
那么, 什么是預(yù)編譯呢?
在Javascript真正被解析之前, js解析引擎會首先把整個(gè)文件進(jìn)行預(yù)處理, 以消除一些歧義. 這個(gè)預(yù)處理的過程就被稱為預(yù)編譯
示例
console.log(a)
var a = 123
console.log(a)
function a() {
console.log(a)
}
a()
這是一段奇怪的代碼, 大家可以先思考一下, 三個(gè)console.log分別會打印出什么
如果要完全理解, 我們就需要深入的分析js引擎到底是如何工作的!!!
2) 全局對象GO
全局對象
全局對象(Global Object):
- 在瀏覽器環(huán)境中,
js引擎會整合<script>標(biāo)簽中的內(nèi)容, 產(chǎn)生window對象, 這個(gè)window對象就是全局對象 - 在node環(huán)境中, 會產(chǎn)生
global對象
全局變量
在<script>標(biāo)簽中聲明的變量為全局變量, 全局變量會作為window對象的屬性存在!!
示例
var a = 100
console.log(a)
console.log(window.a)
這里打印a實(shí)際上相當(dāng)于打印window對象的a屬性
擴(kuò)展
啥叫整合?
示例
<script>
var a = 100
console.log(a)
console.log(window.a)
</script>
<script>
// 在這里能訪問到a嗎???
console.log(a)
</script>
- 可以, 因?yàn)?code>js引擎會把所有的
<script>標(biāo)簽整合到一起, 生成一個(gè)window對象
全局函數(shù)
在<script>標(biāo)簽中聲明的函數(shù)為全局函數(shù), 全局函數(shù)會作為window對象的方法存在!!
示例
function a() {
console.log('111')
}
console.log(window.a)
那么問題來了, 當(dāng)同時(shí)定義變量a和函數(shù)a時(shí), 會發(fā)生什么呢?
就像我們看到的奇怪代碼里一樣, 而預(yù)編譯就是為了處理類似的這些沖突
3) 活動對象AO
活動對象
活動對象(Activation Object): 也叫激活對象
- 在函數(shù)被調(diào)用時(shí)產(chǎn)生, 用來保存當(dāng)前函數(shù)內(nèi)部的執(zhí)行環(huán)境(Execution Context), 也叫執(zhí)行期上下文
- 在函數(shù)調(diào)用結(jié)束時(shí)銷毀
局部變量
在函數(shù)內(nèi)部聲明的變量叫局部變量, 局部變量做為AO對象的屬性存在
示例
function a() {
var i = 0
console.log(i)
}
a()
如何理解局部
在函數(shù)a的外部, 不能訪問變量i, 變量i只在函數(shù)a的范圍內(nèi)才能使用. 其實(shí), 這也就是作用域的由來, skr~
- 如果不執(zhí)行函數(shù), 不會產(chǎn)生
AO對象, 就不會存在i屬性 - 如果執(zhí)行函數(shù), 就會產(chǎn)生
AO對象, 并將變量i作為AO對象的屬性 - 函數(shù)執(zhí)行完后, AO對象被銷毀, 也就意味著不能使用
i屬性
局部函數(shù)
在函數(shù)內(nèi)部聲明的函數(shù)叫局部函數(shù), 局部函數(shù)做為AO對象的方法存在
示例
function a() {
function b() {
console.log(222)
}
b()
}
a()
2 全局預(yù)編譯
1) 流程
- 查找變量聲明, 作為GO對象的屬性名, 值為undefined
- 查找函數(shù)聲明, 作為GO對象的屬性名, 值為function
變量聲明
通過var關(guān)鍵字聲明變量
var a // 變量聲明
var a = 111 // 變量聲明+變量賦值
函數(shù)聲明
通過function關(guān)鍵字聲明函數(shù)
function a () {} // 函數(shù)聲明
var a = function () {} // 函數(shù)表達(dá)式, 不是函數(shù)聲明
示例
console.log(a)
var a = 100
console.log(a)
function a() {
console.log(111)
}
console.log(a)
2) 結(jié)論
如果存在同名的變量和函數(shù), 函數(shù)的優(yōu)先級高
3 函數(shù)預(yù)編譯
1) 流程
- 在函數(shù)被調(diào)用時(shí), 為當(dāng)前函數(shù)產(chǎn)生
AO對象 - 查找形參和變量聲明作為
AO對象的屬性名, 值為undefined - 使用實(shí)參的值改變形參的值
- 查找函數(shù)聲明, 作為
AO對象的屬性名, 值為function
2) 示例
示例一
function a(test) {
var i = 0
function b() {
console.log(222)
}
b()
}
a(1)
在函數(shù)a的AO對象中, 存在三個(gè)屬性
- test: 形參, 值為1
- i: 局部變量, 值為0
- b: 局部函數(shù)
示例二
function a(test) {
console.log(b)
var b = 0
console.log(b)
function b() {
console.log(222)
}
}
a(1)
當(dāng)局部變量與局部函數(shù)同名時(shí), 函數(shù)的優(yōu)先級高
示例三
function a(b, c) {
console.log(b)
var b = 0
console.log(b)
function b() {
console.log(222)
}
console.log(c)
}
a(1)
示例四
function a(i) {
var i
console.log(i)
}
a(1)
3) 結(jié)論
只要聲明了局部函數(shù), 函數(shù)的優(yōu)先級最高
沒有聲明局部函數(shù), 實(shí)參的優(yōu)先級高
整體來說: 局部函數(shù) > 實(shí)參 > 形參和局部變量
二. 作用域與作用域鏈
1 概念
1) 域
域: 范圍, 區(qū)域
在js中, 作用域分為全局作用域和局部作用域
- 全局作用域: 由
<script>標(biāo)簽產(chǎn)生的區(qū)域, 從計(jì)算機(jī)的角度可以理解為window對象 - 局部作用域: 由函數(shù)產(chǎn)生的區(qū)域, 從計(jì)算機(jī)的角度可以理解為該函數(shù)的AO對象
2) 作用域鏈
在js中, 函數(shù)存在一個(gè)隱式屬性[[scopes]], 這個(gè)屬性用來保存當(dāng)前函數(shù)在執(zhí)行時(shí)的環(huán)境(上下文), 由于在數(shù)據(jù)結(jié)構(gòu)上是鏈?zhǔn)降? 也被稱為作用域鏈. 我們可以把它理解成一個(gè)數(shù)組
函數(shù)類型存在[[scopes]]屬性
function a() {}
console.dir(a) // 打印內(nèi)部結(jié)構(gòu)
輸出
[[scopes]]屬性在函數(shù)聲明時(shí)產(chǎn)生, 在函數(shù)被調(diào)用時(shí)更新
[[scopes]]屬性記錄當(dāng)前函數(shù)的執(zhí)行環(huán)境
在函數(shù)被調(diào)用時(shí), 將該函數(shù)的AO對象壓入到[[scopes]]中
示例
function a() {
console.dir(a)
function b() {
console.dir(b)
function c() {
console.dir(c)
}
c()
}
b()
}
a()
[[scopes]]屬性是一個(gè)數(shù)組的形式
0: 是函數(shù)b的AO對象
1: 是GO對象
2 作用
作用域鏈有什么作用呢?
在訪問變量或者函數(shù)時(shí), 會在作用域鏈上依次查找, 最直觀的表現(xiàn)是:
- 內(nèi)部函數(shù)可以使用外部函數(shù)聲明的變量
示例
function a() {
var aa = 111
function b() {
console.log(aa)
}
b()
}
a()
- 在函數(shù)a中聲明定義了變量aa
- 在函數(shù)b中沒有聲明, 卻可以使用
思考
如果在函數(shù)b中, 也定義同名變量aa會怎樣
示例
function a() {
var aa = 111
function b() {
var aa = 222
console.log(aa)
}
b()
}
a()
第一個(gè)問題: 函數(shù)a和函數(shù)b里的變量aa是不是同一個(gè)變量?
第二個(gè)問題: 函數(shù)b里打印的aa是用的誰?
結(jié)論
內(nèi)部函數(shù)可以使用外部函數(shù)的變量
外部函數(shù)不能使用內(nèi)部函數(shù)的變量
三. 閉包
如果在內(nèi)部函數(shù)使用了外部函數(shù)的變量, 就會形成閉包. 閉包保留了外部環(huán)境的引用
如果內(nèi)部函數(shù)被返回到了外部函數(shù)的外面, 在外部函數(shù)執(zhí)行完后, 依然可以使用閉包里的值
1 閉包的形成
在內(nèi)部函數(shù)使用外部函數(shù)的變量, 就會形成閉包, 閉包是當(dāng)前作用域的延伸
示例
function a() {
var aa = 100
function b() {
console.log(aa)
}
b()
}
a()
從代碼的角度看, 閉包也是一個(gè)對象, 閉包里包含哪些東西呢?
在內(nèi)部函數(shù)b中使用了外部函數(shù)a中的變量, 這個(gè)變量就會作為閉包對象的屬性!!
思考
function a() {
var aa = 100
function b() {
console.log(b)
}
b()
}
a()
- 會不會形成閉包?
- 如果形成, 閉包里有什么?
答案
會形成閉包, 由于b的聲明是在外部函數(shù)a中的, 在內(nèi)部函數(shù)b中使用了b, 會形成閉包
閉包里存放了一個(gè)屬性, 就是b函數(shù)
思考
function a() {
var aa = 100
function b() {
var b = 200
console.log(b)
}
b()
}
a()
- 會不會形成閉包?
答案
不會形成閉包, 由于在b函數(shù)內(nèi)部定義了變量b, 打印時(shí)直接使用的是內(nèi)部函數(shù)里的變量b, 不會形成閉包
2 閉包的保持
如果希望在函數(shù)調(diào)用后, 閉包依然保持, 就需要將內(nèi)部函數(shù)返回到外部函數(shù)的外部
示例
function a() {
var num = 0
function b() {
console.log(num++)
}
return b
}
var demo = a()
console.dir(demo)
demo()
demo()
第8行, 調(diào)用a函數(shù), 將內(nèi)部函數(shù)b返回, 保存在函數(shù)a的外部
第9行, 調(diào)用demo函數(shù), 實(shí)質(zhì)上是調(diào)用內(nèi)部函數(shù), 在函數(shù)b的[[scopes]]屬性中可以找到閉包對象, 從而訪問到里面的值
3 總結(jié)
使用閉包要滿足兩個(gè)條件
- 閉包要形成: 在內(nèi)部函數(shù)使用外部函數(shù)的變量
- 閉包要保持: 內(nèi)部函數(shù)返回到外部函數(shù)的外面
四. 閉包的應(yīng)用
1 閉包的兩面性
任何事物都有兩面性
好處: 一般來說, 在函數(shù)外部是沒辦法訪問函數(shù)內(nèi)部的變量的, 設(shè)計(jì)閉包最主要的作用就是為了解決這個(gè)問題.
壞處: 有時(shí)不注意使用了閉包, 會導(dǎo)致出現(xiàn)意想不到的結(jié)果
2 閉包的應(yīng)用
- 在函數(shù)外部訪問私有變量
- 實(shí)現(xiàn)封裝
- 防止污染全局變量
示例
在函數(shù)外部訪問私有變量
function a() {
var num = 0
function b() {
console.log(num++)
}
return b
}
var demo = a()
console.dir(demo)
demo()
本來在函數(shù)a的外部(全局)不能直接訪問內(nèi)部變量num, 通過閉包就可以使用num變量了
示例
function Person() {
var uname
function setName(uname) {
this.uname = uname
}
function getName() {
return this.uname
}
return {
getName: getName,
setName: setName,
}
}
var xiaopang = Person()
xiaopang.setName('xiaopang')
var name = xiaopang.getName()
console.log(name)
定義了一個(gè)函數(shù)Person, 一個(gè)內(nèi)部變量uname, 兩個(gè)內(nèi)部函數(shù)
返回內(nèi)部函數(shù), 也是使用了閉包特性
這樣在Person函數(shù)的外部, 通過get和set方法對變量uname進(jìn)行操作, 這就是面向?qū)ο?/code>里的封裝的思想
3 閉包的問題
在很多時(shí)候, 我們寫的代碼會無意識的用了閉包, 但是這并不是我們想要的結(jié)果.
這種情況應(yīng)該盡量避免, 或者說遇到了這類bug時(shí), 我們應(yīng)該知道如何解決
示例
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
arr[0]()
- 會不會形成閉包?
- 打印結(jié)果是什么?
- 為什么
示例
var arr = []
function a() {
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
}
a()
arr[0]()
- 會不會形成閉包?
- 打印結(jié)果是什么?
- 為什么
雖然看起來結(jié)果一樣, 但是執(zhí)行的過程有很大的差異~
現(xiàn)在問題是如果希望依然打印0~9, 該怎么解決
五. 立執(zhí)行函數(shù)
其實(shí), 不管是上述哪種情況, 根本的問題在于js中沒有塊作用域
1 塊作用域
由{}形成的作用區(qū)域
示例
for (var i = 0; i < 10; i++) {
console.log(i)
}
console.log('for執(zhí)行完了, i=' + i)
首先, 我們從預(yù)編譯的角度來分析一下
-
var i屬于變量聲明, 會在window對象添加一個(gè)屬性i - 執(zhí)行for循環(huán), 沒有函數(shù), 不會產(chǎn)生局部作用域, 在for循環(huán)結(jié)束后, i依然可以訪問
一般情況下, 我們更希望循環(huán)變量i只在循環(huán)體內(nèi)有效, 也就是{}里有效. 這個(gè)就是塊作用域
如何解決呢
2 立執(zhí)行函數(shù)
由于函數(shù)會產(chǎn)生作用域, 我們可以嘗試在for循環(huán)中寫一個(gè)函數(shù), 并調(diào)用函數(shù)
示例
for (var i = 0; i < 10; i++) {
function a(j) {
console.log(j)
}
a(i)
}
像這種聲明了馬上執(zhí)行的函數(shù)就是立執(zhí)行函數(shù), 可以合并寫在一起
示例
for (var i = 0; i < 10; i++) {
(function (j) {
console.log(j)
})(i)
}
前面的()表示函數(shù)的聲明
后面的()表示函數(shù)的執(zhí)行
示例
var arr = []
function a() {
for (var i = 0; i < 10; i++) {
(function (i) {
arr[i] = function () {
console.log(i)
}
})(i)
}
}
a()
console.dir(arr)