iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
0
Modern Web

先你一步的菜鳥 - 從 0 開始的前端網頁設計系列 第 11

Day-11 作用域和變數宣告

  • 分享至 

  • xImage
  •  

在昨天我們稍微提到了 react 裡宣告變數的方式,也就是 const、let、var。

不過在了解宣告變數之前,先了解作用域(Scope)會讓你上手更快。

作用域(Scope):一個變數能夠作用的範圍。

基本上 Scope 可以分為 global 和 local 兩種,依照定義的位置決定你的作用域,跟投胎有點像啦,讓我們先來看看各自的定義。

全域(Global Scope)

定義在全域的變數就代表在任何地方都能夠自由存取,舉例來說:

  var fruit ='apple';

  console.log(fruit)    //apple

  function fruitName(){
  console.log(fruit)    
  }

  fruitName()           //apple

區域(Local Scope)

區域有兩種情況 Function Scope 和 Block Scope

Function Scope

定義在函式內就是 Function Scope,舉例來說:

  function fruitName() {
    var fruit = "apple";
    console.log(fruit);   
  }

  fruitName()             //apple
  console.log(fruit);     //not defined

通常定義在 Function Scope 的變數會用 var 來宣告,這是為了避免造成全域命名汙染。

註:全域命名汙染的大致意思就像是你創腳色的時候,你好不容易想到一個超級猛的名字,像是: "xx金城武",但卻被提示說名字已被使用那種感覺,但在 JS 裡並不會提示你這個,如果你的 local 變數已經被 global 變數定義過,他們會共用同一個儲存空間,後果會怎樣,想想都覺得可怕。

Block Scope

定義在 try catch、if esls、while 等邏輯迴圈內就是 Block Scope,其實只要是{}就是一個區塊,舉例來說:

  function fruitName() {
    if(true){
      let fruit1 ="banana"
      let fruit2 ="orange"
      console.log(fruit);   //undefined
      console.log(fruit1);  //banana
      console.log(fruit2);  //orange
    }
    var fruit = "apple";
    console.log(fruit);   //apple
    console.log(fruit1);  //error : not defined
    console.log(fruit2);  //error : not defined
  }

    fruitName()

Block Scope 內定義的變數只能作用在那個區塊內,出了那個區塊就會不存在。

可是其實看完以上這些式例可能會引出幾個疑問:
1.為什麼定義在 function scope 內的都用 var ?用 let 不好嗎?const 呢?怎麼沒出現?
2.為什麼在 block scope 內不用 var 定義,是有甚麼原因嗎?
3.let 和 var 差在哪裡?
4.為什麼在 block scope 內可以接到 fruit ,他不是還沒被定義嗎?
5.為什麼之前用 const 宣告 arrow function 而不是用 function 宣告,這有差別嗎?

這邊給個提示,可以查查看 hosting 和 lexical scope 還有想一下 function scope、 block scope的差別。

為了解決上面這些疑惑,就必須來好好講解一下 var、let、const 有甚麼差別。

變數宣告

其實在 ES6 之前,所有的變數宣告都是靠 var 去獨攬大權的,並且只有 function 可以做為 scope,區塊(block)是沒有的,這就引起了很多問題,像是造成全域命名汙染循環變數洩漏或是區域變數互相影響,直到 ES6 出現,讓 block 也可以作為 scope,並新增了 let/const 兩種宣告方式,讓我們可以用更嚴謹的方式去保護變數,現在我們就來看看到底有甚麼差別吧。

const

const 就是常數(constant)的意思,一開始就需要給值,且不可被重複定值,舉個例子來說:

  function fruitName() {
    const fruit;            //Parsing error: Unexpected token
    const fruit1='apple'    //
    const fruit1='banana'   //Parsing error: Identifier 'fruit1' has already been declared
    console.log(fruit);       //error
    console.log(fruit1);     //apple
  }
  console.log(fruit1)        //error:not defined

  fruitName()

雖然說這樣跑一定會出錯 XD,不過我就讓大家看一下會出現怎樣的結果。

上面可以看到 const 有相對來說最嚴格的命名條件,同時也是保護性最高的。

註:就保護性來說 const > let> var。

其實 const 也可以用來定義 陣列和 arrow function,就像我們前一篇的 function 一樣。

  const setfruit = () =>{}
  const fruit =[]

雖然說不能直接改變 const 宣告的變數,但是可以改變用 const 宣告變數裡面的值,意思就像是這樣:

  const fruit =[]
  console.log(fruit)  //[]
  fruit[0]=1
  console.log(fruit)  //[1]

這樣是可以滴,同理,用在 arrow function 也是一樣,這樣更加確保了 code 的穩固性,也可以在 hook 裡看到 使用 const 來宣告 useState。

let

let 跟 const 一樣,只會存在定義的 block 裡面,可以避免函數被不當存取或影響,我們可以看下面的例子:

  function fruitName() {
    const fruit='apple'
    console.log(fruit);
  }
  fruitName()            //apple
  console.log(fruit);    //error:not defined

因為這個侷限的特性,let 常被用來定義在 for loop、if else、while 等邏輯迴圈內,舉個例子來說:

  const fruits = ["apple", "orange", "banana"];

  function fruitName(fruit) {
    for (let i = 0; i < fruit.length; i++) {
      setTimeout(function(){
        console.log(fruit[i])},1000);
    }
  }

  fruitName(fruits); // apple orange banana

我們希望使用 setTimeout 去調用作為 callback function 的匿名涵式(Anonymous )來回傳每個水果,這時就可以用 let 來定義變數 i,那為什麼不用 var 去定義呢?

註:匿名涵式(Anonymous ),也就是不具名的函式,至於為什要用呢,方便啦!因為不用想名稱,通常用在重複執行的邏輯內,但這也會造成維護困難,畢竟每個東西都用匿名的方式,就不知道 error 從哪邊跑出來的了,我們將會在未來做更詳細的解說。

  const fruits = ["apple", "orange", "banana"];

  function fruitName(fruit) {
    for (var i = 0; i < fruit.length; i++) {
      setTimeout(function(){
        console.log(fruit[i-1])},1000);
    }
  }

  fruitName(fruits); // banana banana banana

他就只會輸出 i=3 時的那個值。

這是因為 var 的特性可以一直被重複定義,每次迴圈賦值時都是指定到同一個變數 i,代表他的值會一直不斷地被覆蓋,所以永遠都只有一個變數 i,加上我們使用 setTimeout 讓變數 i 沒辦法及時輸出,當 for loop 跑完時,i 已經變成 3 了,也就造成了每次出來的結果都會是同一個,這就很像我們前面講的全域命名汙染

那為什麼使用 let 就會正確呢,這是因為 let 定義的變數,只能在那個區塊內被存取以及改變,所以每次 for loop 時的流程就會像這樣:

{
	 i=0
      setTimeout(function(){
        console.log(fruit[0])},1000);
    }
},

{
	 i=1
      setTimeout(function(){
        console.log(fruit[i])},1000);
    }
}

{
	 i=2
      setTimeout(function(){
        console.log(fruit[2])},1000);
    }
}

因為 let 所定義的 i 在各自的區塊{}內,而每一層迴圈都是各自的區塊{},let 所定義的變數只能在區塊內被存取、改變,所以各個變數 i 彼此不會互相影響,所以會有多個變數 i,因此會跑出不同的結果。

至於為什麼要搞得這麼複雜呢,不能直接 console.log 嗎,這樣你們才會看出區別阿。

主要是因為 js 是非同步的,有時候你的資料沒有這麼即時傳回來,這時執行的話就會出錯,當然也有很多種方式可以改善,像是立即執行函式(IIFEs)、閉包(closure)等等的方式,但這樣的話可讀性就不高了,只是改個宣告方式就可以改善,不用嗎。

註:立即執行函式(IIFEs)、閉包(closure)等等的就不多做贅述了,篇幅已經太長了嗚嗚,我們會在未來的幾篇內做詳細解釋。

var

就如同上面講的一樣,var 是保護性最弱的宣告方式,但是他有 let 和 const 都沒有的特性提升(hosting),舉例來說:

  console.log(a)   // undefined
  var a='123'

也就是他可以在還沒被宣告之前就先呼叫它,在 js 的理解裡它是長這樣:

  var a
  console.log(a)   // undefined
  a='123'

這邊需要注意一點,hosting 做的只有提前幫你定義你的變數,給值並不會提前。

而let 或 const 這樣做的話就會出現 ReferenceError

  console.log(a,b)  //ReferenceError 
  let a 
  const b='b'

其實這蠻合理的,因為 const 和 let 的命名規則相對 var 來說嚴格很多,這也可以防止前面一直講的全域變數汙染,所以當你使用一個還沒被宣告的變數時,請記得一定要將他用 var 宣告,不然就慘啦。

今天這篇蠻長的,如果有人看到最後,你很棒。

我是 Chris,在看完今天這篇之後你可以翻上去看一下上面提到的 5 個問題,因該有不少都有解答了,明天我們將會詳細的介紹這些問題,順便帶到一些其他的觀念,我們明天見!


上一篇
Day-10 救世主-Hook
下一篇
Day-12 hosting & Lexical Scope & function expression/declaration 雜談總集
系列文
先你一步的菜鳥 - 從 0 開始的前端網頁設計31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言