變數的資料型態
在 JavaScript 裡面,共有七種資料型態
前六種稱為 primitive type
primitive type(原始型態)
- null
- undefined
- string
- number
- boolean
- symbol (ES6)
除了 primitive type 之外,其他的都是 object(物件)
- object (包括 array, function, date ...)
用 typeof
來得知變數的資料型態是什麼
但是,用 typeof
並不能保證最後出來的結果會是這七種型態(這是一個滿混淆人的地方),typeof
有一個表格會列出每一種變數出來的結果是什麼
例如:
- function 的資料型態是 object,但是如果用
typeof
對 function,出來的結果會是 functionconsole.log(typeof function () {});
null
的資料型態是 null,但是如果用typeof
對 null,出來的結果會是 object
這是 JavaScript 從以前到現在最廣為人知的一個 bug,但是沒人敢去修它,因為修了怕很多東西會壞掉console.log(typeof null);
typeof null
會是 object 的原因
官方文件 typeof null
在 JS 底層的實作裡面,有一個東西叫做 type tag(用來標記一個變數是什麼 type)
給 object 的 type tag 是 0
null
會被表示成一個 NULL pointer,type tag 是 0x00
用 typeof
去檢視 null
時,type tag 0x00
就被當成是 0
,因此會回傳 object
用 typeof
來檢查:一個變數是否有被宣告、且被賦予值
如果 a
都沒有被宣告和賦予值 ,typeof a
結果會是 undefined
console.log(typeof a); // undefined
所以,有時會用下面這樣來檢查:一個變數是否有被宣告、且被賦予值
- 如果沒有被宣告和賦予值,就不會發生任何事情
- 如果有被宣告和賦予值,才印出來
if (typeof a !== "undefined") { console.log(a); }
為什麼要用 typeof
來檢查,不能直接寫 if (a !== "undefined")
嗎?
如果是直接寫 if (a !== "undefined")
- 當
a
有被賦予值時,是 ok 的,沒有問題
var a = 10;
if (a !== "undefined") {
console.log(a);
}
// output: 10
- 可是當
a
沒有被宣告和賦予值時,程式就會整個出錯,會跳出「ReferenceError: a is not defined」,造成程式無法執行下去if (a !== "undefined") { console.log(a); }
用 Array.isArray()
判斷是否為 array
因為 typeof []
結果會是 object
如果想要知道一個變數是不是 array 的話,可以用 Array.isArray()
注意:在比較舊的瀏覽器,沒有 Array.isArray()
這個方法可以用
Object.prototype.toString.call()
可以準確判斷出型態
第一個參數傳:我要檢測的東西
console.log(Object.prototype.toString.call("hello")); // [object String]
primitive type 是 immutable
primitive type 和 object 最大的差別就是:primitive type 是 immutable(不能改變的)
範例一:primitive type 是 immutable
下面這樣不叫做「改變」,叫做「重新賦值」
var a = 10;
a = 20;
範例二:primitive type 是 immutable
str.toUpperCase()
會回傳一個新的字串,而不是去改變 str
自己
所以 console.log(str)
的結果並不會變成大寫
var str = "hello";
str.toUpperCase();
console.log(str); // output: hello
所以,必須要用一個新的變數 newStr
去接收新的字串:
var str = "hello";
var newStr = str.toUpperCase();
console.log(str, newStr);
// output: hello HELLO
範例三:object 是可以改變的
arr.push(2)
可以改變原本的 arr
var arr = [1];
arr.push(2);
console.log(arr);
// output: [ 1, 2 ]
讓你摸不透的 = 賦值
primitive type 和 object,對於「賦值」的反應是不一樣的
primitive type
var a = 10;
var b = a;
console.log(a, b); // 10 10
b = 200;
console.log(a, b); // 10 200
object
var obj = {
number: 10,
};
var obj2 = obj;
console.log(obj, obj2); // { number: 10 } { number: 10 }
obj2.number = 20;
console.log(obj, obj2); // { number: 20 } { number: 20 }
把 obj2
的 number 改成 20,但是發現 obj
和 obj2
都變成 { number: 20 }
了
對於 primitive type,變數裡面存的是「值」
宣告變數 a
和 b
:
把 a
的值(10)複製給 b
var a = 10;
var b = a;
在記憶體裡面,可以想成就是這樣存的:
a: 10
b: 10
當我把 b
的值改成 20:
var a = 10;
var b = a;
b = 20;
在記憶體裡面就會把 b
的值改成 20:
a: 10
b: 20
對於 object,變數裡面存的是「記憶體位置」
當我宣告變數 obj
是 { number: 10 }
這個物件時,{ number: 10 }
會被放到某個記憶體位置上(例如 0x01
這個位置),在變數 obj
裡面,存的會是 0x01
這個記憶體位置
當我寫 var obj2 = obj
時,就是把 obj
的值(0x01
)複製給 obj2
,所以 obj
和 obj2
裡面存的都是 0x01
,是指向同一個記憶體位置(是同一個物件)
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 10
}
// 變數裡面存的東西
obj: 0x01
obj2: 0x01
var obj = {
number: 10,
};
var obj2 = obj; // 把 obj 的值複製給 obj2
console.log(obj, obj2); // { number: 10 } { number: 10 }
obj2.number = 20;
console.log(obj, obj2); // { number: 20 } { number: 20 }
當我用 obj2.number = 20
時,意思是:我要去改變「obj2
存的記憶體位置 0x01
裡面的 object 底下的 number
屬性,把 value 改成 20」
所以, obj
和 obj2
都會是 { number: 20 }
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 20
}
如果是用 =
重新賦值,就會是另一個新的記憶體位置了
舉例來說
當我宣告 arr = []
時,背後做的事情是:
- 先給
[]
一個記憶體位置0x10
- 再把
0x10
存到變數arr
裡面
當我寫 var arr2 = arr
時,就是把 arr
的值複製給 arr2
,所以 arr
和 arr2
裡面存的都是 0x10
,是指向同一個記憶體位置(是同一個陣列)
// 在 0x10 這個記憶體位置裡面
0x10: []
// 變數裡面存的東西
arr: 0x10
arr2: 0x10
var arr = [];
var arr2 = arr;
console.log(arr, arr2); // [], []
但是,當我寫 arr2 = ["arr2"]
時,因為是用 =
賦值,所以背後做的一樣會是:
- 先給
['arr2']
一個新的記憶體位置0x20
- 再把
arr2
裡面存的東西,改成0x20
這個新的記憶體位置
這時,arr
和 arr2
已經指向不同的記憶體位置了,因此不會互相影響
// 在 0x10 這個記憶體位置裡面
0x10: []
0x20: ['arr2']
// 變數裡面存的東西
arr: 0x10
arr2: 0x20
var arr = [];
var arr2 = arr;
console.log(arr, arr2); // [], []
arr2 = ["arr2"];
console.log(arr, arr2); // [], ['arr2']
在 JS,當我對 object 用 =
賦值時,做的事情是:
- 先給這個 object 一塊新的記憶體位置
- 把這個記憶體位置存到變數裡面
如果不小心在 if
裡面寫了 =
var a = 10;
if ((a = 20)) {
console.log(123);
}
// output: 123
在 if
裡面應該要用 ==
或是 ===
才對
如果不小心在 if
裡面寫了 =
,是重新賦值的意思,if ((a = 20))
的執行流程就會變成是:
- 先執行
a = 20
- 再去判斷
a
是否為 true
a = 20;
if (a) {
console.log(123);
}
因此,會印出 123
== 與 === 的差別
差別在於:==
會按照一個規則去轉換型態,===
不會轉換型態
==
會轉換型態,所以 console.log(2 == '2')
是 true
===
不會轉換型態,所以 console.log(2 === '2')
是 false
用 ===
比較兩個 primitive type 時,比較的是「兩個變數的值」
用 ===
比較兩個 object 時,只有在「兩個 object 指向同一個記憶體位置」時,===
才會是 true
console.log([] === []); // 兩個陣列是在「不同的記憶體位置」,所以是 false
console.log({} === {}); // 兩個物件是在「不同的記憶體位置」,所以是 false
範例一:比較物件
因為 object 裡面存的是記憶體位置,所以 obj === obj2
比較的是「變數裡面存的記憶體位置」,因為記憶體位置都是 0x01
,所以會是 true
// 在 0x01 這個記憶體位置裡面
0x01: {
number: 1,
};
// 在 object 裡面存的是記憶體位置
obj: 0x01
obj2: 0x01
var obj = {
number: 1,
};
var obj2 = obj;
obj2.number = 2;
console.log(obj === obj2); // true
範例二:陣列
因為 arr
和 arr2
裡面存的記憶體位置不同,所以儘管陣列的值一樣,arr === arr2
還是 false
// 在記憶體位置裡面
0x01: [1];
0x02: [1];
// 在 object 裡面存的是記憶體位置
arr: 0x01
arr2: 0x02
var arr = [1];
var arr2 = [1];
console.log(arr === arr2); // false
edge case: NaN
當我用 Number()
把一個不是數字的東西要轉成數字時,a
就會是 NaN
這是在使用 ==
和 ===
時的唯一特殊案例:NaN
不會等於任何東西(甚至是連自己本身都不等於)
因為一個「不是數字的東西」無法跟另一個不是數字的東西做比較
var a = Number("hello");
console.log(a); // NaN
console.log(a === a); // false
console.log(NaN === NaN); // false
用 isNaN()
這個 function 來檢視是不是 NaN
let 與 const
下面這樣寫會跳出錯誤「SyntaxError: Missing initializer in const declaration」
因為用 const
宣告變數時,一開始就要給初始值
const b;
b = 20;
變數的生存範圍:Scope
ES6 以前的作用域
Scope(作用域),就是「變數的生存範圍」
區域變數
範例一:
在 ES6 以前,作用域的基本單位是「function」,只有 function 能產生一個新的作用域
在上面的例子中,function test()
產生了一個 test scope,只有在這個 test scope 裡面,變數 a
才會被看到
變數 a
是區域變數
function test() {
var a = 10;
console.log(a); // 10
}
test();
console.log(a); // a is not defined
全域變數
在最外層宣告的,叫做 global variable(全域變數)
全域變數的作用域是以「檔案」為單位,意思是:一旦宣告後,整個檔案的任何位置都看得到這個變數
變數 a
是全域變數
作用域是「往上層找」
- 第三行的
console.log(a)
,JS 的引擎會先在function test()
的作用域裡面找「是否有宣告變數a
」,沒有找到的話,就會繼續往上層找,就找到了 global 這層的變數a
var a = 20;
function test() {
console.log(a); // 20
}
test();
console.log(a); // 20
區域變數和全域變數不會互相影響
範例一:
因為 function test
自己就有宣告變數 a
了,所以在 test scope 裡面用的 a
是「只存活在 test scope 的區域變數」,跟外面的全域變數 a
沒有任何關係
第五行把區域變數的 a
的值改成 30,並不會影響到全域變數的 a
var a = 1;
function test() {
var a = 8; // 這個 a 是在 test scope 裡面的區域變數
console.log(a); // 8
a = 30; // 改變區域變數的 a 的值,並不會影響到全域變數的 a
console.log(a); // 30
}
test();
console.log(a); // 1 (這是全域變數的 a)
範例二:
因為在 function test
裡面沒有宣告過 a
,所以第三行的 a = 8
會往上層找到全域變數 a
,把全域變數 a
的值改成 8
在 test scope 裡面的 a
就是「全域變數的 a
」
var a = 1;
function test() {
a = 8; // 這個 a 會是全域變數的 a,把值改成 8
console.log(a); // 8
}
test();
console.log(a); // 8 (全域變數 a 的值已經變成 8 了)
範例三:
第四行的 console.log(a)
,會先在自己的作用域(test scope)裡面找,所以會找到 var a = 10
第八行的 console.log(a)
,會先在自己的作用域(global)裡面找,所以會找到 var a = 20
var a = 20;
function test() {
var a = 10;
console.log(a); // 10 (這個 a 是在 test scope 的區域變數)
}
test();
console.log(a); // 20 (這個 a 是在 global scope 的全域變數)
自動在 global 幫我宣告一個變數
如果 a = 10
在前面都沒有被宣告過,就會自動在 global 幫我宣告一個變數 a
注意,下面這樣是不好的寫法,因為原本只想在 function 裡面宣告的變數,變成全域變數了(全域變數太多,可能會跟其他變數衝突到)
function test() {
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
就等同於是這樣寫:
var a; // 在 global 幫我宣告一個變數 a
function test() {
a = 10;
console.log(a); // 10
}
test();
console.log(a); // 10
scope chain 就是「一直往上層 scope 找」的過程
下面範例的 scope chain(作用域鏈)就是:inner scope -> test scope -> global scope
會從自己的作用域開始找,找不到的話就一直往上一層找
var a = "global"; // global scope
function test() {
// test scope
var a = "test scope a";
var b = "test scope b";
console.log(a, b); // test scope a test scope b
function inner() {
// inner scope
var b = "inner scope b";
console.log(a, b); // test scope a inner scope b
}
inner();
}
test();
console.log(a); // global
作用域的判斷
錯誤的觀念:
因為我在 function change
裡面呼叫 test()
,所以在 function test
裡面的 a
會去找 function change
裡面宣告的 a
下面這是錯誤的 scope chain
test scope -> change scope -> global scope
正確的觀念:
關於作用域的判斷,跟「在哪裡呼叫 function」一點關係都沒有,而是用「在哪裡宣告 function」來判斷。在宣告 function 的當下,scope chain 就已經定義好了,不管在哪裡呼叫這個 function,scope chain 都不會改變
- change scope 和 test scope 是兩個平行的 scope,也就是說:
- change scope 的上一層就是 global scope
- test scope 的上一層也是 global scope,而不是 change scope
下面這是正確的 scope chain(永遠不會改變)
change scope -> global scope
test scope -> global scope
所以,下面 console.log(a)
會去找「在 global scope 的 a
」
var a = "global"; // global scope
function change() {
// change scope 的上一層就是 global scope
var a = 10;
test();
}
function test() {
// test scope 的上一層就是 global scope
console.log(a); // global
}
change();
注意,在 function change
裡面呼叫 test()
,並不等於下面這樣:
下面這樣叫做「在 function change
裡面宣告 function test
」,這時,test scope 的上一層就會是 change scope 了
function change() {
// change scope
var a = 10;
function test() {
console.log(a);
}
}
如果把 function test
放在 function change
裡面宣告,那不管我在哪裡呼叫 test()
,function test
的 scope chain 都會是:test -> change -> global
var a = "global"; // global scope
function change() {
// change scope
var a = "change";
function inner() {
var a = "inner";
test();
}
function test() {
// scope chain 會是:test -> change -> global
console.log(a);
}
inner();
}
change();
let 與 const 的生存範圍
用 var
宣告的變數,作用域的範圍是「一個 function」
例如:
在 function test
裡面,都是變數 b
的作用域
function test() {
var a = 60;
if (a === 60) {
var b = 10; // b 的作用域是「一整個 function test」
}
console.log(b); // 10
}
test();
用 let
或 const
宣告的變數,作用域的範圍是「一個 block」
例如:
function test() {
var a = 60;
if (a === 60) {
let b = 10; // b 的作用域只有在「if 這個 block」
}
console.log(b); // b is not defined
}
test();