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 中,它是显式检查的类型转换。
*类型强制转换*是隐式类型转换:操作会自动将其参数转换为所需的类型。可以是已检查、未检查或介于两者之间。
[来源:维基百科]