JS 紀錄2 - 包裹物件、自動轉型

JavaScript 轉型觀念紀錄

原始型別(Primitive Type) - 不允許自由擴增屬性

  • 但卻有屬性 & 方法可以使用,因為有原始型別包裹物件 (primititve type wrapper type) & 父元素的關係
  • null, undefined 沒有原始型別包裹物件
  • string, number, boolean

物件型別(Object Type) - 可以自由擴增屬性

原始型別包裹物件 (primititve type wrapper type)

  • 主要用途
    • 透過包裹物件的 toPrimitive 中的 valueOf()toString()原始型別產生物件型別的特性,自由擴增屬性方法

toPrimitive

  • toPrimitive(input [, PreferredType])
    • input
      • 代入的值
    • PreferredType
    • 會依照設定的首選的類型,決定要先後呼叫 valueOftoString 的順序
      • valueOf()
        • 取得物件內部的原始型別的值 (Primitive Value)
      • toString()
        • 取得物件內部的原始型別的值 (Primitive Value) 並轉換成字串型別
      • 沒有提供這個值也就是預設情況,則會設定轉換的 hint 值為 “default”
  • 轉換方式
    • 如果 input 是原始資料類型,則直接回傳 input
    • PreferredType 為 Number 首選類型時,優先使用 valueOf,然後再呼叫 toString
    • PreferredType 為 String 首選類型時,優先使用 toString,然後再呼叫 valueOf
    • 預設呼叫方式則是先呼叫 valueOf 再呼叫 toString,否則,拋出TypeError錯誤
    • 兩個例外,一個是 Date 物件預設首選類型是字串(String),另一是 Symbol物件,它們覆蓋了原來的 PreferredType 行為
  • 簡略規則
    • undefined -> undefined(基本型別值,不轉換)
    • null -> null(基本型別值,不轉換)
    • boolean -> boolean(基本型別值,不轉換)
    • number -> number(基本型別值,不轉換)
    • string -> string(基本型別值,不轉換)
    • object:使用 [[DefaultValue]] 內部方法,依照傳入的參數決定要使用 toString 或 valueOf 取得原始型別值

Date

  • 首選類型為 String,它優先使用 toString 來進行轉換,最後字串連接運算
1
1 + (new Date()) // "1Sun Nov 27 2016 01:09:03 GMT+0800 (CST)"
  • Date 物件中的 valueOf 回傳值,需要使用一元加號(+),來強制轉換它為數字類
1
+new Date() // 1480180751492

Object

  • valueOf() 回傳值: 物件本身
  • toString() 回傳值: “[object Object]” 字串值,不同的內建物件的回傳值是 “[object type]”字串,”type” 指的是物件本身的類型識別
1
2
3
var a = {age:20}
a.valueOf() // {age: 20}
a.toString() // "[object Object]"

Array

  • valueOf() 回傳值: 物件本身
  • toString() 回傳值: 相當於用陣列值呼叫 join(‘,’) 所回傳的字串,也就是 [1,2,3].toString() 會是 “1,2,3”,這點要特別注意
1
2
3
var b = ['hot','cold’]
b.valueOf() // ["hot", "cold”]
b.toString() // "hot,cold"

Function

  • valueOf() 回傳值: 物件本身
  • toString () 回傳值: 函式中包含的程式碼轉為字串值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person (name,age){
this.name = name
this.age = age
}
var p1 = new Person1('Jim',20)
Person.prototype.valueOf = function () {
return this.age;
};
Person.prototype.toString = function () {
return this.name;
};

p1.valueOf() // 20
p1.toString() // "Jim"

轉型過程的抽象值運算

  • toPrimitive 賦予 String, Number, Boolean 有 valueOf() & toString() 方法

JSON.stringify

  • JSON 的字串化
  • 無法轉為 JSON 字串的非法值有 undefined、function、symbol、具有循環參考(circular reference)的物件
1
2
3
4
JSON.stringify(42)   // "42"
JSON.stringify(true) // "true"
JSON.stringify(null) // "null"
JSON.stringify('Hello World') // ""Hello World"",字串會在外面再包一層引號

String

  • 任何非字串的值被強制轉型為字串
1
2
3
4
String([1,2,3]) // “1,2,3"
String([{}]) // "[object Object]”
String(false) // “false"
String(0) // "0"

Number

  • 將非數字值當成數字來操作
1
2
3
4
5
6
7
Number(undefined)         // NaN
Number(null) // 0
Number(true) // 1
Number(false) // 0
Number('12345') // 12345
Number('Hello World') // NaN
Number({ name: 'Jack' }}) // NaN

Boolean

  • Truthy 與 Falsy 的概念
1
2
3
4
Boolean(false) // false
Boolean('1’) // true
Boolean([]) // true
Boolean({}) // true

自動轉型

  • 物件型別來比較原始型別,所有的物件型別物件,一定會透過 toPrimitive 裡面的 valueOf()toString() 先轉成原始型別物件,然後才進行比較,這就是「自動型別轉換」

強制轉型(coercion)分為兩種

「明確的」強制轉型 (explicit coercion) 程式碼中刻意寫出來的型別轉換的動作

1
2
3
4
5
6
7
8
9
String(123)         // "123"
(123).toString() // "123"
Number('123') // 123
+('123’) // 123
-('-123') // 123
String(123) // “123"
Math.floor(-29.8) // -30
~~-29.8 // -29
-29.8 | 0 // -29

「隱含的」強制轉型 (implicit coercion) 程式碼中沒有明確指出要轉換型別卻轉型的

1
2
3
4
5
6
"0" == false;  // true,字串轉數字、布林再轉數字
false == 0; // true,布林轉數字
false == “”; // true,字串轉數字、布林再轉數字
false == []; // true,布林轉數字、陣列取 toString 得到空字串再轉數字
false == {}; // false,布林轉數字、物件取 valueOf 得到空物件
"" == 0; // true,字串轉數字
  • + 運算子是數字的相加,還是字串的串接?
    • 兩運算元的型別不同,當其中一方是字串時,+ 所代表的就是字串運算子,而會將另外一個運算元強制轉型為字串,並連接兩個字串
    • [] + {} 中,[] 會轉為空字串,而 {} 會轉為字串 “[object Object]”
    • {} + [] 中,{} 被當成空區塊而無作用, +[] 被當成強制轉型為數字 Number([]) (由於陣列是物件,中間會先使用 toString 轉成字空串,導致變成 Number(‘’))而得到 0
1
2
3
4
5
6
7
8
9
10
11
12
13
const a = '1';
const b = 1;
const c = [1, 2];
const d = [3, 4];

a + 1 // "11"
b + 1 // 2
b + '' // "1"
c + d // “1,23,4"

[] + {} // "[object Object]"
{} + [] // 0
-> [].toString() // “”
  • 在什麼狀況下會隱含地將值強制轉為布林呢?
    • if 述句中的條件判斷
    • for 述句中的條件判斷,意即測試運算式的第二個子句
    • while 與 do…while 中檢測條件是否成立的測試運算式
    • 三元運算式 條件 ? 值1 : 值2 中的條件運算,意即測試運算式的第一個子句
    • 邏輯運算子的 ||(or) 和 &&(and)左手邊的運算元會被當成測試運算式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = 12345;
var b = 'Hello World';
var c; // undefined
var d = null;

if (a) { // true
console.log('a 是真的'); // a 是真的
}

while (c) { // false
console.log('從來沒跑過');
}

c = d ? a : b;
console.log(c) // "Hello World"

if ((a && d) || c) { // true
console.log('結果是真的'); // 結果是真的
}

比較運算

  • 大家都知道
    • 兩個等號 ( == ) 比對兩邊物件時,JavaScript 會自動轉型,然後才進行比對
  • 怎麼自動轉型?
    • 它是透過 toPrimitive 的 valueOf() 或 toString() 轉換
    • 除此之外,可以自訂 valueOf() 或 toString()
  • 所以結果
    • 當 JavaScript 任意物件在進行 比較運算 時,都會先執行 valueOf() 或 toString() ,取回該物件相對應原始型別的值,看當下兩邊比較的是甚麼原始型別,然後再進行比較
  • 若有任一值轉型後的結果不是字串,就使用 Number 的規則轉為數字,來做數字上的比較

種類

相等比較

  • 可分為 ==(寬鬆相等)、===(嚴格相等)、!=(寬鬆不相等)、!==(嚴格不相等),主要差異是在做值的比較時是否會做強制轉型
  • == 和 === 其實都會做型別的檢查,只是當面對型別不同時的反應是不一樣
1
2
3
4
5
const a = '100';
const b = 100;

a == b // true,強制轉型,將字串 '100' 轉為數字 100
a === b // false
  • 簡略規則
    • 型別相同,就會以同一型做比較,但要注意
      • NaN 不等於自己(其實,NaN 不大於、不小於也不等於任何數字,所以當然也不等於它自己)
      • +0、-0 彼此相等
      • 物件(含 function 和 array)的相等是比較參考(reference),若參考相等才是相等
    • 型別不同,將其中一個或兩個值先做強制轉型,再用型別相同的做比較
      • 字串轉為數字
      • 布林轉為數字
      • null 與 undefined 在寬鬆相等下會強制轉型為彼此,因此是相等的但不等於其他值,
    • 若比較的對象是物件,使用 valueOf()(優先)或 toString() 將物件取得基本型別的值,再做比較
      • 而 != 和 !== 就是先分別做 == 和 === 再取否定(!)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
null === undefined // false
null == undefined // true

// 在 a == b 當中,字串 a 優先轉為數字後,此時就可比較 123 == 123,因此是相等的(true)
const a = '123';
const b = 123;

a === b // false
a == b // true

// 在 a == b 當中,布林 a 優先轉為數字(Numer(true) 得到 1)後,此時就可比較 1 == 123,因此是不相等的(false)
const a = true;
const b = 123;

a === b // false
a == b // false

// 在 a == b 當中其實比較的是 null == 123,因此是不相等的(false)
const a = null;
const b = 123;

a === b // false
a == b // false

//在 a == b 當中,陣列 a 由於沒有 valueOf(),只好使用 toString() 取得其基型值而得到字串 '1,2,3',此時就可比較 '1,2,3' == '1,2,3',因此是相等的(true)
const a = [1,2,3];
const b = '1,2,3';

a === b // false
a == b // true

大小比較

  • <(小於)、 >(大於)、<=(小於等於)、>=(大於等於)
  • 例如:a > b 表示比較 a 是否大於 b

簡略規則

  • 若兩個運算元皆為字串時,就直接依照字典字母順序做比較

注意

  • 由於規格只定義了 a < b 的演算法,因此 a > b 會以 b < a 的方式做比較
  • 由於沒有「嚴格關係比較」(===),所以一定會遇到強制轉型的狀況
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 由於 a 和 b 都不是字串且陣列沒有 valueOf,因此先用 toString 取得基型值,得到 a 為 '12'、b 為 '13',型別都是字串,接著做字母順序的比較
const a = [12];
const b = ['13'];

a < b // true,'12' < '13'
a > b // false,其實是比較 b < a,即 '13' < ’12'

// 先用 valueOf 取得基型值(只取到原來的物件),再用 toString 而得到兩個字串 [object Object],因此比較 [object Object] 與 [object Object]
// a == b 比較的是兩物件存值的所在的記憶體位置,也就是參考(reference)
const a = { b: 12 };
const b = { b: 13 };

a < b // false,'[object Object]' < '[object Object]'
a > b // false,其實是比較 b < a,即 '[object Object]' < '[object Object]'
a == b // false,其實是比較兩物件的 reference

a >= b // true
a <= b // true

例外

  • null 與 undefined 沒有其物件包裹形式,因此 Object(null) 與 Object(undefiend) 等同於 Object(),也就是空物件 {}
  • Number(NaN) 得到 NaN,且 NaN 不等於自己
1
2
3
4
5
6
7
8
9
10
11
var a = null;
var b = Object(a); // 等同於 Object()
a == b; // false

var c = undefined;
var d = Object(c); // 等同於 Object()
c == d; // false

var e = NaN;
var f = Object(e); // 等同於 new Number(e)
e == f;
  • object
    • 在 JavaScript 裡,所有的物件都是不相等的,每一個都是獨立的物件實體,即便實作了 valueOf 或 toString 方法,還是無法對使用者定義物件進行任何相等比較運算
  • 任何兩個物件相比都是 false
1
2
3
4
5
{} = {}   // {}

{} == {} // false

{} === {} // false
  • 兩個相同物件比較都是 true
1
2
3
4
5
6
7
8
let a = {}
a.name = {}

let b = {}
b.name = {}

a == b // false
a === b // true

運算子 || (or) 與 && (and)

  • 在兩個運算元當中「選擇」其中一個運算元的值作為結果
  • 簡略規則
    • ||(or) 和 &&(and)會將第一個運算元做布林測試或強制轉型為布林以便測試
    • 對 ||(or)來說,若結果為 true,則取第一個運算元為結果;若結果為 false,則取第二個運算元為結果
    • 對 &&(and)來說,若結果為 true,則取第二個運算元為結果;若結果為 false,則取第一個運算元為結果
  • 可應用於
    • ||(or) 可用來設定變數的初始值
    • &&(and)可用來執行「若特定條件成立,才做某件事情」,功能近似 if 述句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const a = 'Hello World!'
const b = 777;
const c = null;

a && c // 測試 a 為 true,選 c,結果是 null
a && b // 測試 a 為 true,選 b,結果是 777
undefined && b // 測試 undefined 為 false,選 undefined,結果是 undefined
a || b // 測試 a 為 true,選 a,結果是 "Hello World!"
c || 'foo' // 測試 c 為 false,選 'foo',結果是 "foo"

const flag = true;
function foo() {
console.log('try me');
}

flag && foo(); // try me

Symbol 強制轉型

  • 屬性名現在可以有兩種類型,一種是原來就有的字串,另一種就是新增的 Symbol 類型
  • 凡是屬性名屬於 Symbol 類型,可以保證不會與其他屬性名產生衝突
1
2
3
let a = 33
let a2 = Symbol(33)
console.log(a===a2) // false
  • 簡略規則
  • 在轉為 string 方面,將 Symbol 明確的強制轉型是允許的,但隱含的強制轉型是被禁止的,並且會丟出錯誤訊息
1
2
3
4
5
var s1 = Symbol('Hello World');
console.log(String(s1)); // "Symbol(Hello World)"

var s2 = Symbol(' World Hello');
console.log(s2 + ''); // TypeError: Cannot convert a Symbol value to a string
  • 在轉為 number 方面,無論是明確或隱含都是禁止的,並且會丟出錯誤訊息
1
2
3
4
5
const n1 = Symbol(777);
console.log(Number(s1)); // TypeError: Cannot convert a Symbol value to a number

const n2 = Symbol(999);
console.log(+n2); // TypeError: Cannot convert a Symbol value to a number
  • 在轉為 boolean 方面,無論是明確或隱含都是可以的,並且結果都是 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const b1 = Symbol(true);
const b2 = Symbol(false);
Boolean(b1); // true
Boolean(b2); // true

const b3 = Symbol(true);
const b4 = Symbol(false);

if (b3) {
console.log('b3 是真的'); // b3 是真的
}

if (b4) {
console.log('b4 是真的'); // b4 是真的
}
0%