JavaScript对象的toString()和valueof()

103 阅读6分钟

​(本文为本人在学习《JavaScript权威指南》时的心得体会,未必完全准确。)

​在使用JavaScript时,我们常常会遇到需要将对象数据类型(引用数据类型)转换为原始数据类型的情况。如在执行下面这段语句时,实际上就是先将对象转换为了原始数据类型,然后再进行是否相等(==)的判断。

const obj = {};
if (obj == 1) {	     // 判定为false
  console.log("相等");
}

​那么,JavaScript是如何将对象转换为基本数据类型的呢?JavaScript规范定义了对象到原始值的3种基本算法:

  • 偏字符串:该算法返回原始值,而且只要可能就返回字符串。
  • 偏数值:该算法返回原始值,而且只要可能就返回数值。
  • 无偏好:该算法不倾向于任何原始值类型,而是由类定义自己的转换规则。

​在介绍了上面3种转换方法之后,我们来了解一下JavaScript如何将对象转化为布尔值(Boolean)、字符串(String)以及数值(Number)。

1、对象转换为布尔值

​对于对象到布尔值的转换,我们只需要知道任何对象转换到布尔值都是true。这一过程不需要使用上面3种算法,而是直接适用于所有对象。值得注意的是,typeof null的结果虽然是"object",但是null实际并不属于引用数据类型,它是一个特殊值,表示某个值不存在,可以理解为”没有对象“。

2、对象转换为字符串/数值

​这里我们将对象转换为字符串和对象转换为数值的情况结合起来讲,是因为它们的执行过程具有一定的相似性。首先,我们从宏观的角度来理解转换的过程。结合上面提到的3种基本算法,当我们讲对象转换为字符串时,JavaScript会调用偏字符串算法将对象转为一个原始数据类型,然后将这个原始数据类型转换为字符串。同样地,当我们讲对象转换为数值时,JavaScript会调用偏数值算法将对象转为一个原始数据类型,然后将这个原始数据类型转换为数值。

​在介绍具体的转换过程之前,我们需要先了解两个Object原型上的方法——toString()valueOf()

  • toString() 方法返回对象的字符串表示。在默认情况下,toString()方法会返回一个默认值:
const obj = {a: 1, b: 2};
console.log(obj.toString());    // 输出:[object Object]

​然而,很多类都会对toString()方法进行重写。如Array类的toString()方法会将数组的每个元素转换为字符串,并将这些字符串用逗号作为分隔符拼接起来。Function类的toString()方法会将用户定义的函数转化为JavaScript源代码的字符串形式。

const arr = [1, 2, 3];
console.log(arr.toString());    // 输出:1,2,3

const fun = function () {};
console.log(fun.toString());    // 输出:function () {}
  • valueOf() 方法没有非常明确的定义,大体上可以认为它是把对象转换为代表对象的原始值(如果存在这么一个原始值的话)。这是由于对象是一种复合数据且大多数对象不能通过一个原始值去表示,因此valueOf()方法默认情况下只返回这个对象本身,而不是返回原始值。String、Number和Boolean这样的包装类定义的valueOf()方法也只是简单地返回被包装的原始值。Array、Function和RegExp则直接继承默认方法。而Date类则对valueOf()方法进行了改写,使其返回日期的内部表示形式
const str = 'hello';
console.log(str.valueOf());     // 输出:hello

const num = 100;
console.log(num.valueOf());     // 输出:100

const boo = false;
console.log(boo.valueOf());     // 输出:false

const arr = [1, 2, 3];
console.log(num.valueOf());     // 与console.log(arr)结果一致

const fun = function () {};
console.log(fun.valueOf());     // 与console.log(fun)结果一致

const date = new Date();		
console.log(date);              // Thu Aug 17 2023 16:50:17 GMT+0800 (中国标准时间)
console.log(date.valueOf());    // 1692262217778

​在解释了toString()和valueOf()两个方法之后,我们就可以对前面提到的3种对象到原始值的算法进行具体实现了。

  • 偏字符串算法会首先尝试toString()方法。如果这个方法有定义且返回原始值,则JavaScript使用该原始值(即使这个值不是字符串)。如果toString()不存在,或者返回结果是对象类型数据,则JavaScript会尝试valueOf()方法。如果这个方法存在且返回原始值,则JavaScript使用该值。否则,转换失败,报TypeError。
  • 偏数值算法与偏字符串算法类似。只不过先尝试valueOf()方法,再尝试toString()方法。
  • 无偏好算法取决于被转换的对象的类。如果是一个Date对象,则JavaScript会使用偏字符串算法。如果是其他类型的对象,则JavaScript使用偏数值算法。

​通过下面这个例子,我们可以更好地理解这一过程。

// 我们首先定义一个对象obj,我们在obj内部对toString()和valueOf()进行重写
const obj = {
  toString() {
    return "Hello World!";
  },
  valueOf() {
    return 1
  }
};
/*
 * Nunber()会将obj以偏数值算法的形式进行转换,会首先调用obj的valueOf()方法,
 * 该方法返回值为1,是原始值(数值),则直接输出1
 */
console.log(Number(obj));    // 输出:1
/*
 * String()会将obj以偏字符串算法的形式进行转换,会首先调用obj的toString()方法,
 * 该方法返回值为"Hello World!",是原始值(字符串),则直接输出"Hello World!"
 */
console.log(String(obj));    // 输出:Hello World!

​那么,回到我们开头提到的if(obj == 1)。为了进行相等判定,JavaScript首先会将obj通过偏数值算法转换成一个原始值。在这个例子中,为了实现偏数值算法的转换,JavaScript会首先尝试调用obj的valueOf(),但我们没有重写过obj的valueOf(),所以按照默认情况valueOf()会返回obj这个对象本身。很明显,obj本身并不是一个原始值,valueOf()的尝试失败了。进而,JavaScript会首先尝试调用obj的toString(),这里也是按照默认情况toString()会返回默认值,即"[object Object]"。显然返回的默认值与1是不相等的,所以判断结果为false,下面的输出语句无法执行。

const obj = {};
if (obj == 1) {	     // 判定为false
  console.log("相等");
}

​当我们对obj的valueOf()方法进行改写并强制令其返回值为1时,就可以使得判断结果为true并执行输出语句;

const obj = {
  valueOf() {
    return 1;
  }
};
if (obj == 1) {      // 判定为true
  console.log("相等");
}

面试题: 添加代码,使得下面的JavaScript代码能够正常输出文字。

if (a == 1 && a == 2 && a == 3) {
  console.log("hello,word");
}

​在上面的判断条件中,都是将a与数值类型进行相等判断,结合前文学习的知识,我们可以很快知道只需要定义一个对象类型的数据a,并重写它的valueOf()方法即可令上面的判断通过。在每次判断a == xxx的操作时,都会得到返回_a的值然后使_a自加1,那么每次的判断都可以通过。

const a = {
  _a: 1,     // 定义一个属性存放需要返回的数据
  valueOf() {
    return this._a++;
  },
}
if (a == 1 && a == 2 && a == 3) {   // 判定为true
  console.log("hello,word");
}