成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JS學(xué)習(xí)系列 03 - 函數(shù)作用域和塊作用域

Ashin / 1411人閱讀

摘要:在及之前版本,只擁有函數(shù)作用域,沒有塊作用域和除外。函數(shù)表達(dá)式分為匿名函數(shù)表達(dá)式和具名函數(shù)表達(dá)式。但是,由于這個(gè)事件回調(diào)函數(shù)形成了一個(gè)覆蓋當(dāng)前作用域的閉包,引擎極有可能依然保存著這個(gè)數(shù)據(jù)結(jié)構(gòu)取決于具體實(shí)現(xiàn)。總結(jié)函數(shù)是中最常見的作用域單元。

在 ES5 及之前版本,JavaScript 只擁有函數(shù)作用域,沒有塊作用域(with 和 try...catch 除外)。在 ES6 中,JS 引入了塊作用域,{ } 內(nèi)是多帶帶的一個(gè)作用域。采用 let 或者 const 聲明的變量會(huì)挾持所在塊的作用域,也就是說,這聲明關(guān)鍵字會(huì)將變量綁定到所在的任意作用域中(通常是 {...} 內(nèi)部)。

今天,我們就來深入研究一下函數(shù)作用域塊作用域。

1. 函數(shù)中的作用域

函數(shù)作用域的含義是指,屬于這個(gè)函數(shù)的任何聲明(變量或函數(shù))都可以在這個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(包括這個(gè)函數(shù)嵌套內(nèi)的作用域)。

舉個(gè)例子:

function foo (a) {
   var b = 2;

   // something else

   function bar () {
      // something else   
   }

   var c = 3;
}

bar();      // 報(bào)錯(cuò),ReferenceError: bar is not defined
console.log(a, b, c);        // 報(bào)錯(cuò),原因同上

在這段代碼中,函數(shù) foo 的作用域包含了標(biāo)識(shí)符a、b、c 和 bar ,函數(shù) bar 的作用域中又包含別的標(biāo)識(shí)符。

由于標(biāo)識(shí)符 a、b、c 和 bar都屬于函數(shù) foo 的作用域,所以在全局作用域中訪問會(huì)報(bào)錯(cuò),因?yàn)樗鼈兌紱]有定義,但是在函數(shù) foo 內(nèi)部,這些標(biāo)識(shí)符都是可以訪問的,這就是函數(shù)作用域。

1.1 為什么要有這些作用域

當(dāng)我們用作用域把代碼包起來的時(shí)候,其實(shí)就是對(duì)它們進(jìn)行了“隱藏”,讓我們對(duì)其有控制權(quán),想讓誰訪問就可以讓誰訪問,想禁止訪問也很容易。

想像一下,如果所有的變量和函數(shù)都在全局作用域中,當(dāng)然我們可以在內(nèi)部的嵌套作用域中訪問它們,但是因?yàn)楸┞读颂嗟淖兞炕蚝瘮?shù),它們可能被有意或者無意的篡改,以非預(yù)期的方式使用,這就導(dǎo)致我們的程序會(huì)出現(xiàn)各種各樣的問題,嚴(yán)重會(huì)導(dǎo)致數(shù)據(jù)泄露,造成無法挽回的后果。

例如:

var obj = {
   a: 2,
   getA: function () {
      return this.a;
   }
};

obj.a = 4;
obj.getA();      // 4

這個(gè)例子中,我們可以任意修改對(duì)象 obj 內(nèi)部的值,在某種情況下這并不是我們所期望的,采用函數(shù)作用域就可以解決這個(gè)問題,私有化變量 a 。

var obj = (function () {
  var a = 2;
  return {
     getA: function () {
        return a;
     },
     setA: function (val) {
        a = val;
     }
  }
}());

obj.a = 4;
obj.getA();      // 2
obj.setA(8);
obj.getA();      // 8

這里通過立即執(zhí)行函數(shù)(IIFE)返回一個(gè)對(duì)象,只能通過對(duì)象內(nèi)的方法對(duì)變量 a 進(jìn)行操作,其實(shí)這里有閉包的存在,這個(gè)我們在以后會(huì)深入討論。

“隱藏”作用域中的變量和函數(shù)所帶來的另一個(gè)好處,是可以避免同名標(biāo)識(shí)符之間的沖突,沖突會(huì)導(dǎo)致變量的值被意外覆蓋。

例如:

function foo () {
   function bar (a) {
      i = 3;        // 修改了 for 循環(huán)所屬作用域中的 i
      console.log(a + i);
   }

   for (var i = 0; i < 10; i++) {
      bar(i * 2);      // 這里因?yàn)?i 總會(huì)被設(shè)置為 3 ,導(dǎo)致無限循環(huán)
   }
}

foo();

bar(...) 內(nèi)部的賦值表達(dá)式 i = 3 意外的覆蓋了聲明在 foo(...) 內(nèi)部 for 循環(huán)中的 i ,在這個(gè)例子中因?yàn)?i 始終被設(shè)置為 3 ,永遠(yuǎn)滿足小于 10 這個(gè)條件,導(dǎo)致無限循環(huán)。

bar(...) 內(nèi)部的賦值操作需要聲明一個(gè)本地變量來使用,采用任何名字都可以,var i = 3; 就可以滿足這個(gè)要求。另外一種方法是采用一個(gè)完全不同的標(biāo)識(shí)符名稱,比如 var j = 3; 。但是軟件設(shè)計(jì)在某種情況下可能自然而然的要求使用同樣的標(biāo)識(shí)符名稱,因此在這種情況下使用作用域來“隱藏”內(nèi)部聲明是唯一的最佳選擇。

總結(jié)來說,作用域可以起到兩個(gè)作用:

私有化變量或函數(shù)

規(guī)避同名沖突

1.2 函數(shù)聲明和函數(shù)表達(dá)式

如果 function 是聲明中的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。

函數(shù)聲明舉個(gè)例子:

function foo () {
   // something else
}

這就是一個(gè)函數(shù)聲明。

函數(shù)表達(dá)式分為匿名函數(shù)表達(dá)式和具名函數(shù)表達(dá)式。

對(duì)于函數(shù)表達(dá)式來說,最熟悉的場景可能就是回調(diào)參數(shù)了,例如:

setTimeout(function () {
   console.log("I wait for one second.")
}, 1000);

這個(gè)叫作匿名函數(shù)表達(dá)式,因?yàn)?function ()... 沒有名稱標(biāo)識(shí)符。函數(shù)表達(dá)式可以是匿名的,但是函數(shù)聲明不可以省略函數(shù)名,在 javascript 中這是非法的。

匿名函數(shù)表達(dá)式書寫簡便,但是它也有幾個(gè)缺點(diǎn)需要注意:

匿名函數(shù)在瀏覽器棧追蹤中不會(huì)顯示出有意義的函數(shù)名,這會(huì)加大調(diào)試難度。

如果沒有函數(shù)名,當(dāng)函數(shù)需要引用自身的時(shí)候就只能使用已經(jīng)不是標(biāo)準(zhǔn)的 arguments.callee 來引用,比如遞歸。在事件觸發(fā)后的事件監(jiān)聽器中也有可能需要通過函數(shù)名來解綁自身。

匿名函數(shù)對(duì)代碼的可讀性和可理解性有一定的影響。一個(gè)有意義的函數(shù)名可以讓代碼不言自明。

具名函數(shù)表達(dá)式又叫行內(nèi)函數(shù)表達(dá)式,例如:

setTimeout(function timerHandler () {
   console.log("I wait for one second.")
}, 1000);

這樣,在函數(shù)內(nèi)部需要引用自身的時(shí)候就可以通過函數(shù)名來引用,當(dāng)然要注意,這個(gè)函數(shù)名只能在這個(gè)函數(shù)內(nèi)部使用,在函數(shù)外使用時(shí)未定義的。

1.3 立即執(zhí)行函數(shù)表達(dá)式(IIFE)

IIFE 全寫是 Immediately Invoked Function Expression,立即執(zhí)行函數(shù)。

var a = 2;

(function foo () {
   var a = 3;
   console.log(a);      // 3
})();

console.log(a);      // 2

由于函數(shù)被包含在一對(duì) ( ) 括號(hào)內(nèi)部,因此成為了一個(gè)函數(shù)表達(dá)式,通過在末尾加上另一對(duì) ( ) 括號(hào)可以立即執(zhí)行這個(gè)函數(shù),比如 (function () {})() 。第一個(gè) ( ) 將函數(shù)變成函數(shù)表達(dá)式,第二個(gè) ( ) 執(zhí)行了這個(gè)函數(shù)。

也有另外一種立即執(zhí)行函數(shù)的寫法,(function () {}()) 也可以立即執(zhí)行這個(gè)函數(shù)。

var a = 2;

(function foo () {
   var a = 3;
   console.log(a);      // 3
}());

console.log(a);      // 2

這兩種寫法功能是完全一樣的,具體看大家使用。

IIFE 的另一種普遍的進(jìn)階用法是把它們當(dāng)做函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去。

var a = 2;

(function (global) {
   var a = 3;
   console.log(a);      // 3
   console.log(global.a)      // 2
})(window);

console.log(a);      // 2

我們將 window 對(duì)象的引用傳遞進(jìn)去,但將參數(shù)命名為 global,因此在代碼風(fēng)格上對(duì)全局對(duì)象的引用變得比引用一個(gè)沒有“全局”字樣的變量更加清晰。當(dāng)然可以從外部作用域傳遞你需要的任何東西,并將變量命名為任何你覺得合適的文字。這對(duì)于改進(jìn)代碼風(fēng)格是非常有幫助的。

這個(gè)模式的另外一個(gè)應(yīng)用場景是解決 undefined 標(biāo)識(shí)符的默認(rèn)值被錯(cuò)誤覆蓋的異常(這并不常見)。將一個(gè)參數(shù)命名為 undefined ,但是并不傳入任何值,這樣就可以保證在代碼塊中 undefined 的標(biāo)識(shí)符的值就是 undefined 。

undefined = true;

(function IIFE (undefined) {
   var a;
   if (a === undefined) {
      console.log("Undefined is safe here.")
   }
}()); 
2. 塊作用域

ES5 及以前 JavaScript 中具有塊作用域的只有 with 和 try...catch 語句,在 ES6 及以后的版本添加了具有塊作用域的變量標(biāo)識(shí)符 let 和 const 。

2.1 with
var obj = {
   a: 2,
   b: 3
};

with (obj) {
   console.log(a);      // 2
   console.log(b);      // 3
}

console.log(a);      // 報(bào)錯(cuò),a is not defined
console.log(b);      // 報(bào)錯(cuò),a is not defined

用 with 從對(duì)象中創(chuàng)建出的作用域僅在 with 聲明中而非外部作用域中有效。

2.2 try...catch
try {
  undefined();      // 非法操作
} catch (err) {
  console.log(err);      // 正常執(zhí)行
}

console.log(err);      // 報(bào)錯(cuò),err is not defined

try/catch 中的 catch 分句會(huì)創(chuàng)建一個(gè)塊作用域,其中的變量聲明僅在 catch 內(nèi)部有效。

2.3 let

let 關(guān)鍵字可以將變量綁定到任意作用域中(通常是 {...} 內(nèi)部)。換句話說,let 為其聲明的變量隱式的劫持了所在的塊作用域。

var foo = true;

if (foo) {
   let a = 2;
   var b = 2;
   console.log(a);      // 2
   console.log(b);      // 2
}

console.log(b);      // 2
console.log(a);      // 報(bào)錯(cuò),a is not defined

用 let 將變量附加在一個(gè)已經(jīng)存在的塊作用域上的行為是隱式的。在開發(fā)和修改代碼的過程中,如果沒有密切關(guān)注哪些代碼塊作用域中有綁定的變量,并且習(xí)慣性的移動(dòng)這些塊或者將其包含到其他塊中,就會(huì)導(dǎo)致代碼混亂。

為塊作用域顯示的創(chuàng)建塊可以部分解決這個(gè)問題,使變量的附屬關(guān)系變得更加清晰。

var foo = true;

if (foo) {
   {
      let a = 2;
      console.log(a);      // 2
   }
}

在代碼的任意位置都可以使用 {...} 括號(hào)來為 let 創(chuàng)建一個(gè)用于綁定的塊。

還有一點(diǎn)要注意的是,在使用 var 進(jìn)行變量聲明的時(shí)候會(huì)存在變量提升,提升是指聲明會(huì)被視為存在于其所出現(xiàn)的作用域的整個(gè)范圍內(nèi)。但是使用 let 進(jìn)行的聲明不會(huì)存在作用域提升,聲明的變量在被運(yùn)行之前,并不存在。

console.log(a);      // undefined
console.log(b);      // 報(bào)錯(cuò), b is not defined

// 在瀏覽器中運(yùn)行這段代碼時(shí),因?yàn)榍懊鎴?bào)錯(cuò)了,所以不會(huì)看到接下來打印的結(jié)果,但是理論上就是這樣的結(jié)果
var a = 2;
console.log(a);      // 2 

let b = 4;
console.log(b);      // 4

2.3.1 垃圾收集
另一個(gè)塊作用域非常有用的原因和閉包及垃圾內(nèi)存的回收機(jī)制有關(guān)。
舉個(gè)例子:

function processData (data) {
   // do something
}

var bigData = {...};

processData(bigData);

var btn = document.getElementById("my_button");

btn.addEventListener("click", function () {
   console.log("button clicked");
}, false);

這個(gè)按鈕點(diǎn)擊事件的回調(diào)函數(shù)中并不需要 bigData 這個(gè)非常占內(nèi)存的數(shù)據(jù),理論上來說,當(dāng) processData 函數(shù)處理完之后,這個(gè)占有大量空間的數(shù)據(jù)結(jié)構(gòu)就可以被垃圾回收了。但是,由于這個(gè)事件回調(diào)函數(shù)形成了一個(gè)覆蓋當(dāng)前作用域的閉包,JavaScript 引擎極有可能依然保存著這個(gè)數(shù)據(jù)結(jié)構(gòu)(取決于具體實(shí)現(xiàn))。

使用塊作用域可以解決這個(gè)問題,可以讓引擎清楚的知道沒有必要繼續(xù)保存這個(gè) bigData 。

function processData (data) {
   // do something
}

{
   let bigData = {...};

   processData(bigData);
}

var btn = document.getElementById("my_button");

btn.addEventListener("click", function () {
   console.log("button clicked");
}, false);

2.3.2 let 循環(huán)
一個(gè) let 可以發(fā)揮優(yōu)勢的典型例子就是 for 循環(huán)。

var lists = document.getElementsByTagName("li");

for (let i = 0, length = lists.length; i < length; i++) {
   console.log(i);
   lists[i].onclick = function () {
     console.log(i);      // 點(diǎn)擊每個(gè) li 元素的時(shí)候,都是相對(duì)應(yīng)的 i 值,而不像用 var 聲明 i 的時(shí)候,因?yàn)闆]有塊作用域,所以在回調(diào)函數(shù)通過閉包查找 i 的時(shí)候找到的都是最后的 i 值
   };
};

console.log(i);      // 報(bào)錯(cuò),i is not defined

for 循環(huán)頭部的 let 不僅將 i 綁定到 fir 循環(huán)的塊中,事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中,確保上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值。

當(dāng)然,我們在 for 循環(huán)中使用 var 時(shí)也可以通過立即執(zhí)行函數(shù)形成一個(gè)新的閉包來解決這個(gè)問題。

var lists = document.getElementsByTagName("li");

for (var i = 0, length = lists.length; i < length; i++) {
   lists[i].onclick = (function (j) {
        return function () {
           console.log(j);
        }
   }(i));
}

或者

var lists = document.getElementsByTagName("li");

for (var i = 0, length = lists.length; i < length; i++) {
   (function (i) {
      lists[i].onclick = function () {
         console.log(i);
      }
   }(i));
}

其實(shí)原理無非就是,為每個(gè)迭代創(chuàng)建新的閉包,立即執(zhí)行函數(shù)執(zhí)行完后本來應(yīng)該銷毀變量,釋放內(nèi)存,但是因?yàn)檫@里有回調(diào)函數(shù)的存在,所以形成了閉包,然后通過形參進(jìn)行同名變量覆蓋,所以找到的 i 值就是每個(gè)迭代新閉包中的形參 i 。

2.4 const

除了 let 以外,ES6 還引入了 const ,同樣可以用來創(chuàng)建作用域變量,但其值是固定的(常亮)。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤。

var foo = true;

if (foo) {
   var a = 2;
   const b = 3;      // 包含在 if 中的塊作用域常亮

   a = 3;      // 正常
   b = 4;      // 報(bào)錯(cuò),TypeError: Assignment to constant variable
}

console.log(a);      // 3
console.log(b);      // 報(bào)錯(cuò), b is not defined

和 let 一樣,const 聲明的變量也不存在“變量提升”。

3. 總結(jié)

函數(shù)是 JavaScript 中最常見的作用域單元。塊作用域指的是變量和函數(shù)不僅可以屬于所處的函數(shù)作用域,也可以屬于某個(gè)代碼塊。

本質(zhì)上,聲明在一個(gè)函數(shù)內(nèi)部的變量或函數(shù)會(huì)在所處的作用域中“隱藏”起來,這是有意為之的良好軟件的設(shè)計(jì)原則。

有些人認(rèn)為塊作用域不應(yīng)該完全作為函數(shù)作用域的替代方案。兩種功能應(yīng)該同時(shí)存在,開發(fā)者可以并且也應(yīng)該根據(jù)需要選擇使用哪種作用域,創(chuàng)造可讀、可維護(hù)的優(yōu)良代碼。

歡迎關(guān)注我的公眾號(hào)

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/81663.html

相關(guān)文章

  • [JS]《你不知道的Javascript·上》——函數(shù)作用和塊作用

    摘要:提供了同時(shí)解決這兩個(gè)問題的方案以上這種模式稱為立即執(zhí)行函數(shù)表達(dá)式。塊作用域不應(yīng)該完全作為函數(shù)作用域的替代方案,兩種功能應(yīng)該同時(shí)存在。 函數(shù)作用域 為了隱藏內(nèi)部實(shí)現(xiàn),可以通過在任意代碼片段外部添加包裝函數(shù),但是這并不理想,因?yàn)楸仨毬暶饕粋€(gè)具名函數(shù),意味著這個(gè)函數(shù)名稱本身會(huì)污染函數(shù)所在的作用域;同時(shí)必須通過顯式地調(diào)用函數(shù)才能運(yùn)行其中的代碼。 *:區(qū)分函數(shù)聲明和表達(dá)式最簡單的方法是看func...

    changfeng1050 評(píng)論0 收藏0
  • JavaScript的作用和塊級(jí)作用概念理解

    摘要:說到這里我們需要理解兩個(gè)概念塊級(jí)作用域與函數(shù)作用域。大多數(shù)類語言都擁有塊級(jí)作用域,卻沒有。塊級(jí)作用域任何一對(duì)花括號(hào)中的語句集都屬于一個(gè)塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級(jí)作用域。 作用域 作用域永遠(yuǎn)都是任何一門編程語言中的重中之重,因?yàn)樗刂浦兞颗c參數(shù)的可見性與生命周期。說到這里我們需要理解兩個(gè)概念:塊級(jí)作用域與函數(shù)作用域。 函數(shù)作用域 這個(gè)應(yīng)該好理解...

    banana_pi 評(píng)論0 收藏0
  • JavaScript的作用和塊級(jí)作用概念理解

    摘要:說到這里我們需要理解兩個(gè)概念塊級(jí)作用域與函數(shù)作用域。大多數(shù)類語言都擁有塊級(jí)作用域,卻沒有。塊級(jí)作用域任何一對(duì)花括號(hào)中的語句集都屬于一個(gè)塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級(jí)作用域。 作用域 作用域永遠(yuǎn)都是任何一門編程語言中的重中之重,因?yàn)樗刂浦兞颗c參數(shù)的可見性與生命周期。說到這里我們需要理解兩個(gè)概念:塊級(jí)作用域與函數(shù)作用域。 函數(shù)作用域 這個(gè)應(yīng)該好理解...

    劉永祥 評(píng)論0 收藏0
  • JavaScript的作用和塊級(jí)作用概念理解

    摘要:說到這里我們需要理解兩個(gè)概念塊級(jí)作用域與函數(shù)作用域。大多數(shù)類語言都擁有塊級(jí)作用域,卻沒有。塊級(jí)作用域任何一對(duì)花括號(hào)中的語句集都屬于一個(gè)塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級(jí)作用域。 作用域 作用域永遠(yuǎn)都是任何一門編程語言中的重中之重,因?yàn)樗刂浦兞颗c參數(shù)的可見性與生命周期。說到這里我們需要理解兩個(gè)概念:塊級(jí)作用域與函數(shù)作用域。 函數(shù)作用域 這個(gè)應(yīng)該好理解...

    mochixuan 評(píng)論0 收藏0
  • 重讀你不知道的JS (上) 第一節(jié)三章

    摘要:如果是聲明中的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。給函數(shù)表達(dá)式指定一個(gè)函數(shù)名可以有效的解決以上問題。始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐。也有開發(fā)者干脆關(guān)閉了靜態(tài)檢查工具對(duì)重復(fù)變量名的檢查。 你不知道的JS(上卷)筆記 你不知道的 JavaScript JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多復(fù)雜微妙技術(shù)的語言,即使是經(jīng)驗(yàn)豐富的 Ja...

    lavor 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<