Javascript 中的 valueOf()与 toString()

在 Javascript 中,每个对象都有 valueOf ()和 toString ()方法。我本来以为每当调用字符串转换时都会调用 toString ()方法,但显然它被 valueOf ()超越了。

例如,代码

var x = {toString: function() {return "foo"; },
valueOf: function() {return 42; }};
window.console.log ("x="+x);
window.console.log ("x="+x.toString());

将打印

x=42
x=foo

在我看来,这是倒退。.例如,如果 x 是一个复数,我希望 valueOf ()给出它的大小,但是无论什么时候我想转换成字符串,我都需要类似于“ a + bi”的东西。而且我不希望在包含字符串的上下文中显式地调用 toString ()。

事情就是这样吗?

37365 次浏览

在我得到答案之前,还有一些细节:

var x = {
toString: function () { return "foo"; },
valueOf: function () { return 42; }
};


alert(x); // foo
"x=" + x; // "x=42"
x + "=x"; // "42=x"
x + "1"; // 421
x + 1; // 43
["x=", x].join(""); // "x=foo"

一般来说,toString功能是 没有“胜过”valueOf的。ECMAScript 标准实际上很好地回答了这个问题。每个对象都有一个按需计算的 [[DefaultValue]]属性。当请求此属性时,解释器还提供一个“提示”,说明它期望的值类型。如果提示是 String,则在 valueOf之前使用 toString。但是,如果提示是 Number,那么将首先使用 valueOf。请注意,如果只有一个选项存在,或者它返回一个非原语,它通常会调用另一个选项作为第二个选项。

即使第一个操作数是字符串值,+操作符也始终提供提示 Number。尽管它要求 x使用它的 Number表示,但是由于第一个操作数返回来自 [[DefaultValue]]的字符串,因此它执行字符串串联。

如果要确保为字符串串联调用 toString,请使用数组和 .join("")方法。

(ActionScript 3.0稍微修改了 +的行为。如果任何一个操作数是 String,它将把它当作一个字符串连接操作符,并在调用 [[DefaultValue]]时使用提示 String。因此,在 AS3中,这个示例生成“ foo,x = foo,foo = x,foo1,43,x = foo”。)

(“ x =”+ x)给出“ x = value”而不是“ x = tostring”的原因如下。在计算“ +”时,javascript 首先收集操作数的基元值,然后根据每个基元的类型决定是否应该应用加法或连接。

原来你是这么想的

a + b:
pa = ToPrimitive(a)
if(pa is string)
return concat(pa, ToString(b))
else
return add(pa, ToNumber(b))

这就是实际发生的情况

a + b:
pa = ToPrimitive(a)
pb = ToPrimitive(b)*
if(pa is string || pb is string)
return concat(ToString(pa), ToString(pb))
else
return add(ToNumber(pa), ToNumber(pb))

也就是说,toString 应用于 valueOf 的结果,而不是原始对象。

如需进一步参考,请参阅 ECMAScript 语言规范中的11.6.1 加法运算符(+)部分。


*When called in string context, ToPrimitive does invoke toString, but this is not the case here, because '+' doesn't enforce any type context.

TLDR

类型强制(或隐式类型转换)支持弱类型,并在整个 JavaScript 中使用。大多数运算符(严格相等运算符 ===!==除外)和值检查操作(例如。如果这些值的类型与操作不能立即兼容,则 if(value)...)将强制提供给它们的值。

用于强制值的精确机制取决于所计算的表达式。在问题中,使用的是 加法运算符

加法运算符将首先确保两个操作数都是原语,在本例中,这涉及到调用 valueOf方法。在此实例中不调用 toString方法,因为对象 x上重写的 valueOf方法返回一个基元值。

然后,因为问题中的一个操作数是字符串,所以 都有操作数被转换为字符串。这个过程使用抽象的内部操作 ToString(注意: 大写) ,并且不同于对象(或其原型链)上的 toString方法。

最后,将生成的字符串连接起来。

细节

在 JavaScript 中对应于每种语言类型的每个构造函数对象的原型(即。Number、 BigInt、字符串、布尔值、符号和对象) ,有两种方法: valueOftoString

The purpose of valueOf is to retrieve the primitive value associated with an object (if it has one). If an object does not have an underlying primitive value, then the object is simply returned.

如果针对基元调用 valueOf,则该基元将以正常方式自动装箱,并返回基础基元值。注意,对于字符串,基本原语值(即。返回的值)是字符串表示形式本身。

下面的代码显示了 valueOf方法从包装器对象返回底层的基元值,并且它显示了未修改的与基元不对应的对象实例如何没有基元值要返回,所以它们只是返回自己。

console.log(typeof new Boolean(true)) // 'object'
console.log(typeof new Boolean(true).valueOf()) // 'boolean'
console.log(({}).valueOf()) // {} (no primitive value to return)

另一方面,toString的用途是返回对象的字符串表示形式。

例如:

console.log({}.toString()) // '[object Object]'
console.log(new Number(1).toString()) // '1'

对于大多数操作,JavaScript 会悄悄地尝试将一个或多个操作数转换为所需的类型。选择此行为是为了使 JavaScript 更易于使用。JavaScript最初没有例外,这可能也在这个设计决策中起到了一定的作用。这种隐式类型转换称为类型强制,它是 JavaScript 松散(弱)类型系统的基础。此行为背后的复杂规则旨在将类型转换的复杂性转移到语言本身,并转移到代码之外。

在强制过程中,有两种转换模式可能发生:

  1. 将对象转换为基元(这可能涉及类型转换本身) ,以及
  2. 直接转换到特定类型实例,使用其中一种基本类型的构造函数对象(即。Number()Boolean()String()等)

Conversion To A Primitive

当试图将非原语类型转换为要操作的原语类型时,抽象操作 ToPrimitive会被调用,并带有一个可选的“提示”,即“数字”或“字符串”。如果省略该提示,则默认提示为“ number”(除非已经重写了 @@toPrimitive方法)。如果提示是“ string”,则首先尝试 toString,如果 toString没有返回原语,则第二次尝试 valueOf。反之亦然。提示取决于请求转换的操作。

加法运算符不提供提示,因此首先尝试 valueOf。减法运算符提供一个“数字”的提示,因此首先尝试 valueOf。在规范中,我能找到的唯一提示为“字符串”的情况是:

  1. Object#toString
  2. 抽象操作 ToPropertyKey,它将参数转换为可用作属性键的值

直接类型转换

每个操作符都有自己的完成操作的规则。加法运算符将首先使用 ToPrimitive来确保每个操作数都是原语; 然后,如果任何一个操作数都是字符串,它将故意在每个操作数上调用抽象操作 ToString,以传递我们期望的字符串串联行为。如果在 ToPrimitive步之后,两个操作数都不是字符串,则执行算术加法。

与加法不同,减法运算符没有重载行为,因此将在每个操作数上调用 toNumeric,并首先使用 ToPrimitive将它们转换为原语。

所以:

 1  +  1   //  2
'1' +  1   // '11'   Both already primitives, RHS converted to string, '1' + '1',   '11'
1  + [2]  // '12'   [2].valueOf() returns an object, so `toString` fallback is used, 1 + String([2]), '1' + '2', 12
1  + {}   // '1[object Object]'    {}.valueOf() is not a primitive, so toString fallback used, String(1) + String({}), '1' + '[object Object]', '1[object Object]'
2  - {}   // NaN    {}.valueOf() is not a primitive, so toString fallback used => 2 - Number('[object Object]'), NaN
+'a'       // NaN    `ToPrimitive` passed 'number' hint), Number('a'), NaN
+''        // 0      `ToPrimitive` passed 'number' hint), Number(''), 0
+'-1'      // -1     `ToPrimitive` passed 'number' hint), Number('-1'), -1
+{}        // NaN    `ToPrimitive` passed 'number' hint', `valueOf` returns an object, so falls back to `toString`, Number('[Object object]'), NaN
1 + 'a'   // '1a'    Both are primitives, one is a string, String(1) + 'a'
1 + {}    // '1[object Object]'    One primitive, one object, `ToPrimitive` passed no hint, meaning conversion to string will occur, one of the operands is now a string, String(1) + String({}), `1[object Object]`
[] + []    // ''     Two objects, `ToPrimitive` passed no hint, String([]) + String([]), '' (empty string)
1 - 'a'   // NaN    Both are primitives, one is a string, `ToPrimitive` passed 'number' hint, 1-Number('a'), 1-NaN, NaN
1 - {}    // NaN    One primitive, one is an object, `ToPrimitive` passed 'number' hint, `valueOf` returns object, so falls back to `toString`, 1-Number([object Object]), 1-NaN, NaN
[] - []    // 0      Two objects, `ToPrimitive` passed 'number' hint => `valueOf` returns array instance, so falls back to `toString`, Number('')-Number(''), 0-0, 0

注意,Date内部对象是唯一的,因为它是唯一覆盖默认 @@toPrimitive方法的内部对象,其中默认提示被假定为“ string”(而不是“ number”)。这样做的原因是,为了方便程序员,在默认情况下将 Date实例转换为可读的字符串,而不是将它们的数值转换为可读的字符串。可以使用 Symbol.toPrimitive在自己的对象中重写 @@toPrimitive

下面的网格显示了抽象相等运算符(==)(来源)的强制结果:

enter image description here

Addendum

Note that JavaScript coercion rules are designed to maintain internal consistency of behavior, and are not designed to meet your intuition (although they usually do).

因此,存在着违反直觉的边缘情况。例如,以下内容看起来违反直觉和前后不一致,但它是维护一致的内部行为的人为产物

!![] // true because Boolean([]) => true, [[Negation]](true) => false => [[Negation]](false) => true
[] == false // true because [].valueOf() => [], which is not primitive, fallback to [].toString() => '', Boolean('') => false, false === false => true

参见 还有在这里你不知道 JS