讀《你不知道的JavaScript(上卷)》后感-淺談JavaScript作用域(一)


原文

一、 序言

最近我在讀一本書:《你不知道的JavaScript》,這書分為上中卷,內容非常豐富,認真細讀,能學到非常多JavaScript的知識點,希望廣大的前端同胞們,也入手看看這本書,受益匪淺。

《你不知道的JavaScript上卷》

image

現在我讀完這本書的一些心得與總結:

很多人在做項目時候,遇到bug是我們程序猿最令人頭疼的一件事,不過,無論多大多小的bug,都會被我們debug,所以,一切的bug都有原因,只要慢慢靜下心來細想想這段代碼的流程結構是否正確,哪一步驟出了錯誤,bug就迎刃而解啦。

聊了這么多鋪墊,其實我想說的就一句話:bug從不細心得來,debug是從細心解決。

這本書的第一部分是講的是作用域與閉包,現在我談談作用域的理解,同時也聊聊理解JavaScript的作用域,是對分析JavaScript的代碼流程有多么的重要。

二、JavaScript的作用域是什么,他是如何運行工作的?

好比:

  1.這段代碼會輸出什么呢?
  var num = 10;
  console.log(num);
  
  2.或許這段呢?
  var num;
  console.log(num);
  num = 10;

我們都輕易知道上面的代碼會分別輸出:10,undefined;即使簡單,相信大家腦子已經想了一次這段代碼的執行流程;

不用着急,先理解一下作用域:

《你不知道的JavaScript》先開頭就已經有定義好的約定,我們也來一下:

1.引擎:從頭到尾負責整個 JavaScript 程序的編譯及執行過程。
2.編譯器:引擎的好朋友之一,負責語法分析及代碼生成等臟活累活
3.作用域:引擎的另一位好朋友,負責收集並維護由所有聲明的標識符(變量)組成的一系列查 詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限
 
 為了能夠完全理解 JavaScript 的工作原理,你需要開始像引擎(和它的朋友們)一樣思考, 從它們的角度提出問題,並從它們的角度回答這些問題

這段出處《你不知道的JavaScript》上卷第一部分

好,我們一起來分析一下上面的代碼:

var num = 10;

我們第一眼看到這句代碼,很可能認為這是一句聲明,但js的引擎卻認為這里應該有2個聲明,第一個是由編譯器
在編譯時處理,另一個是由引擎在運行時處理


也就是說:

var num = 10; 分為:

聲明:var num;
賦值:num = 10;

三、引用一下《你不知道的JavaScript》的引擎和作用域的對話:

LHS 和 RHS 的含義是“賦值操作的左側或右側”並不一定意味着就是“= 賦值操作符的左側或右側”。

function foo(a) { 
  console.log( a ); // 2
}
foo( 2 );
讓我們把上面這段代碼的處理過程想象成一段對話,這段對話可能是下面這樣的。

我的理解圖順序:

第一步:當開始執行js時候,js引擎用上到下開始掃描

=> 1.讀到了一個foo的函數 

    foo(){
        ...
    }
   之后繼續讀下一步(沒有查詢到foo()調用是不會繼續讀函數下去的)
   
=> 2. 讀到了foo();
   這里就要開始調用foo函數,
   所以引擎:我說作用域,我需要為 foo 進行 RHS 引用。你見過它嗎?
   
   作用域:別說,我還真見過,編譯器那小子剛剛聲明了它。它是一個函數,給你。
   引擎:哥們太夠意思了!好吧,我來執行一下 foo
   
=> 3. 當引擎執行foo函數時候發現有個a的參數,
   然后引擎當然需要為a開始查詢:
   引擎:作用域,還有個事兒。我需要為 a 進行 LHS引用,這個你見過嗎?
   作用域:這個也見過,編譯器最近把它聲名為 foo 的一個形式參數了,拿去吧。
   引擎:大恩不言謝,你總是這么棒。現在我要把 2 賦值給 a。 
   
=> 4. js引擎繼續往下面讀:
   發現一個console.log,所以
   引擎:哥們,不好意思又來打擾你。我要為 console 進行 RHS 引用,你見過它嗎?
   作用域:咱倆誰跟誰啊,再說我就是干這個。這個我也有,console 是個內置對象。 給你。
   引擎:么么噠。我得看看這里面是不是有 log(..)。太好了,找到了,是一個函數。

=> 5. 最好執行console.log()里面的a
   引擎:哥們,能幫我再找一下對 a 的 RHS 引用嗎?雖然我記得它,但想再確認一次。
   作用域:放心吧,這個變量沒有變動過,拿走,不謝。
   引擎:真棒。我來把 a 的值,也就是 2,傳遞進 log(..)。
   ...

這段對話引用《你不知道的JavaScript》的引擎和作用域的對話

到了這里,我想常常我在開發中,當遇到bug時候,我都會整理思路,想想哪一步出錯了,現在我才發現,理解js的作用域是多么重要,才知道哪一步出了問題。

補充

來,繼續看考慮一下一下代碼:

function foo(a){
    console.log(a + b);
}
var b = 2;

foo(2);  會輸出什么呢?他的執行流程是什么呢?

tips:引擎從當前的執行作用域開始查找變量,如果找不到就會向上一級繼續查找。當抵達最外層的全局作用域還是沒有找到,查找的過程都會停止。

四、函數作用域

好,聰明的我們繼續來看一段代碼:

function foo(a) { 

  var b = a * 2;
  
  function bar(c) { 
    console.log( a, b, c );
  }
  
  bar( b * 3 ); 
   
}
foo( 2 ); // 分別輸出多少?

聰明的我們肯定看了以上代碼一下子能知道答案,我們再來看一段想想分別輸出多少:

function foo(a) { 

  var b = 2;
  
  function bar(c) { 
    console.log( a, b, c );
  }
  
  var c = 3;
   
}

bar();  // 輸出多少呢?
console.log(a);  // 輸出多少呢?
console.log(b);  // 輸出多少呢?
console.log(c);  // 輸出多少呢?
好了,到了這里,我們都已經有了答案,很明顯,
第一個代碼域,都分別輸出了2,4,12
  ..
第二個代碼域,都報了ReferenceError錯誤
  很明顯,在外面想訪問函數里面的值,是訪問不到的,所以知道了函數擁有自己的作用域,外面是訪問不到的。
  

這里可以引申一個技巧:
私有變量 與 共有變量

看一下代碼如何優化:

function doSomething(a) {
  b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
function doSomethingElse(a) { 
  return a - 1;
}
var b;
doSomething( 2 ); // 15

在這個代碼片段中,變量 b 和函數 doSomethingElse(..) 應該是 doSomething(..) 內部具體 實現的“私有”內容。給予外部作用域對 b 和 doSomethingElse(..) 的“訪問權限”不僅 沒有必要,而且可能是“危險”的,因為它們可能被有意或無意地以非預期的方式使用, 從而導致超出了 doSomething(..) 的適用條件。更“合理”的設計會將這些私有的具體內 容隱藏在 doSomething(..) 內部,例如:

function doSomething(a) { 
  function doSomethingElse(a) {
    return a - 1;
  }
  var b;
  b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
doSomething( 2 ); // 15

現在,b 和 doSomethingElse(..) 都無法從外部被訪問,而只能被 doSomething(..) 所控制。 功能性和最終效果都沒有受影響,但是設計上將具體內容私有化了,設計良好的軟件都會 依此進行實現

五、塊級作用域

兄弟,以下這段代碼我們肯定寫過幾萬次了~

for (var i = 0; i< 10; i++) {
    console.log(i);
}

這個就是最常見的塊級作用域,
你可以試試在for{}外面執行一下console.log(i)試試 輸出什么?

for (var i = 0; i< 5; i++) {
console.log(i); //  0,1,2,3,4
}

console.log(i); // 5

我們在 for 循環的頭部直接定義了變量 i,通常是因為只想在 for 循環內部的上下文中使
用 i,而忽略了 i 會被綁定在外部作用域(函數或全局)中的事實。

for (let i = 0; i< 5; i++) {
  console.log(i); //  0,1,2,3,4
}

console.log(i); // ReferenceError: i is not defined

當把var 改為了 let ,那么for循環里的i只能在{}這個作用域有效,外面就是訪問不到了,所以報了ReferenceError

{
    console.log( bar );  //報了ReferenceError
    let bar = 10;
}

上面代碼未聲明的變量,不能使用,不存在變量的提升

5.2.垃圾回收

另一個塊作用域非常有用的原因和閉包及回收內存垃圾的回收機制相關。這里簡要說明一 下,而內部的實現原理

function process(data) {
  console.log(data);
}
var someReallyBigData = { 
 'name': 'bobobo', 
};
process( someReallyBigData );

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

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

click 函數的點擊回調並不需要 someReallyBigData 變量。理論上這意味着當 process(..) 執 行后,在內存中占用大量空間的數據結構就可以被垃圾回收了。

但是,由於 click 函數形成 了一個覆蓋整個作用域的閉包,JavaScript 引擎極有可能依然保存着這個結構(取決於具體 實現)。
塊作用域可以打消這種顧慮,可以讓引擎清楚地知道沒有必要繼續保存 someReallyBigData 了:

function process(data) {
  console.log(data);
}
// 在這個塊中定義的內容可以銷毀了!
{
  var someReallyBigData = { 
	'name': 'bobobo', 
  };
}
process( someReallyBigData );

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

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

六、先有雞還是先有蛋?

a = 2;
var a; 
console.log( a ); 輸出什么?

當我看見這段代碼時候,肯定想有沒有坑啊?這不是等於2么?

《你不知道的JavaScript》書里解釋到:很多開發者會認為是 undefined,因為 var a 聲明在 a = 2 之后,他們自然而然地認為變量 被重新賦值了,因此會被賦予默認值 undefined。但是,真正的輸出結果是 2。

果然是2!一起再考慮另外一段代碼:

console.log( a );  輸出多少??
var a = 2;

是2,ReferenceError 還是 undefined呢?

以上代碼結合一開始所說的:

js引擎去讀時候,會讀到了

var a;
consoel.log(a);
a = 2;

所以輸出undefined: 先有蛋(聲明)后有雞(賦值)。

七、函數優先?

函數聲明和變量聲明都會被提升。


foo(); // 輸出多少?
var foo;
function foo() { 
  console.log( 1 );
}
foo = function() { 
  console.log( 2 );
};

是函數優先還是變量優先呢?
答案是: 輸出1,函數優先

一個普通塊內部的函數聲明通常會被提升到所在作用域的頂部,這個過程不會像下面的代 碼暗示的那樣可以被條件判斷所控制:

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

但是需要注意這個行為並不可靠,在 JavaScript 未來的版本中有可能發生改變,因此應該 盡可能避免在塊內部聲明函數。

原文


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM