JS的作用域

對于任何編程語言來說个唧,都有一個很基礎(chǔ)但也很重要的概念:變量的管理江解;它包括變量的聲明,變量的賦值徙歼,變量的存儲犁河,變量的查找鳖枕,變量的更改,變量的銷毀等桨螺。而從另外一個角度來看這一系列問題就可以理解為:這個變量存在哪兒宾符?存活多久?怎樣才能找到它灭翔?在JS中魏烫,解決這些問題的基礎(chǔ)就是作用域,同時了解作用域也是學(xué)習(xí)閉包的基礎(chǔ)

1. 需要理解的概念

在闡述作用域的概念之前肝箱,首先需要了解的是哄褒,在面對一段程序的時候,JS內(nèi)部是如何進(jìn)行處理的煌张,有一個流傳很廣的說法是JS是解釋型語言呐赡,而非編譯型語言,其實JS程序的執(zhí)行也是需要編譯的骏融,只是其不是預(yù)編譯的链嘀,而是在程序段執(zhí)行之前進(jìn)行的臨時編譯,其編譯過程分為下面幾步:

  1. 分詞/語法分析绎谦,如var a = 2;就會被分為var,a,=,2等標(biāo)記
  2. 解析管闷,將上一步得出的所有標(biāo)記轉(zhuǎn)換為一個元素樹,其實可以看做是該段程序的語法結(jié)構(gòu)窃肠;這個元素樹統(tǒng)稱為"AST"(abstract syntax tree)
  3. 生成可執(zhí)行碼,即將上一步代碼塊對應(yīng)的AST轉(zhuǎn)換為機(jī)器可執(zhí)行的指令

上面三部過程需要涉及到三個重要的角色:

  1. 引擎刷允,負(fù)責(zé)JS代碼的編譯與執(zhí)行
  2. 編譯器冤留,引擎的好朋友;主要為引擎做一些準(zhǔn)備工作树灶,如解析纤怒,生成可執(zhí)行碼
  3. 作用域,引擎的另一個好朋友天通;主要負(fù)責(zé)管理程序?qū)?yīng)的元素(變量泊窘,方法等),同時定義一套規(guī)則像寒,該規(guī)則約束當(dāng)前程序可以訪問哪些元素

當(dāng)面對代碼段var a = 2;的時候烘豹,編譯器會執(zhí)行下列步驟:

  1. 編譯器詢問作用域是否已經(jīng)存在一個叫a的變量,若存在诺祸,則進(jìn)入下一步携悯,若不存在,則通知作用域創(chuàng)建一個叫a的變量
  2. 編譯器為引擎生成可執(zhí)行碼筷笨,然后引擎詢問當(dāng)前作用域是否在可以訪問的a變量憔鬼,若存在龟劲,則用之,否則轴或,引擎將前往別處尋找(嵌套作用域)
  3. 如果引擎最終找到了變量a昌跌,則將2賦值給變量a,否則引擎將報錯

2. 作用域中變量的聲明與賦值

2.1 Hoisting

JS在面對一個變量的聲明與賦值的時候照雁,會首先在編譯期對變量聲明進(jìn)行處理蚕愤,然后在執(zhí)行期對變量進(jìn)行賦值;而在編譯器進(jìn)行代碼編譯的時候囊榜,會將變量或方法的聲明由其代碼申明處提至語義作用域(語義作用域?qū)⒃诤罄m(xù)章節(jié)中做詳細(xì)解釋)的頂部审胸,這個過程就稱為Hoisting或變量提升,Hoisting也是作用域中變量聲明與賦值的核心和難點卸勺,首先看下面兩段代碼:

  a = 2;

  var a;

  console.log( a );
  console.log( a );

  var a = 2;

經(jīng)過編譯器編譯后砂沛,上面第一段代碼會被轉(zhuǎn)換為:

  var a;

  a = 2;

  console.log( a );//2

而第二段代碼將被轉(zhuǎn)換為:

  var a;

  console.log( a );

  a = 2;//undefined

可以發(fā)現(xiàn),由于Hoisting的存在曙求,在一個語義作用域內(nèi)碍庵,只要存在變量聲明,無論該聲明語句處于什么位置悟狱,都會在執(zhí)行前被提至語義作用域的頂部静浴,需要注意的是只有聲明會被Hoisting,而賦值不會做任何處理挤渐,維持原順序苹享;同時Hoisting只會在當(dāng)前語義作用域中起效

方法的聲明也一樣會在編譯期執(zhí)行Hoisting,如:

  foo();

  function foo() {
      console.log( a ); // undefined

      var a = 2;
  }

將會在編譯期是轉(zhuǎn)換為:

  function foo() {
    var a;

    console.log( a ); // undefined

    a = 2;
  }

  foo();

又如:

  foo();

  var foo = function bar() {
    // ...
  };

將會在編譯期是轉(zhuǎn)換為:

  var foo;
  foo();

  foo = function bar() {
    // ...
  };

又又如:

  foo();
  bar();

  var foo = function bar() {
    // ...
  };

將會在編譯期是轉(zhuǎn)換為:

  var foo;

  foo(); // TypeError
  bar(); // ReferenceError

  foo = function() {
      var bar = ...self...
      // ...
  }

初學(xué)JS的人經(jīng)常會很奇怪為什么JS代碼中浴麻,經(jīng)常會出現(xiàn)對某個變量或方法的使用出現(xiàn)在其聲明的前面得问,而JS引擎照樣可以正常的執(zhí)行,不會報錯软免,這些要?dú)w功于Hoisting

2.2 變量Hoisting與方法Hoisting的優(yōu)先級

如果在語義作用域中同時存在變量Hoisting和方法Hoisting宫纬,JS也規(guī)定了它們的優(yōu)先級:
方法Hoisting優(yōu)先級 > 變量Hoisting優(yōu)先級

如:

  foo();

  var foo;

  function foo() {
      console.log( 1 );
  }

  foo = function() {
      console.log( 2 );
  };

將會在編譯期是轉(zhuǎn)換為:

  function foo() {
    console.log( 1 );
  }

  foo(); // 1

  foo = function() {
    console.log( 2 );
  };

在這段代碼中,有兩個同名的聲明膏萧,變量foo與方法foo漓骚,首先,方法foo將會被Hoisting榛泛,同時后續(xù)的變量foo的聲明將會被忽略(因為JS引擎已經(jīng)找到了變量foo蝌蹂,那么它就不會重新去聲明一個同名變量)

在代碼塊里定義的方法也將被Hoisting

  foo(); // "b"

  var a = true;
  if (a) {
   function foo() { console.log( "a" ); }
  }
  else {
   function foo() { console.log( "b" ); }
  }

將會在編譯期是轉(zhuǎn)換為:

  function foo() { console.log( "a" ); }
  function foo() { console.log( "b" ); }
  foo(); // "b"

  var a = true;
  if (a) {

  }
  else {

  }

3. 作用域中變量的找尋機(jī)制

JS引擎在編譯包含有變量a的代碼時,會在作用域中找尋變量a挟鸠,總體來說有兩種找尋方式叉信,分別為:

  • LHS:Left-hand Side
  • RHS:Right-hand Side

這里的side指的是assignment operation,即通過賦值操作區(qū)分是LHS還是RHS艘希,如果變量在賦值操作的左邊硼身,則是LHS硅急;而RHS卻不能簡單定義為變量在assignment operation的右邊,應(yīng)該理解為非LHS的即為RHS佳遂,從變量找尋與賦值的角度來說营袜,LHS指的是找尋變量本身,而RHS指的是獲取變量的值丑罪,如:

  • var a = 1;荚板,變量在賦值操作符"="的左邊,所以屬于LHS
  • console.log( a );吩屹,變量不在賦值操作符的左邊跪另,而是直接獲取變量的值,所以屬于RHS

這里之所以要介紹著兩種變量找尋方式煤搜,是因為這兩種變量找尋方式在作用域中會有不同的表現(xiàn)免绿,如:在RHS模式下,如果找到了對應(yīng)變量擦盾,則返回該變量嘲驾,反之未找到對應(yīng)變量,會彈出ReferenceError迹卢;而在LHS模式下辽故,如果未找到對應(yīng)變量,則根據(jù)不同情況作出不同反應(yīng)腐碱,如果是“Strict Mode”下誊垢,則會彈出ReferenceError,而非“Strict Mode”則在當(dāng)前作用域下自動創(chuàng)建該變量

4. JS中的作用域(語義作用域)

說了這么多症见,如何識別JS中的作用域呢彤枢?首先從大的層面了解一下作用域的分類,一般來說作用域可分為兩種:

  • 語義作用域筒饰,即在"分詞/語法分析"定義的作用域,或者說在代碼編寫階段就已經(jīng)決定了作用域的結(jié)構(gòu)范圍壁晒,JS使用的就是語義作用域
  • 動態(tài)作用域瓷们,Bash腳本,Perl中依然使用的是動態(tài)作用域秒咐,本文不予討論

而在JS中谬晕,根據(jù)代碼形式,作用域也可以分為兩種:

  • 函數(shù)作用域携取,顧名思義攒钳,函數(shù)作用域就是通過定義一個JS的function而生成的作用域,在JS中所謂的語義作用域指的就是函數(shù)作用域雷滋,這一點一定要記清楚
  • 塊級作用域不撑,而塊級作用域則是通過定義一個JS的代碼塊生成的作用域文兢,塊級作用域的典型示例:
    • {},即單獨(dú)的代碼塊
    • for(;;) {}焕檬,即for循環(huán)代碼塊
    • if() {}姆坚,即if判斷代碼塊

塊級作用域其實只是形式上的作用域,它并是嚴(yán)格意義上的語義作用域实愚,所以會出現(xiàn)代碼塊里的變量聲明直接被Hoisting其外部語義作用域(函數(shù)作用域)頂部的情況

那么除開寫法上的不同兼呵,函數(shù)作用域和塊級作用域主要有什么區(qū)別呢?其實它們最重要的區(qū)別在于函數(shù)作用域可以進(jìn)行有效的變量隔離腊敲,即在函數(shù)作用域里定義的變量不會影響其嵌套作用域击喂,這在模塊化開發(fā)里尤其有用,它可以保證在A模塊定義的變量不會影響與B模塊的同名變量碰辅,更不會污染global作用域懂昂,典型的函數(shù)作用域示例:

  • 方法定義與調(diào)用
  function foo(a) {

      var b = a * 2;

      function bar(c) {
          console.log( a, b, c );
      }

      bar(b * 3);
  }

  foo( 2 ); // 2 4 12
  • IIFE(Invoking Function Expressions Immediately)
  var a = 2;

  (function foo(){

      var a = 3;
      console.log( a ); // 3

  })();

  console.log( a ); // 2

需要注意的是IIFE的方法不能在外部語義scope里再次調(diào)用,如:

  (function foo() {
      a = 2;
      console.log("a is " + a);
  })();

  foo();//ReferenceError

看下列示例乎赴,并思考這段代碼中包含有幾個函數(shù)(語義)作用域:

  function foo(a) {

      var b = a * 2;

      function bar(c) {
          console.log( a, b, c );
      }

      bar(b * 3);
  }

  foo( 2 ); // 2 4 12

這段代碼有三個函數(shù)作用域:

scope.png
  • 作用域1:全局作用域忍法,只定義了一個變量foo
  • 作用域2:foo方法內(nèi)的作用域,定義了三個變量榕吼,b,abar
  • 作用域3:方法bar內(nèi)的作用域饿序,定義了一個變量c

其中,作用域1是作用域2的嵌套作用域羹蚣,而2又是3的嵌套作用域原探,如在作用域3中需要使用變量a的值,但是此時在自己的作用域中并未找到變量a顽素,那么就會到其上一級嵌套作用域咽弦,也就是作用域2中找尋變量a,以此類推胁出;同時語義作用域只與方法的定義位置有關(guān)型型,與其調(diào)用位置毫無關(guān)系(所以也叫[語義]作用域) ;另外全蝶,在根據(jù)語義作用域進(jìn)行變量找尋的時候闹蒜,只適用于單獨(dú)變量的情況,如a,b等抑淫,而對于通過對象屬性找尋變量的情況绷落,如foo.bar.baz就不是根據(jù)語義作用域進(jìn)行變量的找尋,而是通過對象屬性訪問規(guī)則找尋其對應(yīng)變量

上面已經(jīng)說過始苇,塊級作用域其實只是相當(dāng)于形式上的作用域砌烁,沒有任何變量隔離效果,如下面代碼:

  function foo() {
      function bar(a) {
          i = 3; // 就是for循環(huán)中創(chuàng)建的變量i
          console.log( a + i );
      }

      for (var i=0; i<10; i++) {// i屬于foo方法所創(chuàng)造的作用域
          bar( i * 2 ); // 死循環(huán)
      }
  }

  foo();

即在塊級作用域中定義的變量實際上還是屬于其對應(yīng)的語義作用域內(nèi)催式,或者說離它最近的函數(shù)作用域函喉,這一點很容易造成錯誤

  function foo() {
      var i = 1;

      for (var i=0; i<10; i++) {// 由于在foo方法創(chuàng)造的作用域中避归,變量i已經(jīng)存在,所以此時for循環(huán)中的i其實就是
                                          //上面的"var i = 1;"創(chuàng)建的i
          //do something
      }

      console.log("now i is " + i);//10
  }

  foo();

到了ES6函似,可以通過letconst實現(xiàn)塊級作用域的變量隔離槐脏,即通過let在塊級作用域中聲明變量,該變量將只會存在于該塊級作用域中

  function foo() {
      var i = 1;

      for (let i=0; i<10; i++) {
          //do something
      }

      console.log("now i is " + i);//1
  }

  foo();

雖然說塊級作用域并沒有變量隔離的效果撇寞,但是使用得當(dāng)顿天,塊級作用域也能發(fā)揮意想不到的用處,如:加快垃圾清理蔑担,來看下面代碼

  function process(data) {
      // do process
  }

  var someReallyBigData = { .. };

  process( someReallyBigData );

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

  btn.addEventListener( "click", function click(evt){// 閉包的存在
      console.log("button clicked");
  }, /*capturingPhase=*/false );

可以發(fā)現(xiàn)在click事件對應(yīng)的方法中牌废,someReallyBigData完全無用,可以將其回收掉啤握,以減輕內(nèi)存負(fù)擔(dān)鸟缕,但由于有閉包的存在,JS并不會馬上對其進(jìn)行回收排抬,那么此時可以采用下列寫法

  function process(data) {
      // do process
  }

  // block scope定義的任何數(shù)據(jù)都可以在scope結(jié)束后清理掉
  {
      let someReallyBigData = { .. };

      process( someReallyBigData );
  }

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

  btn.addEventListener( "click", function click(evt){
      console.log("button clicked");
  }, /*capturingPhase=*/false );

這段代碼里懂从,將大量臨時數(shù)據(jù)的處理放置于外部語義作用域的塊級作用域中,它不會受到閉包的影響蹲蒲,在執(zhí)行完成后會被JS的垃圾回收機(jī)制及時清理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末番甩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子届搁,更是在濱河造成了極大的恐慌缘薛,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卡睦,死亡現(xiàn)場離奇詭異宴胧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)表锻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門恕齐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞬逊,你說我怎么就攤上這事檐迟。” “怎么了码耐?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長溶其。 經(jīng)常有香客問我骚腥,道長,這世上最難降的妖魔是什么瓶逃? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任束铭,我火速辦了婚禮廓块,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘契沫。我一直安慰自己带猴,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布懈万。 她就那樣靜靜地躺著拴清,像睡著了一般。 火紅的嫁衣襯著肌膚如雪会通。 梳的紋絲不亂的頭發(fā)上口予,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機(jī)與錄音涕侈,去河邊找鬼沪停。 笑死,一個胖子當(dāng)著我的面吹牛裳涛,可吹牛的內(nèi)容都是我干的木张。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼端三,長吁一口氣:“原來是場噩夢啊……” “哼舷礼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起技肩,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤且轨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后虚婿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旋奢,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年然痊,在試婚紗的時候發(fā)現(xiàn)自己被綠了至朗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡剧浸,死狀恐怖锹引,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情唆香,我是刑警寧澤嫌变,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站躬它,受9級特大地震影響腾啥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一倘待、第九天 我趴在偏房一處隱蔽的房頂上張望疮跑。 院中可真熱鬧,春花似錦凸舵、人聲如沸祖娘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渐苏。三九已至,卻和暖如春增热,著一層夾襖步出監(jiān)牢的瞬間整以,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工峻仇, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留公黑,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓摄咆,卻偏偏與公主長得像凡蚜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吭从,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 在之前的文章中我已經(jīng)介紹了執(zhí)行上下文的變量對象朝蜘。在這一篇文章我要介紹執(zhí)行上下文的作用域鏈了。 執(zhí)行上下文.作用域鏈...
    csRyan閱讀 3,845評論 1 17
  • js的詞法作用域 對于JavaScript來說涩金,無論函數(shù)在哪里被調(diào)用谱醇,也無論它如何被調(diào)用,它的詞法作用域都只有函數(shù)...
    魔法少女王遺瘋閱讀 175評論 0 0
  • You don't KnowJS 引語:你不懂的JS這本書?github上已經(jīng)有了7w的star最近也是張野大大給...
    Sleet閱讀 577評論 0 0
  • 雲(yún)中誰寄錦書來;雁字回時全度,月滿西樓煮剧。這狂熱的時代,唯剩焦躁将鸵。誰又會記得未曾泛起一絲漣漪的她勉盅;誰又怎會想起撰一書問候...
    浪子衍閱讀 208評論 0 0
  • 臨近畢業(yè)還有不到一個月了,b還是沒有找到工作顶掉。 正午的陽光像塊厚厚的棉墊披在他身上草娜,沒走出就業(yè)指導(dǎo)中心幾步汗就落下...
    蘇語閱讀 826評論 4 5