JavaScript中的valueOf()与toString()


115

在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()。

这就是它的样子吗?


6
您尝试过window.console.log (x);还是alert (x);
Li0liQ

5
它们分别给出“对象”和“ foo”。好玩的东西。
Brainjam 2010年

实际上,alert(x); 给出“ foo”,和window.console.log(x); 在Firebug中提供“ foo {}”,在Chrome控制台中提供整个对象。
Brainjam 2010年

在Firefox 33.0.2中alert(x)显示foowindow.console.log(x)显示Object { toString: x.toString(), valueOf: x.valueOf() }
John Sonderson 2014年

Answers:


106

(“ 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 加法运算符(+)


*在字符串上下文调用时,ToPrimitive 确实会调用toString,但这不是这种情况,因为'+'不会强制​​执行任何类型上下文。


3
“ actually”块中的条件语句不应该读为“ if(pa(是字符串&& pb是字符串)”吗?即“ &&”而不是“ ||” ?
Brainjam 2010年

3
该标准肯定会说“或”(请参阅​​链接)。
2010年

2
是的,这是完全正确的-在连接中,字符串优先于其他类型。如果任何一个操作数是一个字符串,则整个对象将被串联为一个字符串。好答案。
devios1 2012年

76

在我得到答案之前,这里有一些详细信息:

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,则toString在之前使用valueOf。但是,如果提示为NumbervalueOf则将首先使用。请注意,如果仅存在一个,或者它返回一个非基本类型,则通常将另一个作为第二选择。

+运营商一直提供的提示Number,即使第一个操作数是一个字符串值。尽管它要求x提供Number表示形式,但由于第一个操作数从返回一个字符串[[DefaultValue]],所以它进行字符串连接。

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

(ActionScript 3.0稍微修改了的行为+,但是。如果其中一个操作数是a String,它将将其视为字符串连接运算符,并String在调用时使用提示[[DefaultValue]]。因此,在AS3中,此示例产生“ foo,x = foo,foo = x,foo1、43,x = foo”。)


1
还要注意,如果valueOftoString返回非基本体,则它们将被忽略。如果都不存在,或者都不返回原语,则TypeError抛出a。
樱桃

1
谢谢bcherry,这就是我希望得到的答案的能力。但是不应该x +“ x =”; 产生“ 42x =”?和x +“ 1”; 产量421?另外,您是否有ECMAScript标准相关部分的URL?
Brainjam 2010年

2
实际上,“ +”不使用提示(请参阅$ 11.6.1),因此ToPrimitive invokes [[DefaultValue]](no-hint)等效于[[DefaultValue]](number)
2010年

9
内置Date类似乎并非如此。("" + new Date(0)) === new Date(0).toString()。当Date对象toString()添加到某些对象时,它似乎总是返回其值。
kpozin 2012年

7
+1&Thx!我找到了您的博客文章,您在其中详细说明了此答案,并希望在此处链接/共享。这对答案的确很有帮助(包括Dmitry A. Soshnikov的评论)。
GitaarLAB

1

TLDR

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

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

加法运算符将首先确保两个操作数都是基元,在这种情况下,这涉及调用valueOf方法。toString在这种情况下不会调用该方法,因为valueOf对象上的重写方法x返回了原始值。

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

最后,将结果字符串串联在一起。

细节

在与JavaScript中每种语言类型(即Number,BigInt,String,Boolean,Symbol和Object)相对应的每个构造函数对象的原型上,有两种方法:valueOftoString

的目的valueOf是检索与对象(如果有)的原始值。如果对象不具有基础原始值,则仅返回该对象。

如果valueOf是针对基元调用的,则该基元将以常规方式自动装箱,并返回基础基元值。请注意,对于字符串,基础原始值(即所返回的值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()等等)

转换为原始

当尝试将非基本类型转换为要操作的基本类型时,将ToPrimitive使用“数字”或“字符串”的可选“提示”来调用抽象操作。如果省略了提示,则默认提示为“数字”(除非该@@toPrimitive方法已被覆盖)。如果提示为“字符串”,则toString首先尝试,valueOf如果toString未返回原语,则第二次尝试。反之亦然。提示取决于请求转换的操作。

加法运算符不提供任何提示,因此请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方法的唯一内部对象,在默认方法中,默认提示被假定为“字符串”(而不是“数字”)。这样做的原因是Date,为了程序员的方便,默认情况下将实例转换为可读字符串,而不是其数值。您可以使用覆盖@@toPrimitive自己的对象Symbol.toPrimitive

以下网格显示了抽象等式运算符(==)(source)的强制结果:

在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.