輕松應(yīng)對大廠面試-徹底理解js作用域和閉包

免費(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) 流程

  1. 查找變量聲明, 作為GO對象的屬性名, 值為undefined
  2. 查找函數(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) 流程

  1. 在函數(shù)被調(diào)用時(shí), 為當(dāng)前函數(shù)產(chǎn)生AO對象
  2. 查找形參和變量聲明作為AO對象的屬性名, 值為undefined
  3. 使用實(shí)參的值改變形參的值
  4. 查找函數(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()
  1. 會不會形成閉包?
  2. 如果形成, 閉包里有什么?

答案

會形成閉包, 由于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()
  1. 會不會形成閉包?

答案

不會形成閉包, 由于在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è)條件

  1. 閉包要形成: 在內(nèi)部函數(shù)使用外部函數(shù)的變量
  2. 閉包要保持: 內(nèi)部函數(shù)返回到外部函數(shù)的外面

四. 閉包的應(yīng)用

1 閉包的兩面性

任何事物都有兩面性

好處: 一般來說, 在函數(shù)外部是沒辦法訪問函數(shù)內(nèi)部的變量的, 設(shè)計(jì)閉包最主要的作用就是為了解決這個(gè)問題.

壞處: 有時(shí)不注意使用了閉包, 會導(dǎo)致出現(xiàn)意想不到的結(jié)果

2 閉包的應(yīng)用

  1. 在函數(shù)外部訪問私有變量
  2. 實(shí)現(xiàn)封裝
  3. 防止污染全局變量

示例

在函數(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]()
  1. 會不會形成閉包?
  2. 打印結(jié)果是什么?
  3. 為什么

示例

var arr = []
function a() {
  for (var i = 0; i < 10; i++) {
    arr[i] = function () {
      console.log(i)
    }
  }
}
a()
arr[0]()
  1. 會不會形成閉包?
  2. 打印結(jié)果是什么?
  3. 為什么

雖然看起來結(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ù)編譯的角度來分析一下

  1. var i屬于變量聲明, 會在window對象添加一個(gè)屬性i
  2. 執(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)

3 函數(shù)表達(dá)式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容