JavaScript: Data Type & Variable Assignment


Posted by Nicolakacha on 2020-09-29

本篇會討論 JavaScript 的資料型態,以及原始型態及物件型態在變數宣告賦值上的行為。

JavaScript 資料型態

JavaScript 資料型態可以分為原始型態(Primitive type)和物件型態(Object type):

原始型態(Primitive type)

  • null (null)
  • undefined (undefined)
  • string ("hello")
  • number (5487)
  • boolean (true, false)
  • symbol (Sym)
  • bigints (8n)

物件型態(Object type)

  • object (object, array, function, date)

資料型態的檢查

Typeof

typeof 可以用來檢查資料的型態,但要注意 typeof null 回傳的是 object,這是 JavaScript 的歷史 bug。

另外常見的用法是可以先用 typeof 檢查變數是不是 undefined,得知 a 有沒有被宣告:

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

Array.isArray()

Array.isArray() 可以用來檢查是不是陣列:

var a = [1, 2, 3, 4];
Array.isArray(a);
// true

Object.prototype.toString.call()

Object.prototype.toString.call() 更精確地檢查資料的型態:

var a = [1, 2, 3, 4];
Object.prototype.toString.call(a);
// "[object Array]"

關於 == 與 === 的迷思

許多人會說:「=== 會檢查型別但 == 不會,因為不好判斷 == 是怎麼自動轉型的,所有關於比較的問題,我一律建議使用 ===,就像所有的感情問題,我一律建議分手

var a = 1;
var b = '1';
console.log(a == b); // true
console.log(a === b); // false

所以 == 就沒有任何適用的地方嗎 QQ?與其去記住「就用 === 就對了。」,不妨思考,在這裡讓 value 自動轉型有幫助嗎?如果要比較的對象的資料型態已經很確定時,使用 == 反而可以讓程式碼看起來更好讀且直觀哦。

"== is not about comparisons with unknown types, == is about comparisons with known type(s) optionally where conversions are helpful."
-- Kyle Simpson

NaN

當一個字串被自動轉型成 number 時,若轉出來的值不是 number,則 會變成 NaN,就是不是數字的意思。

但要注意的是,NaN 什麼也不是,甚至不等於它自己 XD

var a = Number('hello')

console.log(type of a)
// NaN

console.log(a === a)
// false

此外,也可以用 isNaN() 來檢查該變數是否是 NaN。

變數的宣告與賦值

宣告和賦值是不同的事,var a 是在宣告一個變數 a,var b = 1 是在宣告一個變數 b 並賦值為 1,b = 2 則是在把變數 b 重新賦值為 2。

var a;
var b = 1;
b = 2;

可以把等號左側的變數想像成是一條線,指向等號右側的東西。所以 var b = 1,是宣告一個變數 b,並讓它指向 1,而 b = 2 則是把這個指向從 1 身上移開,改成指另一個叫做 2 的值。

"Variables are not values.
Variables point to values."
-- Dan Abramov

undeclare ≠ undefined
未宣告(undeclare)和未定義(undefined)是兩個完全不同的概念,未宣告不等於未定義,未宣告的意思是,這個變數從頭到尾都不存在,沒有被宣告過,所以印出來就會向下面例子中的 a 一樣報錯,而未定義則是這個變數被宣告過了,它存在但還沒被賦值,此時它就指向 undefined。

var b;
console.log(a) // Uncaught ReferenceError: b is not define
console.log(b) // undefined

原始型態(Primitive type)

Immutable

上面列出來的資料型態,除了物件型態(Object type)之外,其他 的都是原始型態(Primitive type),可以想成每個原始型態都是獨一無二且不可改變的(Immutable)的值。

不可改變的意思是,我們寫的任何程式碼都無法改變它。123 就是 123,永遠不會變成 321,而 321 自己也是獨一無二的,永遠不會變成 123。

下面這個例子中,可以想像成 a, b, c 都指向那個獨一無二的 4:

let a = 4;
let b = a;
let c = 2 + 2;

畫成模型圖:

這個例子中,把變數 b 賦值為 200,是讓 b 不指向 10 而去指向 200,但 a 還是指向 10,所以 a 和 b 現在分別指向不同的值:

var a = 10;
var b = a;
b = 200;
console.log(a, b);
// 10 200

Pass by value

下列程式碼中,我們宣告 a 變數並賦值為 10,當我們把 a 當成參數傳進去 test 函式時,意思是:找到 x 變數所指向那個值,是把變數的值,而不是把變數傳進去,所以變數 a 還是指向那個 10,而不是指向 20:

function test(x) {
  x = x * 2;
}

var a = 10;
test(a);
console.log(a); // 10

物件型態(Object type)

Mutable

物件型態也是值,但這個值在底層實際上是存一個記憶體的位置,物件型態的值不像是原始型態都是獨一無二且不可改變的,而是一個可以被我們創造出來並改變的值。

這段程式碼印出是把 2 這個值印出 3 次,因為 2 是原始型態,印出來的 2 自始至終都是同一個獨一無二的 2:

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

這段程式碼中,我們自己創造了 3 個物件並印出來,雖然都是 {},但 3 個物件都是完全不同的物件:

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

這個例子中,雖然 a, b, c 印出來的結果一樣,但其實 a 和 b 是指向同一個物件,而 c 則指向另一個物件:

var a = {}
var b = a;
var c = {}

console.log(a, b, c);
// {} {} {}
  1. 創造一個新的物件 { },讓 a 指向這個物件
  2. 讓 b 指向 a 所指向的物件
  3. 創造一個新的物件 { },讓 a 指向這個物件

所以可以得出結論:當我們為變數賦值為物件型態時,如果本來就這個物件本來就存在,該變數就指向同一個物件,如果沒有,就創造一個物件並讓變數指向它。

Property

而物件本身可以透過 property 來指向某個值,可以想像成 property 是一條線或是一個箭頭,指向另一個值。

下列程式碼中,先創造一個物件,讓宣告的變數 a 指向這個物件中,在此物件中建立一個名為 number 的 property,而它指向 10 這個值。宣告變數 b 所指向的的是變數 a 所指向的值,所以 a 和 b 所指向相同的物件。至於 c 則是指向另一個新創造出來的物件,儘管看起來和 a 和 b 一樣,但實際上是不同的物件:

var a = {
  number: 10;
}

var b = a;
var c = {
  number: 10;
}

console.log(a, b);
// { number: 10 } { number: 10 } { number: 10 }

若把 a, b, c 畫成圖,可以想像成這樣:

改變物件 Property 的指向

下列程式碼中,b.number = 20 的意思是,讓物件裡面 number 不去指向 10 了,改成指向 20,既然 a 和 b 變數都是指向同一個物件,印出來都會是 { number: 20 },且 a === b 是 true,而 c 則是指向另一個物件,印出來還是 { number: 10 }。

var a = {
  number: 10;
}

var b = obj;
obj2.number = 20;

console.log(a, b, c);
// { number: 20 } { number: 20 } { number: 10 }

console.log(a === b);

畫成模型圖:

陣列也是一種物件,所以行為也相同:

var a = [];
var b = a;
a = ['hellooo'];
console.log(a, b);

// [] ['arr2']
  1. 把變數 a 賦值為 [ ],創造一個 [ ],讓變數 a 指向 [ ],
  2. 把變數 b 賦值為 a ,意思讓 b 去指向 a 所指向的那個 [ ]。
  3. 把 b 重新賦值為 ['hellooo'] ,創造一個新的 [ ],讓 arr 2 去指向它,而這個陣列裡,把第一個參數當成一條線,去指向獨一無二的 hellooo。

畫成模型圖:

Property 永遠指向值

在下面這個例子中, home 這條 property 指向的是 person1.home 所指向的那個值,而不是指向 person1.home 這個 property,要記住 property 只是一條線,永遠指向一個值。

let person1 = {
  name: 'Nicolas',
  home: { city: 'Taipei' }
};

let person2 = {
  name: 'Matt',
  home: person1.home
};

let & const

ES6 的 const 宣告之後之後就不能再重新賦值了,也就是不能重新指向別的值,所以一開始宣告的時候就要賦值。而 let 可以被重新賦值,但又不會像 var 一樣汙染到外部的變數。

下列程式碼中,因為 { number: 1 }{ number: 2 } 是兩個完全不同的物件,這樣就等於把變數重新賦值了,所以會報錯:

const a = {
    number: 1
}

a = {
    number: 2
}

下列程式碼並沒有去改動 a 所指向的物件,只是讓該物件裡 number 這個 property 去指向另一個原始型態的值,所以不會報錯。

const obj = {
    number: 1
}

obj.number = 2

Pass by sharing

物件型態還有一個有趣的地方是,當 function 的參數是物件,而傳進來的物件在 function 內被重新賦值時,外部變數還是不會被重新指向,因為傳進去的是物件的值,而不是變數。

下列程式碼中,這個 coin1 不會被改到,是因為傳進來的是變數的值 { value: 10 } ,參考 MDN,arguments 物件是函式內的區域變數,所以 obj = { value: 123 } 這行實際上是讓 arguments 物件的第一個索引不去指向 { value: 10 } ,改成去指向 { value: 123 } ,而不是把外面的變數 coin1 去指向 { value: 123 }

var coin1 = { value: 10 };

function changeValue(obj) {
  obj = { value: 123 };
}

changeValue(coin1);
console.log(coin1);   // 此時 coin1 仍是 { value: 10 }

總結

要理解變數的賦值,最重要的是熟悉這種指向的觀念,而不是直覺的把賦值當作把值「放進」變數裡,這種指來指去的概念就是賦值的精髓,要更詳細釐清這個概念,推薦可以看 Dan Abramov 的 Just JavaScript

參考資料(也推薦閱讀)

Dan Abramov 的 Just JavaScript

MDN web docs - The arguments object

深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

覺得 JavaScript function 很有趣的我是不是很奇怪


#javascript #variable #變數 #程式導師計畫







Related Posts

Day05 從 Hash Anchor 看原生 History API (上)

Day05 從 Hash Anchor 看原生 History API (上)

Day 1:女媧造人,創造你的主人公吧

Day 1:女媧造人,創造你的主人公吧

Radio Button 事件切換實作

Radio Button 事件切換實作


Comments