溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Javascript之作用域、作用域鏈、閉包的示例分析

發(fā)布時(shí)間:2021-08-07 10:08:43 來源:億速云 閱讀:144 作者:小新 欄目:web開發(fā)

這篇文章主要介紹Javascript之作用域、作用域鏈、閉包的示例分析,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!

什么是作用域?

作用域是一種規(guī)則,在代碼編譯階段就確定了,規(guī)定了變量與函數(shù)的可被訪問的范圍。全局變量擁有全局作用域,局部變量則擁有局部作用域。 js是一種沒有塊級(jí)作用域的語言(包括if、for等語句的花括號(hào)代碼塊或者單獨(dú)的花括號(hào)代碼塊都不能形成一個(gè)局部作用域),所以js的局部作用域的形成有且只有函數(shù)的花括號(hào)內(nèi)定義的代碼塊形成的,既函數(shù)作用域。

什么是作用域鏈?

作用域鏈?zhǔn)亲饔糜蛞?guī)則的實(shí)現(xiàn),通過作用域鏈的實(shí)現(xiàn),變量在它的作用域內(nèi)可被訪問,函數(shù)在它的作用域內(nèi)可被調(diào)用。

作用域鏈?zhǔn)且粋€(gè)只能單向訪問的鏈表,這個(gè)鏈表上的每個(gè)節(jié)點(diǎn)就是執(zhí)行上下文的變量對(duì)象(代碼執(zhí)行時(shí)就是活動(dòng)對(duì)象),單向鏈表的頭部(可被第一個(gè)訪問的節(jié)點(diǎn))始終都是當(dāng)前正在被調(diào)用執(zhí)行的函數(shù)的變量對(duì)象(活動(dòng)對(duì)象),尾部始終是全局活動(dòng)對(duì)象。

作用域鏈的形成?

我們從一段代碼的執(zhí)行來看作用域鏈的形成過程。

function fun01 () {
 console.log('i am fun01...');
 fun02();
}
function fun02 () {
 console.log('i am fun02...');
}
fun01();

Javascript之作用域、作用域鏈、閉包的示例分析

數(shù)據(jù)訪問流程

如上圖,當(dāng)程序訪問一個(gè)變量時(shí),按照作用域鏈的單向訪問特性,首先在頭節(jié)點(diǎn)的AO中查找,沒有則到下一節(jié)點(diǎn)的AO查找,最多查找到尾節(jié)點(diǎn)(global AO)。在這個(gè)過程中找到了就找到了,沒找到就報(bào)錯(cuò)undefined。

延長(zhǎng)作用域鏈

從上面作用域鏈的形成可以看出鏈上的每個(gè)節(jié)點(diǎn)是在函數(shù)被調(diào)用執(zhí)行是向鏈頭unshift進(jìn)當(dāng)前函數(shù)的AO,而節(jié)點(diǎn)的形成還有一種方式就是“延長(zhǎng)作用域鏈”,既在作用域鏈的頭部插入一個(gè)我們想要的對(duì)象作用域。延長(zhǎng)作用域鏈有兩種方式:

1.with語句

function fun01 () {
 with (document) {
  console.log('I am fun01 and I am in document scope...')
 }
}
fun01();

Javascript之作用域、作用域鏈、閉包的示例分析

2.try-catch語句的catch塊

function fun01 () {
 try {
  console.log('Some exceptions will happen...')
 } catch (e) {
  console.log(e)
 }
}
fun01();

Javascript之作用域、作用域鏈、閉包的示例分析

ps:個(gè)人感覺with語句使用需求不多,try-catch的使用也是看需求的。個(gè)人對(duì)這兩種使用不多,但是在進(jìn)行這部分整理過程中萌發(fā)了一點(diǎn)點(diǎn)在作用域鏈層面的不成熟的性能優(yōu)化小建議。

由作用域鏈引發(fā)的關(guān)于性能優(yōu)化的一點(diǎn)不成熟的小建議

1.減少變量的作用域鏈的訪問節(jié)點(diǎn)

這里我們自定義一個(gè)名次叫做“查找距離”,表示程序訪問到一個(gè)非undefined變量在作用域鏈中經(jīng)過的節(jié)點(diǎn)數(shù)。因?yàn)槿绻诋?dāng)前節(jié)點(diǎn)沒有找到變量,就跳到下一個(gè)節(jié)點(diǎn)查找,還要進(jìn)行判斷下一個(gè)節(jié)點(diǎn)中是否存在被查找變量。“查找距離”越長(zhǎng),要做的“跳”動(dòng)作和“判斷”動(dòng)作也就越多,資源開銷就越大,從而影響性能。這種性能帶來的差距可能少數(shù)的幾次變量查找操作不會(huì)帶來太多性能問題,但如果是多次進(jìn)行變量查找,性能對(duì)比則比較明顯了。

(function(){
 console.time()
 var find = 1      //這個(gè)find變量需要在4個(gè)作用域鏈節(jié)點(diǎn)進(jìn)行查找
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    while(i <= 100000000){
     if(find){
      i++
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

Javascript之作用域、作用域鏈、閉包的示例分析

(function(){
 console.time()
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    var find = 1      //這個(gè)find變量只在當(dāng)前節(jié)點(diǎn)進(jìn)行查找
    while(i <= 100000000){
     if(find){
      i++
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

Javascript之作用域、作用域鏈、閉包的示例分析

在mac pro的chrome瀏覽器下做實(shí)驗(yàn),進(jìn)行1億次查找運(yùn)算。

實(shí)驗(yàn)結(jié)果:前者運(yùn)行5次平均耗時(shí)85.599ms,后者運(yùn)行5次平均耗時(shí)63.127ms。

2.避免作用域鏈內(nèi)節(jié)點(diǎn)AO上過多的變量定義

過多的變量定義造成性能問題的原因主要是查找變量過程中的“判斷”操作開銷較大。我們使用with來進(jìn)行性能對(duì)比。

(function(){
 console.time()
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    var find = 10
    with (document) {
     while(i <= 1000000){
      if(find){
       i++
      }
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

Javascript之作用域、作用域鏈、閉包的示例分析

在mac pro的chrome瀏覽器下做實(shí)驗(yàn),進(jìn)行100萬次查找運(yùn)算,借助with使用document進(jìn)行的延長(zhǎng)作用域鏈,因?yàn)閐ocument下的變量屬性比較多,可以測(cè)試在多變量作用域鏈節(jié)點(diǎn)下進(jìn)行查找的性能差異。

實(shí)驗(yàn)結(jié)果:5次平均耗時(shí)558.802ms,而如果刪掉with和document,5次平均耗時(shí)0.956ms。

當(dāng)然,這兩個(gè)實(shí)驗(yàn)是在我們假設(shè)的極端環(huán)境下進(jìn)行的,結(jié)果僅供參考!

關(guān)于閉包

1.什么是閉包?

函數(shù)對(duì)象可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)體內(nèi)的數(shù)據(jù)(變量和函數(shù)聲明)都可以保存在函數(shù)作用域內(nèi),這種特性在計(jì)算機(jī)科學(xué)文獻(xiàn)中被稱為“閉包”。既函數(shù)體內(nèi)的數(shù)據(jù)被隱藏于作用于鏈內(nèi),看起來像是函數(shù)將數(shù)據(jù)“包裹”了起來。從技術(shù)角度來說,js的函數(shù)都是閉包:函數(shù)都是對(duì)象,都關(guān)聯(lián)到作用域鏈,函數(shù)內(nèi)數(shù)據(jù)都被保存在函數(shù)作用域內(nèi)。

2.閉包的幾種實(shí)現(xiàn)方式

實(shí)現(xiàn)方式就是函數(shù)A在函數(shù)B的內(nèi)部進(jìn)行定義了,并且當(dāng)函數(shù)A在執(zhí)行時(shí),訪問了函數(shù)B內(nèi)部的變量對(duì)象,那么B就是一個(gè)閉包。如下:

Javascript之作用域、作用域鏈、閉包的示例分析

Javascript之作用域、作用域鏈、閉包的示例分析

如上兩圖所示,是在chrome瀏覽器下查看閉包的方法。兩種方式的共同點(diǎn)是都有一個(gè)外部函數(shù)outerFun(),都在外部函數(shù)內(nèi)定義了內(nèi)部函數(shù)innerFun(),內(nèi)部函數(shù)都訪問了外部函數(shù)的數(shù)據(jù)。不同的是,第一種方式的innerFun()是在outerFun()內(nèi)被調(diào)用的,既聲明和被調(diào)用均在同一個(gè)執(zhí)行上下文內(nèi)。而第二種方式的innerFun()則是在outerFun()外被調(diào)用的,既聲明和被調(diào)用不在同一個(gè)執(zhí)行上下文。第二種方式恰好是js使用閉包常用的特性所在:通過閉包的這種特性,可以在其他執(zhí)行上下文內(nèi)訪問函數(shù)內(nèi)部數(shù)據(jù)。

我們更常用的一種方式則是這樣的:

//閉包實(shí)例
function outerFun () {
 var outerV1 = 10
 function outerF1 () {
  console.log('I am outerF1...')
 }
 function innerFun () {
  var innerV1 = outerV1
  outerF1()
 }
 return innerFun //return回innerFun()內(nèi)部函數(shù)
}
var fn = outerFun()  //接到return回的innerFun()函數(shù)
fn()     //執(zhí)行接到的內(nèi)部函數(shù)innerFun()

此時(shí)它的作用域鏈?zhǔn)沁@樣的:

Javascript之作用域、作用域鏈、閉包的示例分析

3.閉包的好處及使用場(chǎng)景

js的垃圾回收機(jī)制可以粗略的概括為:如果當(dāng)前執(zhí)行上下文執(zhí)行完畢,且上下文內(nèi)的數(shù)據(jù)沒有其他引用,則執(zhí)行上下文pop出call stack,其內(nèi)數(shù)據(jù)等待被垃圾回收。而當(dāng)我們?cè)谄渌麍?zhí)行上下文通過閉包對(duì)執(zhí)行完的上下文內(nèi)數(shù)據(jù)仍然進(jìn)行引用時(shí),那么被引用的數(shù)據(jù)則不會(huì)被垃圾回收。就像上面代碼中的outerV1,放我們?cè)谌稚舷挛耐ㄟ^調(diào)用innerFun()仍然訪問引用outerV1時(shí),那么outerFun執(zhí)行完畢后,outerV1也不會(huì)被垃圾回收,而是保存在內(nèi)存中。另外,outerV1看起來像不像一個(gè)outerFun的私有內(nèi)部變量呢?除了innerFun()外,我們無法隨意訪問outerV1。所以,綜上所述,這樣閉包的使用情景可以總結(jié)為:

(1)進(jìn)行變量持久化。

(2)使函數(shù)對(duì)象內(nèi)有更好的封裝性,內(nèi)部數(shù)據(jù)私有化。

進(jìn)行變量持久化方面舉個(gè)栗子:

我們假設(shè)一個(gè)需求時(shí)寫一個(gè)函數(shù)進(jìn)行類似id自增或者計(jì)算函數(shù)被調(diào)用的功能,普通青年這樣寫:

 var count = 0
 function countFun () {
  return count++
 }

這樣寫固然實(shí)現(xiàn)了功能,但是count被暴露在外,可能被其他代碼篡改。這個(gè)時(shí)候閉包青年就會(huì)這樣寫:

function countFun () {
 var count = 0
 return function(){
  return count++
 }
}
var a = countFun()
a()

這樣count就不會(huì)被不小心篡改了,函數(shù)調(diào)用一次就count加一次1。而如果結(jié)合“函數(shù)每次被調(diào)用都會(huì)創(chuàng)建一個(gè)新的執(zhí)行上下文”,這種count的安全性還有如下體現(xiàn):

function countFun () {
 var count = 0
 return {
  count: function () {
   count++
  },
  reset: function () {
   count = 0
  },
  printCount: function () {
   console.log(count)
  }
 }
}
var a = countFun()
var b = countFun()
a.count()
a.count()
b.count()
b.reset()
a.printCount()  //打印:2 因?yàn)閍.count()被調(diào)用了兩次
b.printCount()  //打印出:0 因?yàn)檎{(diào)用了b.reset()

以上便是閉包提供的變量持久化和封裝性的體現(xiàn)。

4.閉包的注意事項(xiàng)

由于閉包中的變量不會(huì)像其他正常變量那種被垃圾回收,而是一直存在內(nèi)存中,所以大量使用閉包可能會(huì)造成性能問題。

以上是“Javascript之作用域、作用域鏈、閉包的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI