介紹
上一篇講了 JavaScript 中的型別和值,本篇主要關於 型別轉換、變數。
需要注意包裝物件、引用型別變更、物件到原始值的型別轉換規則等。
運算子相關的隱式型別轉換請檢視:
自我提問
- 字串、數字等原始值為什麼可以使用 . 或 [] 讀取屬性?
- 引用型別、非引用型別到底有什麼區別?
- 物件是怎麼轉變為原始值的?
- 如何進行顯示型別轉換?
腦圖
關鍵知識點
包裝物件
犀牛書這裡提到了一個 包裝物件 的概念(見 3.6 ),使用包裝物件的概念解釋了 為什麼字串、數字等原始值也可以擁有各自的屬性。
當 JavaScript 要讀取字串 s 的屬性時,將會通過呼叫 new String(s) 來 將字串轉換為物件(也相當於呼叫 Object(s) ,對原始值使用 . 操作符讀取屬性會將原始值進行隱式型別轉換,轉換為物件),然後引用這個物件的屬性,而使用結束後這個物件就會被 銷燬(實現不一定要建立和銷燬,但是可以用來類比這個過程)。
通過包裝物件的概念可以很容易理解字串和數字等為什麼有屬性,為什麼無法給它們的屬性賦值。
var s = 'string';
// 0
console.log(s.indexOf('s'));
// true
console.log(s.__proto__ === String.prototype);
複製程式碼
上述程式碼可以看做為
var s = 'string';
console.log(new String(s).indexOf('s'));
複製程式碼
var s = 'string';
// 可想象為賦值在包裝物件上,然而這段語句執行完成包裝物件就被銷燬了,所以 s 上找不到 test
s.test = 'test';
// undefined
console.log(s.test);
複製程式碼
不可變的原始值
原始值都是不可變更的,當為原始值的變數重新賦值時,修改的是新的值。
可以理解為每個原始值都是一個獨立的地址,把一個原始值賦值給一個變數會在記憶體中重新開闢一個空間賦值原始值並把新的地址賦值給這個變數。
var a = 1; // 開闢棧空間 0001 -> 把空間地址賦給變數 a -> 存值 1 到 0001
var b = a; // 開闢棧空間 0002 -> 把空間地址賦給變數 b -> 複製 a 的值 1 -> 0002
// 1 1
console.log(b, a); // b 和 a 分別去 0002 和 0001 查詢儲存值 得出 1 1
b = 2; // 取出 b 的地址 0002 -> 存值 2 到 0002
// 2 1
console.log(b, a); // b 和 a 分別去 0002 和 0001 查詢儲存值 得出 2 1
複製程式碼
可變的物件
物件型別是可變的,將一個物件型別賦值給一個變數時,實際上 傳遞的是它的引用(指標)。
var a = {}; // 開闢棧空間 0001 -> 將空間地址賦給變數 a -> 開闢堆空間儲存 X0001 -> 存值 {} -> 儲存 X0001 地址到 0001
var b = a; // 開闢棧空間 0002 -> 將空間地址賦給變數 b -> 賦值 0001 中的地址,也就是 X0001 到 0002
// {} {}
console.log(b, a); // b 和 a 分別去 0002 和 0001 查詢儲存值,查出是堆地址時會去堆中取值,得出 {} {}
b.c = 2; // 讀取 b 從堆中取值,修改堆中值為 {c: 2}
// {c:2} {c:2}
console.log(b, a); // b 和 a 分別去 0002 和 0001 查詢儲存值,查處是堆地址時會去堆中取值,得出 {c: 2} {c: 2}
複製程式碼
== 和 ===
== 運算子在判斷時會進行 型別轉換,=== 運算子 不會做任何轉換。
顯式型別轉換
通常可以使用全域性函式 Object、Number、Boolean、String 來進行顯式型別轉換。
然而我們也可以 藉助隱式型別轉換做顯示型別轉換。(隱式型別轉換見上一篇)
console.log(+'0'); // 0 通過 + 運算子的隱式轉換將字串顯式轉換為數字
複製程式碼
null 和 undefined 的物件型別轉換
注意,使用 Object 對 null、undefined 進行顯示型別轉換時,可以正常輸出一個空物件而不報錯,相當於沒傳入值。但是當 JavaScript 嘗試對它們轉換為物件型別時,將會丟擲型別錯誤(比如 null.toString 相當於將 null 轉為物件然後呼叫他的 toString,然而這將會丟擲型別錯誤)。
物件到原始值的轉換
物件到原始值的轉換相對而言比較複雜。
-
所有的物件轉換為布林型 都為 true。
-
物件轉換為字串時,將會經過幾個步驟:
- 如果物件有 toString 方法,並且呼叫後返回原始值跳到第 3 步,否則第 2 步。
- 如果物件有 valueOf 方法,並且呼叫後返回原始值跳到第 3 步,否則第 4 步。
- 將通過上述方法返回的原始值轉換為字串。
- 得不到原始值,丟擲型別錯誤。
使用程式碼測試一下
const a = { toString: function() { console.log('toString is called'); return null; } }; console.log(String(a)); // 'toString is called' // 'null' const b = { toString: function() { console.log('toString is called'); return {}; }, valueOf: function() { console.log('valueOf is called'); return false; } }; console.log(String(b)); // 'toString is called' // 'valueOf is called' // 'false' const c = { toString: function() { console.log('toString is called'); return {}; }, valueOf: function() { console.log('valueOf is called'); return {}; } }; console.log(String(c)); // 'toString is called' // 'valueOf is called' // TypeError: Cannot convert object to primitive value 複製程式碼
使用程式碼模擬一下
const isPrimitive = v => v == null || v === true || v === false || typeof v === 'number' || typeof v === 'string'; const obj2Str = obj => { let primitiveValue; if ('toString' in obj) { primitiveValue = obj.toString(); } else if(!('valueOf' in obj)) { throw new TypeError("Can't find toString and valueOf"); } if ('valueOf' in obj && (!('toString' in obj) || !isPrimitive(primitiveValue))) { primitiveValue = obj.valueOf(); } if (isPrimitive(primitiveValue)) { return String(primitiveValue); } else { throw new TypeError("Can't convert to primitive"); } }; 複製程式碼
可以拿上面的測試程式碼驗證一下是不是一樣的輸出
-
物件轉換為數字時,將會經過幾個步驟:(類似字串的轉換,但是有所差別)。
- 如果物件有 valueOf 方法,並且呼叫後返回原始值跳到第 3 步,否則第 2 步。
- 如果物件有 toString 方法,並且呼叫後返回原始值跳到第 3 步,否則第 4 步。
- 將通過上述方法返回的原始值轉換為字串。
- 得不到原始值,丟擲型別錯誤。
同樣使用程式碼測試一下
const a = { valueOf: function() { console.log('valueOf is called'); return null; } }; console.log(Number(a)); // 'valueOf is called' // 0 const b = { valueOf: function() { console.log('valueOf is called'); return {}; }, toString: function() { console.log('toString is called'); return true; } }; console.log(Number(b)); // 'valueOf is called' // 'toString is called' // 1 const c = { valueOf: function() { console.log('valueOf is called'); return {}; }, toString: function() { console.log('toString is called'); return {}; } }; console.log(Number(c)); // 'valueOf is called' // 'toString is called' // TypeError: Cannot convert object to primitive value 複製程式碼
同樣使用程式碼模擬一下
const isPrimitive = v => v == null || v === true || v === false || typeof v === 'number' || typeof v === 'string'; const obj2Num = obj => { let primitiveValue; if ('valueOf' in obj) { primitiveValue = obj.valueOf(); } else if(!('toString' in obj)) { throw new TypeError("Can't find toString and valueOf"); } if ('toString' in obj && (!('valueOf' in obj) || !isPrimitive(primitiveValue))) { primitiveValue = obj.toString(); } if (isPrimitive(primitiveValue)) { return Number(primitiveValue); } else { throw new TypeError("Can't convert to primitive"); } }; 複製程式碼
-
在使用 +、==、關係運算子(>、< 等)操作物件時,將會將物件轉換為原始值,對於 非日期物件,轉換套用上述的數字的轉換方式(先 valueOf 後 toString),而對於 日期物件,轉換套用上述的字串的轉換方式(先 toString 後 valueOf),但是 不會執行最後的變為數字或字串的型別轉換部分。
使用程式碼模擬一下非日期物件的轉換
const isPrimitive = v => v == null || v === true || v === false || typeof v === 'number' || typeof v === 'string'; const obj2Primitive = obj => { let primitiveValue; if ('valueOf' in obj) { primitiveValue = obj.valueOf(); } else if(!('toString' in obj)) { throw new TypeError("Can't find toString and valueOf"); } if ('toString' in obj && (!('valueOf' in obj) || !isPrimitive(primitiveValue))) { primitiveValue = obj.toString(); } if (isPrimitive(primitiveValue)) { return primitiveValue; } else { throw new TypeError("Can't convert to primitive"); } }; 複製程式碼
+、== 會在進行上述轉換後再次根據運算元進行二次判斷,並再次將得到的原始值進行轉換。
null 和 undefined 在進行 == 運算時將不會進行型別轉換,JavaScript 將會直接判斷另一個運算元是否為 null 或者 undefined 然後直接返回結果
-
剩餘的其它操作符轉換型別比較明確,如 - 會將兩個運算元都轉換為數字(套用到轉換為數字的轉換規則)
var 重複宣告和宣告提升
使用 var 對變數進行 重複宣告是無害的,因為所有的變數宣告都會被提升到程式碼執行的頂端(宣告實質上會在程式碼編譯時執行),所以宣告 1 ~ n 次的效果都是一樣的。
作用域鏈
函式在 定義時 就會繫結一個作用域鏈(詞法作用域、靜態作用域相關),如:函式定義區域的作用域 -> 上層函式定義區域的作用域 -> ... -> 全域性作用域。
函式在執行時作用域鏈的頂端將會加入它自身的函式作用域(引數、區域性變數)。變數的查詢將會隨著作用域鏈一直查詢直到找到或者到全域性作用域仍未找到報錯為止。
擴充套件
之前有道題目是什麼情況下 a == 1 && a == 2 為 true,看到上面的物件到原始值的轉換,應該就能理解了,其實很簡單,藉助 == 操作符的隱式型別轉換就可以做到。
const a = {
value: 1,
valueOf: function() {
return this.value++;
}
};
// true
console.log(a == 1 && a == 2);
複製程式碼
補充內容
型別轉換表
注意物件到原始值的轉換指的是沒有對物件的 toString 和 valueOf 做過變更時的結果。
認真看了上面的轉換規則的肯定能看出來,為啥 [] 轉換為數字會變成 0 呢,因為先呼叫 valueOf 然而陣列預設的 valueOf 就是它自身,不是原始值,所以只能呼叫 toString,得到空字串,然後再轉成數字,就變成 0 了。
值 | 字串 | 數字 | 布林 | 物件 |
---|---|---|---|---|
undefined | 'undefined' | NaN | false | throws TypeError |
null | 'null' | 0 | false | throws TypeError |
true | 'true' | 1 | new Boolean(true) | |
false | 'false' | 0 | new Boolean(false) | |
'' | 0 | false | new String('') | |
' ' | 0 | true | new String(' ') | |
'1.2' | 1.2 | true | new String('1.2') | |
'one' | NaN | true | new String('one') | |
0 | '0' | false | new Number(0) | |
-0 | '0' | false | new Number(-0) | |
NaN | 'NaN' | false | new Number(NaN) | |
Infinity | 'Infinity' | true | new Number(Infinity) | |
-Infinity | '-Infinity' | true | new Number(-Infinity) | |
1.2 | '1.2' | true | new Number(1.2) | |
{} | '[object Object]' | NaN | true | |
[] | '' | 0 | true |