JavaScript 核心 - 變數與他們的產地


Posted by ai86109 on 2020-09-29

從資料型態談起

在講變數前,先來看一下在 JavaScript 中,變數的型態可能有哪幾種。

在 JavaScript 中有七種資料型態:

  1. null
  2. undefined
  3. string
  4. number
  5. boolean
  6. symbol (ES6 引進)
  7. object(array, function, date…)

前六種我們又可稱為原始型別(primitive type),除了此之外都是物件(object)。

要知道這個變數是哪類資料可以用 typeof

console.log(typeof 10)
會印出 number
console.log(typeof ‘123’)
會印出 string

但是 typeof 其實沒有你想的這麼簡單

console.log(typeof [])
會印出 object,這其實滿合理的,因為剛剛說除了那六種,其他都是 object
console.log(typeof function(){})
這裡卻會印出 function
console.log(typeof null)
這裡總該印出 null 了吧,結果卻印出 object,這是 JS 一開始被創造時就有的 bug,詳細緣由可以參考這篇:The history of “typeof null”

如果想要知道 typeof 會印出什麼東東,可以對照這裡

但其實 typeof 還有個常見的用法

先觀察以下程式碼

const a = 10
console.log(typeof a)

會印出 10,很合理

那如果沒賦值呢?

const a
console.log(typeof a)

此時會印出 undefined

那如果連宣告都沒宣告呢?

console.log(typeof a)

也會印出 undefined

那如果是這樣呢?

console.log(a)

就會出現錯誤

所以知道以上的規則之後,我們如果想要寫出一個,當 a 有值得時候印出,沒值則不做事的式子該怎麼寫呢?

var a = 10

if (a !== undefined){
  console.log(a)
}

這樣寫乍看之下沒什麼錯,因為的確印出 10 了,但如果 a 沒宣告時,就會出現錯誤,所以通常我們都會寫成這樣

var a = 10

if (typeof a !== “undefined”){
  console.log(a)
}

這樣就算 a 沒宣告或沒賦值時,也不會出現錯誤了。

從前面可以知道,typeof 可以幫我們檢視一個變數的型態,但若變數是一個陣列,只會印出 object,如果要檢視是否為 array 的話,要使用 Array.isArray([]),是的話就會回傳 true。

你可能會覺得我只是要判斷個形態怎麼這麼難,沒有可以準確顯示的嗎?

可以用Object.prototype.toString.call(放入你要檢測的東西)

例如:console.log(Object.prototype.toString.call(null))

會印出[object Null],看後者即是答案。


可變 vs 不可變

講了這麼多,還記得開頭我們說到的 primitive type 嗎?

他跟 object type 之間很大的不同在於,原始型別是不可變的(immutable)

可是你會說不是啊,這樣不就變了嗎?

var a = 10
a = 20

這只是重新賦值。

這邊談的不可變是指我們在操作值所回傳的結果

let str = “hello”
str.toUpperCase()
console.log(str)

印出的還是 hello

所以換言之 object type 是可變的

var arr = [1]
arr.push(2)
console.log(arr)

會印出[1, 2],這就是可變的(mutable)。

而物件型別之所以可變也跟他記憶體儲存的方式有關

當我們宣告 var arr = [1],他會將 [1] 存於一個記憶體我們假設是 0x01

所以當我們宣告 var arr2 = arr 時,也就是將 arr2 指向 0x01

所以 arr2 做動作時就會針對 0x01 去改變

但若我們寫 var arr2 = [1, 2, 3]

因為是一個新值,因此會新開一個記憶體空間給他,假設是 0x02

所以我們再對 arr2 做動作時,就是對 0x02 去做動作而非 0x01 了,這點要特別注意!

這邊推薦 Huli 大大寫的一篇,看完可能會對這種模式有更多不同的想法也說不定:深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?


== 和 ===

講到資料型態,感覺就不得不提到兩個等於和三個等於的差別了。

先看一個簡單的例子

console.log(2 == ‘2’) // true
console.log(2 === ‘2’) // false

當使用 == 時,他會幫你轉換型別,所以這裡的數字 2 和字串 2 就會相等,但這其中機制有點複雜,就不另外講述。

而使用 === 則是會比較型別,如果型別不同,答案絕對是 false

所以推薦在比較時,使用 === 才不會出現非預期的 bug!

接著要提一個特別的東西叫 NaN(Not a Number)

var a = Number(‘123’)
console.log(a)

印出 123,Number 可以將數字的 string 轉為數字

但如果轉換的東西不是數字呢?

var a = Number(‘hihi’)
console.log(a)

則會印出 NaN

這時候我們判定一下他的型別 console.log(typeof a),會得到 number,從這裡我們可以知道 NaN 的型別竟然是 number,很有趣吧。

但你確定你真的認識 NaN 嗎,我們來比較一下

var a = Number(‘hihi’)
console.log(a === a)

本人跟本人比較一定一樣啊,為何要比?
結果竟然印出 false

這就是一個特例,無法解釋,你可以當作一個特殊規則記下來。

要判定是不是 NaN 可以用 isNaN()

var a = Number(‘hihi’)
console.log(isNaN(a))

答案會是 true

要看詳細的 == & === 比較可以參考這裡


let & const

撇開變數的作用域,var 和 let 的功能其實是很像的,所以我們先講 const。

Const 其實就是 constant(常數),也就是不變的數,所以顧名思義他的值無法被改變。

const a = 10
a = 20
console.log(a)

const 一旦被宣告,其值如果被嘗試更改,就會噴出 TypeError: Assignment to constant variable. 的錯誤訊息。

那看看另一個例子

const b = {
  number: 10
}
b.number = 20
console.log(b)

log 出來竟然會印出 20,這是為什麼?不是說好的值不會變嗎?

其實這裡指的不會變的是記憶體位置,所以再看看這題,答案也就合理了。

另外,在使用 const 的時候要特別注意,除了不能重新賦值外,在宣告的時候就要給他值了。


ES6 前變數的作用域(scope)

let & const 在宣告時,跟 var 最大的不同就是作用域(scope)。

但在講哪裡不同之前,我們要先說明作用域這個東西。

因為 let & const 是 ES6 的東西,所以我們會先著重在 ES6 以前的宣告。

作用域其實就是變數的生存範圍

什麼意思?用一段程式碼來說明

function test() {
  var a = 10
  console.log(a)
}

test()
console.log(a)

會印出 10, a is not defined

因為在 ES6 以前,是以 function 作為一個作用域的單位,變數出了 function 就不存在。

所以上面這個 a 就在 test 這個 function 的 scope 裡,出了這個 scope ,a 就找不到。

-

那如果我們把 a 宣告在外面,我們稱為全域變數(global variable)

var a = 10

function test() {
  console.log(a)
}

test()
console.log(a)

兩個都會印出 10

Test scope 裡的 console.log 在 function 裡面找不到 a,就會往外找。

-

再看一個例子

var a = 20

function test (){
  a = 10
  console.log(a)
}

test()
console.log(a)

會印出 10, 10

因為執行 function 時,沒有找到宣告的 a,就會往外找

找到全域有一個 a,那因為 function scope 內有 a = 10

所以會把 a 改成 10

當執行到 console.log(a) 時,就會印出 10

-

那如果做一點小改變,把 var a = 20 拿掉呢?

function test (){
  a = 10
  console.log(a)
}

test()
console.log(a)

也是印出 10, 10

因為如果直接對一個變數賦值,而沒有事先宣告,這個變數就會自動被宣告為全域變數。

所以就算 a 是在 function 裡,但因為他符合上面的條件,所以自動被宣告為全域變數,外面 console.log(a) 就會印出 10。

-

這種一層一層往上找,很像一條鏈的東西,我們稱為 scope chain,JavaScript 的 scope chain 在你寫好程式碼的時候就決定了,而非依照呼叫順序決定。


ES6 後變數的作用域

let & const 的作用域(scope)和 var 不同,剛提到 ES6 以前,是以 function 作為作用域的單位。

但在 ES6,當你使用 let or const 定義的變數,只要有{ },就會產生一個作用域;但如果用 var 宣告,還是會以 function 為一個 scope 的單位。

var : function scope
let, const : block scope

function test() {
  if (10 > 5) {
    var a = 10
  }
  console.log(a)
}

test()

此時會印出 10

但如果是 let 或是 const

function test() {
  if (10 > 5) {
    let a = 10
  }
  console.log(a)
}

test()

則會顯示 a is not defined


#JavaScript 核心 #javascript #var #let #const #scope #typeof







Related Posts

[ 筆記 ] 瀏覽器資料儲存 - Cookie、LocalStorage、SessionStorage

[ 筆記 ] 瀏覽器資料儲存 - Cookie、LocalStorage、SessionStorage

ES6 相關筆記

ES6 相關筆記

上班不如賣雞排?軟體工程師背後的問題

上班不如賣雞排?軟體工程師背後的問題


Comments