ToPrimitive()
ToString()
和相關運算ToPropertyKey()
ToNumeric()
和相關運算+
)==
)在本章中,我們將探討類型強制轉換在 JavaScript 中的角色。我們將深入探討這個主題,例如探討 ECMAScript 規範如何處理強制轉換。
每個運算(函式、運算子等)都希望其參數具有特定的類型。如果值不具備參數的正確類型,函式等三種常見選項為:
函式可以擲回例外
函式可以傳回錯誤值
函式可以將其引數轉換為有用的值
在 (3) 中,運算執行隱式類型轉換。這稱為類型強制轉換。
JavaScript 最初沒有例外狀況,這就是它在大部分運算中使用強制轉換和錯誤值的原因
// Coercion
assert.equal(3 * true, 3);
// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);
不過,也有某些情況(特別是涉及較新的功能時)如果參數類型不正確,它會擲回例外狀況
存取 null
或 undefined
的屬性
使用符號
混合大整數和數字
呼叫不支援該運算的新建或函式呼叫值
變更唯讀屬性(僅在嚴格模式下擲回例外狀況)
處理強制轉換的兩種常見方式是
呼叫者可以明確轉換值,以便它們具有正確的類型。例如,在以下互動中,我們希望將兩個編碼為字串的數字相乘
呼叫者可以讓運算為它們進行轉換
我通常偏好前者,因為它能釐清我的意圖:我希望 x
和 y
不是數字,而是想將兩個數字相乘。
以下各節說明 ECMAScript 規格用於將實際參數轉換為預期類型的最重要的內部函式。
例如,在 TypeScript 中,我們會寫
在規格中,它看起來 如下(已轉換為 JavaScript,以便更容易理解)
每當預期原始類型或物件時,都會使用下列轉換函式
ToBoolean()
ToNumber()
ToBigInt()
ToString()
ToObject()
這些內部函式在 JavaScript 中有非常相似的類比
在與數字並存的大整數引入後,規格經常在以前使用 ToNumber()
的地方使用 ToNumeric()
。請繼續閱讀以了解更多資訊。
目前,JavaScript 有 兩種內建數值類型:數字和大整數。
ToNumeric()
傳回數值 num
。它的呼叫者通常會呼叫 num
規格類型的 mthd
方法
Type(num)::mthd(···)
在其他方法中,下列運算使用 ToNumeric
++
運算子*
運算子ToInteger(x)
用於在預期沒有小數的數字時。結果的範圍通常會在之後進一步限制。
ToNumber(x)
並移除小數(類似於 Math.trunc()
)。ToInteger()
的運算
Number.prototype.toString(radix?)
String.prototype.codePointAt(pos)
Array.prototype.slice(start, end)
ToInt32()
、ToUint32()
將數字強制轉換為 32 位元整數,並由位元運算子使用(請參閱表 1)。
ToInt32()
:有號,範圍 [−231, 231−1](包含限制)ToUint32()
:無號(因此為 U
),範圍 [0, 232−1](包含限制)運算子 | 左運算元 | 右運算元 | 結果類型 |
---|---|---|---|
<< |
ToInt32() |
ToUint32() |
Int32 |
有號 >> |
ToInt32() |
ToUint32() |
Int32 |
無號 >>> |
ToInt32() |
ToUint32() |
Uint32 |
& , ^ , | |
ToInt32() |
ToUint32() |
Int32 |
~ |
— | ToInt32() |
Int32 |
ToPropertyKey()
傳回字串或符號,並由下列使用:
[]
in
運算子的左側Object.defineProperty(_, P, _)
Object.fromEntries()
Object.getOwnPropertyDescriptor()
Object.prototype.hasOwnProperty()
Object.prototype.propertyIsEnumerable()
Reflect
的多種方法ToLength()
主要用於字串索引(直接)。
ToIndex()
的輔助函式l
的範圍:0 ≤ l
≤ 253−1ToIndex()
用於 Typed Array 索引。
ToLength()
的主要差異:如果參數超出範圍,會擲回例外狀況。i
的範圍:0 ≤ i
≤ 253−1ToUint32()
用於陣列索引。
i
的範圍:0 ≤ i
< 232−1(上限不包含,以留給 .length
)當我們設定 Typed Array 元素的值時,會使用下列轉換函式之一
ToInt8()
ToUint8()
ToUint8Clamp()
ToInt16()
ToUint16()
ToInt32()
ToUint32()
ToBigInt64()
ToBigUint64()
在本章節的其餘部分,我們會遇到多種規格演算法,但以 JavaScript「實作」。下列清單顯示一些常用的模式如何從規格轉換為 JavaScript
規格:如果 Type(value) 為字串
JavaScript:if (TypeOf(value) === 'string')
(非常寬鬆的轉換;TypeOf()
定義如下)
規格:如果 IsCallable(method) 為 true
JavaScript:if (IsCallable(method))
(IsCallable()
定義如下)
規格:設 numValue 為 ToNumber(value)
JavaScript:let numValue = Number(value)
規格:設 isArray 為 IsArray(O)
JavaScript:let isArray = Array.isArray(O)
規格:如果 O 有 [[NumberData]] 內部插槽
JavaScript:if ('__NumberData__' in O)
規格:設標籤為 Get(O, @@toStringTag)
JavaScript:let tag = O[Symbol.toStringTag]
規格:傳回字串串接「[object,標籤和「]」。
JavaScript:return '[object ' + tag + ']';
使用let
(而非const
)以符合規格語言。
有些內容已省略,例如 ReturnIfAbrupt 簡寫 ?
和 !
。
/**
* An improved version of typeof
*/
function TypeOf(value) {
const result = typeof value;
switch (result) {
case 'function':
return 'object';
case 'object':
if (value === null) {
return 'null';
} else {
return 'object';
}
default:
return result;
}
}
function IsCallable(x) {
return typeof x === 'function';
}
ToPrimitive()
運算式 ToPrimitive()
是許多強制轉換演算法的過渡步驟(其中一些我們會在本章稍後看到)。它將任意值轉換成原始值。
ToPrimitive()
在規格中經常使用,因為大多數運算子只能使用原始值。例如,我們可以使用加法運算子 (+
) 來加數字和串接字串,但我們無法使用它來串接陣列。
以下是 ToPrimitive()
的 JavaScript 版本
/**
* @param hint Which type is preferred for the result:
* string, number, or don’t care?
*/
function ToPrimitive(input: any,
hint: 'string'|'number'|'default' = 'default') {
if (TypeOf(input) === 'object') {
let exoticToPrim = input[Symbol.toPrimitive]; // (A)
if (exoticToPrim !== undefined) {
let result = exoticToPrim.call(input, hint);
if (TypeOf(result) !== 'object') {
return result;
}
throw new TypeError();
}
if (hint === 'default') {
hint = 'number';
}
return OrdinaryToPrimitive(input, hint);
} else {
// input is already primitive
return input;
}
}
ToPrimitive()
讓物件透過 Symbol.toPrimitive
(A 行)覆寫轉換成原始值。如果物件沒有這麼做,它會傳遞給 OrdinaryToPrimitive()
function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
let methodNames;
if (hint === 'string') {
methodNames = ['toString', 'valueOf'];
} else {
methodNames = ['valueOf', 'toString'];
}
for (let name of methodNames) {
let method = O[name];
if (IsCallable(method)) {
let result = method.call(O);
if (TypeOf(result) !== 'object') {
return result;
}
}
}
throw new TypeError();
}
ToPrimitive()
的呼叫者使用哪些提示?參數 hint
可以有三個值之一
'number'
表示:如果可能,input
應轉換成數字。'string'
表示:如果可能,input
應轉換成字串。'default'
表示:沒有偏好數字或字串。以下是各種運算式如何使用 ToPrimitive()
的幾個範例
hint === 'number'
。下列運算式偏好數字
ToNumeric()
ToNumber()
ToBigInt()
、BigInt()
<
)hint === 'string'
。下列運算式偏好字串
ToString()
ToPropertyKey()
hint === 'default'
。下列運算式對傳回原始值的類型保持中立
==
)+
)new Date(value)
(value
可以是數字或字串)正如我們所見,預設行為是將 'default'
視為 'number'
處理。只有 Symbol
和 Date
的執行個體會覆寫此行為(稍後說明)。
如果轉換為基本型別未透過 Symbol.toPrimitive
覆寫,OrdinaryToPrimitive()
會呼叫下列兩個方法中的任何一個或兩個
hint
指示我們希望基本值為字串,則會先呼叫 'toString'
。hint
指示我們希望基本值為數字,則會先呼叫 'valueOf'
。下列程式碼示範其運作方式
const obj = {
toString() { return 'a' },
valueOf() { return 1 },
};
// String() prefers strings:
assert.equal(String(obj), 'a');
// Number() prefers numbers:
assert.equal(Number(obj), 1);
具有屬性金鑰 Symbol.toPrimitive
的方法會覆寫正常的轉換為基本型別。標準函式庫中只執行過兩次
Symbol.prototype[Symbol.toPrimitive](hint)
Symbol
的執行個體,此方法會永遠傳回包裝的符號。Symbol
的執行個體具有會傳回字串的 .toString()
方法。但即使 hint
為 'string'
,也不應呼叫 .toString()
,以免我們意外將 Symbol
的執行個體轉換為字串(這是一種完全不同的屬性金鑰)。Date.prototype[Symbol.toPrimitive](hint)
Date.prototype[Symbol.toPrimitive]()
這是日期處理轉換為基本值的方式
Date.prototype[Symbol.toPrimitive] = function (
hint: 'default' | 'string' | 'number') {
let O = this;
if (TypeOf(O) !== 'object') {
throw new TypeError();
}
let tryFirst;
if (hint === 'string' || hint === 'default') {
tryFirst = 'string';
} else if (hint === 'number') {
tryFirst = 'number';
} else {
throw new TypeError();
}
return OrdinaryToPrimitive(O, tryFirst);
};
與預設演算法唯一的不同是 'default'
會變成 'string'
(而不是 'number'
)。如果我們使用將 hint
設定為 'default'
的運算,就能觀察到這一點
==
算子 會將物件轉換為基本型別(具有預設提示),如果另一個運算元是除了 undefined
、null
和 boolean
以外的基本值。在下列互動中,我們可以看到轉換日期的結果是字串
+
算子 會將兩個運算元轉換為基本型別(具有預設提示)。如果其中一個結果是字串,它會執行字串串接(否則會執行數字加法)。在下列互動中,我們可以看到轉換日期的結果是字串,因為算子傳回字串。
ToString()
和相關運算這是 JavaScript 版本的 ToString()
function ToString(argument) {
if (argument === undefined) {
return 'undefined';
} else if (argument === null) {
return 'null';
} else if (argument === true) {
return 'true';
} else if (argument === false) {
return 'false';
} else if (TypeOf(argument) === 'number') {
return Number.toString(argument);
} else if (TypeOf(argument) === 'string') {
return argument;
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
return BigInt.toString(argument);
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'string'); // (A)
return ToString(primValue);
}
}
請注意此函數如何使用 ToPrimitive()
作為物件的中間步驟,然後再將原始結果轉換為字串(A 行)。
ToString()
以有趣的方式偏離了 String()
的運作方式:如果 argument
是符號,前者會擲出 TypeError
,而後者則不會。這是為什麼?符號的預設值是將它們轉換為字串會擲出例外。
> const sym = Symbol('sym');
> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a string
此預設值會在 String()
和 Symbol.prototype.toString()
中被覆寫(兩個都說明於下一個小節中)
String()
function String(value) {
let s;
if (value === undefined) {
s = '';
} else {
if (new.target === undefined && TypeOf(value) === 'symbol') {
// This function was function-called and value is a symbol
return SymbolDescriptiveString(value);
}
s = ToString(value);
}
if (new.target === undefined) {
// This function was function-called
return s;
}
// This function was new-called
return StringCreate(s, new.target.prototype); // simplified!
}
String()
的運作方式會有所不同,視其是透過函數呼叫還是透過 new
呼叫。它使用 new.target
來區分這兩者。
這些是輔助函數 StringCreate()
和 SymbolDescriptiveString()
/**
* Creates a String instance that wraps `value`
* and has the given protoype.
*/
function StringCreate(value, prototype) {
// ···
}
function SymbolDescriptiveString(sym) {
assert.equal(TypeOf(sym), 'symbol');
let desc = sym.description;
if (desc === undefined) {
desc = '';
}
assert.equal(TypeOf(desc), 'string');
return 'Symbol('+desc+')';
}
Symbol.prototype.toString()
除了 String()
,我們也可以使用方法 .toString()
將符號轉換為字串。其規格如下。
Symbol.prototype.toString = function () {
let sym = thisSymbolValue(this);
return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
if (TypeOf(value) === 'symbol') {
return value;
}
if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
let s = value.__SymbolData__;
assert.equal(TypeOf(s), 'symbol');
return s;
}
}
Object.prototype.toString
.toString()
的預設規格如下
Object.prototype.toString = function () {
if (this === undefined) {
return '[object Undefined]';
}
if (this === null) {
return '[object Null]';
}
let O = ToObject(this);
let isArray = Array.isArray(O);
let builtinTag;
if (isArray) {
builtinTag = 'Array';
} else if ('__ParameterMap__' in O) {
builtinTag = 'Arguments';
} else if ('__Call__' in O) {
builtinTag = 'Function';
} else if ('__ErrorData__' in O) {
builtinTag = 'Error';
} else if ('__BooleanData__' in O) {
builtinTag = 'Boolean';
} else if ('__NumberData__' in O) {
builtinTag = 'Number';
} else if ('__StringData__' in O) {
builtinTag = 'String';
} else if ('__DateValue__' in O) {
builtinTag = 'Date';
} else if ('__RegExpMatcher__' in O) {
builtinTag = 'RegExp';
} else {
builtinTag = 'Object';
}
let tag = O[Symbol.toStringTag];
if (TypeOf(tag) !== 'string') {
tag = builtinTag;
}
return '[object ' + tag + ']';
};
如果我們將純粹物件轉換為字串,就會使用此運算。
預設情況下,如果我們將類別的實例轉換為字串,也會使用此運算。
通常,我們會覆寫 .toString()
以設定 MyClass
的字串表示,但我們也可以變更字串中方括號內「object
」之後的內容
class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
String(new MyClass()), '[object Custom!]');
將 .toString()
的覆寫版本與 Object.prototype
中的原始版本進行比較很有趣
> ['a', 'b'].toString()
'a,b'
> Object.prototype.toString.call(['a', 'b'])
'[object Array]'
> /^abc$/.toString()
'/^abc$/'
> Object.prototype.toString.call(/^abc$/)
'[object RegExp]'
ToPropertyKey()
ToPropertyKey()
會被方括號運算子等使用。以下是其運作方式
function ToPropertyKey(argument) {
let key = ToPrimitive(argument, 'string'); // (A)
if (TypeOf(key) === 'symbol') {
return key;
}
return ToString(key);
}
再次強調,物件會在使用原始資料之前轉換為原始資料。
ToNumeric()
和相關運算ToNumeric()
會被乘法運算子(*
)等使用。以下是其運作方式
function ToNumeric(value) {
let primValue = ToPrimitive(value, 'number');
if (TypeOf(primValue) === 'bigint') {
return primValue;
}
return ToNumber(primValue);
}
ToNumber()
ToNumber()
的運作方式如下
function ToNumber(argument) {
if (argument === undefined) {
return NaN;
} else if (argument === null) {
return +0;
} else if (argument === true) {
return 1;
} else if (argument === false) {
return +0;
} else if (TypeOf(argument) === 'number') {
return argument;
} else if (TypeOf(argument) === 'string') {
return parseTheString(argument); // not shown here
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
throw new TypeError();
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'number');
return ToNumber(primValue);
}
}
ToNumber()
的結構類似於 ToString()
的結構。
+
)以下是 JavaScript 加法運算子的規格
function Addition(leftHandSide, rightHandSide) {
let lprim = ToPrimitive(leftHandSide);
let rprim = ToPrimitive(rightHandSide);
if (TypeOf(lprim) === 'string' || TypeOf(rprim) === 'string') { // (A)
return ToString(lprim) + ToString(rprim);
}
let lnum = ToNumeric(lprim);
let rnum = ToNumeric(rprim);
if (TypeOf(lnum) !== TypeOf(rnum)) {
throw new TypeError();
}
let T = Type(lnum);
return T.add(lnum, rnum); // (B)
}
此演算法的步驟
Type()
會傳回 lnum
的 ECMAScript 規格類型。.add()
是 數字類型 的方法。==
)/** Loose equality (==) */
function abstractEqualityComparison(x, y) {
if (TypeOf(x) === TypeOf(y)) {
// Use strict equality (===)
return strictEqualityComparison(x, y);
}
// Comparing null with undefined
if (x === null && y === undefined) {
return true;
}
if (x === undefined && y === null) {
return true;
}
// Comparing a number and a string
if (TypeOf(x) === 'number' && TypeOf(y) === 'string') {
return abstractEqualityComparison(x, Number(y));
}
if (TypeOf(x) === 'string' && TypeOf(y) === 'number') {
return abstractEqualityComparison(Number(x), y);
}
// Comparing a bigint and a string
if (TypeOf(x) === 'bigint' && TypeOf(y) === 'string') {
let n = StringToBigInt(y);
if (Number.isNaN(n)) {
return false;
}
return abstractEqualityComparison(x, n);
}
if (TypeOf(x) === 'string' && TypeOf(y) === 'bigint') {
return abstractEqualityComparison(y, x);
}
// Comparing a boolean with a non-boolean
if (TypeOf(x) === 'boolean') {
return abstractEqualityComparison(Number(x), y);
}
if (TypeOf(y) === 'boolean') {
return abstractEqualityComparison(x, Number(y));
}
// Comparing an object with a primitive
// (other than undefined, null, a boolean)
if (['string', 'number', 'bigint', 'symbol'].includes(TypeOf(x))
&& TypeOf(y) === 'object') {
return abstractEqualityComparison(x, ToPrimitive(y));
}
if (TypeOf(x) === 'object'
&& ['string', 'number', 'bigint', 'symbol'].includes(TypeOf(y))) {
return abstractEqualityComparison(ToPrimitive(x), y);
}
// Comparing a bigint with a number
if ((TypeOf(x) === 'bigint' && TypeOf(y) === 'number')
|| (TypeOf(x) === 'number' && TypeOf(y) === 'bigint')) {
if ([NaN, +Infinity, -Infinity].includes(x)
|| [NaN, +Infinity, -Infinity].includes(y)) {
return false;
}
if (isSameMathematicalValue(x, y)) {
return true;
} else {
return false;
}
}
return false;
}
以下運算式在此未顯示
現在我們已經更深入地了解 JavaScript 的類型強制轉換如何運作,讓我們用一個與類型轉換相關的簡短詞彙表來作結
在類型轉換中,我們希望輸出值具有給定的類型。如果輸入值已經具有該類型,則會直接傳回不變更。否則,會轉換成具有所需類型的值。
明確類型轉換表示程式設計師使用運算(函式、運算子等)來觸發類型轉換。明確轉換可以是
類型轉換是什麼,取決於程式語言。例如,在 Java 中,它是明確已檢查類型轉換。
類型強制轉換是隱式類型轉換:運算會自動將其引數轉換成它需要的類型。可以是已檢查、未檢查或介於兩者之間。
[來源:維基百科]