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
的属性
使用符号
混合 bigint 和数字
对不支持该操作的值进行 new 调用或函数调用
更改只读属性(仅在严格模式下抛出)
处理强制转换的两种常见方法是
调用者可以显式转换值,使其具有正确的类型。例如,在以下交互中,我们想将两个编码为字符串的数字相乘
调用者可以让操作为他们进行转换
我通常更喜欢前者,因为它阐明了我的意图:我希望 x
和 y
不是数字,而是想将两个数字相乘。
以下部分描述了 ECMAScript 规范使用的最重要的内部函数,用于将实际参数转换为预期类型。
例如,在 TypeScript 中,我们会这样写
在规范中,它看起来如下(翻译成 JavaScript,以便于理解)
每当需要原始类型或对象时,都会使用以下转换函数
ToBoolean()
ToNumber()
ToBigInt()
ToString()
ToObject()
这些内部函数在 JavaScript 中有非常相似的对应函数
在引入了与数字并存的 bigint 之后,规范通常使用 ToNumeric()
来代替以前使用的 ToNumber()
。继续阅读以了解更多信息。
目前,JavaScript 有两种内置数字类型:数字和 bigint。
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()
用于类型化数组索引。ToLength()
的主要区别:如果参数超出范围,则抛出异常。i
的范围:0 ≤ i
≤ 253−1ToUint32()
用于数组索引。i
的范围:0 ≤ i
< 232−1(排除上限,为 .length
留出空间)当我们设置类型化数组元素的值时,将使用以下转换函数之一
ToInt8()
ToUint8()
ToUint8Clamp()
ToInt16()
ToUint16()
ToInt32()
ToUint32()
ToBigInt64()
ToBigUint64()
在本章的其余部分,我们将遇到几种规范算法,但“实现”为 JavaScript。以下列表显示了如何将一些常用模式从规范转换为 JavaScript
规范:如果 Type(value) 是 String
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)
规范:令 tag 为 Get(O, @@toStringTag)
JavaScript:let tag = O[Symbol.toStringTag]
规范:返回 “[object ”、tag 和 “]” 的字符串连接。
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 中,它是显式检查的类型转换。
*类型强制转换*是隐式类型转换:操作会自动将其参数转换为所需的类型。可以是已检查、未检查或介于两者之间。
[来源:维基百科]