toString 与 valueOf

除了 null, undefined 以外,其他所有数据类型都有这两个方法。它们位于构造函数的原型上,用于解决 js 的值运算与显示的问题
valueOf 和 toString 几乎都是在出现操作符(+-*/==><)时才会被调用(隐式转换)。

toString

返回一个表示该对象的字符串,当对象表示为文本值或以期望的字符串方式被引用时,toString 方法被自动调用。
1. 调用查看
// Object
let a = {};
let a1 = { a1: 1 };
// Array
let b = [];
let b1 = [1, 2, 3];
// String
let c = "123";
// Function
let d = function () {
  console.log("fn");
};
// Number
let e = 1;
// Bigint
let f = 1000000000000000000000000n;
// Symbol
let g = Symbol(777);
// undefined
let h = undefined;
// null
let i = null;

console.log(a.toString()); // '[object Object]'
console.log(a1.toString()); // '[object Object]'
console.log(b.toString()); // ''
console.log(b1.toString()); // '1,2,3'
console.log(c.toString()); // '123'
console.log(d.toString()); // 'function(){ console.log('fn') }'
console.log(e.toString()); // '1'
console.log(f.toString()); // '1000000000000000000000000'
console.log(g.toString()); // 'Symbol(777)'
console.log(h.toString()); // Uncaught TypeError: Cannot read property 'toString' of undefined
console.log(i.toString()); // Uncaught TypeError: Cannot read property 'toString' of null
可以看到,对象被转换成了 [object Object],而数组则变成了内容以逗号分割的字符串,相当于 Array.join(',')
2. 自动调用的场景
使用操作符的时候,如果其中一边为对象,则会先调用valueOf方法将其转变为原始值类型,如果valueOf的结果还是复杂数据类型,会再调用 toSting 方法将其转为原始值(字符串),然后再进行操作。
let c = [1, 2, 3];
let d = { a: 2 };
Object.prototype.toString = function () {
  console.log("Object");
};
Array.prototype.toString = function () {
  console.log("Array");
  return this.join(","); // 返回toString的默认值(下面测试)
};
Number.prototype.toString = function () {
  console.log("Number");
};
String.prototype.toString = function () {
  console.log("String");
};
console.log(2 + 1); // 3
console.log("s"); // 's'
console.log("s" + 2); // 's2'
console.log(c < 2); // false        (一次 => 'Array')
console.log(c + c); // "1,2,31,2,3" (两次 => 'Array')
console.log(d > d); // false        (两次 => 'Object')
3. 重写 toString
class A {
  constructor(count) {
    this.count = count;
  }
  toString() {
    return "current count:" + this.count;
  }
}
let a = new A(100);

console.log(a); // A {count: 100}
console.log(a.toString()); // current count:100
console.log(a + 1); // current count:1001

valueOf

返回当前对象的原始值。
调用查看
// Object
let a = {};
let a1 = { a1: 1 };
// Array
let b = [];
let b1 = [1, 2, 3];
// String
let c = "123";
// Function
let d = function () {
  console.log("fn");
};
// Number
let e = 1;
// Bigint
let f = 1000000000000000000000000n;
// Symbol
let g = Symbol(777);
// undefined
let h = undefined;
// null
let i = null;

console.log(a.valueOf()); // {}
console.log(a1.valueOf()); // {a1: 1}
console.log(b.valueOf()); // []
console.log(b1.valueOf()); // [1, 2, 3]
console.log(c.valueOf()); // '123'
console.log(d.valueOf());
// ƒ () {
//   console.log("fn");
// }
console.log(e.valueOf()); // 1
console.log(f.valueOf()); // 1000000000000000000000000n
console.log(g.valueOf()); // Symbol(777)
console.log(h.valueOf()); // Uncaught TypeError: Cannot read property 'valueOf' of undefined
console.log(i.valueOf()); // Uncaught TypeError: Cannot read property 'valueOf' of null

区别

  • 共同点:在输出对象时会自动调用。
  • 不同点:默认返回值不同,且存在优先级关系。
二者并存的情况下,在数值运算中,优先调用了 valueOf,字符串运算中,优先调用了 toString。
class A {
  valueOf() {
    return 2;
  }
  toString() {
    return "hello world";
  }
}
let a = new A();

console.log(String(a)); // 'hello world'   => (toString)
console.log(Number(a)); // 2         => (valueOf)
console.log(a + "22"); // '222'     => (valueOf)
console.log(a == 2); // true      => (valueOf)
console.log(a === 2); // false     => (严格等于不会触发隐式转换)
可以看出,在转字符串时,往往会调用 toString 方法,而转为数值时会调用 valueOf
但其中的 a + '22' 很不和谐,字符串合拼应该是调用 toString 方法。为了追究真相,我们需要更严谨的实验。
  • 去掉 valueOf
class A {
  toString() {
    return "哈哈哈";
  }
}
let a = new A();

console.log(String(a)); // '哈哈哈'     => (toString)
console.log(Number(a)); // NaN         => (toString)
console.log(a + "22"); // '哈哈哈22'   => (toString)
console.log(a == 2); // false       => (toString)
  • 去掉 toString
class A {
  valueOf() {
    return 2;
  }
}
let a = new A();

Object.prototype.toString = null;

console.log(String(a)); // 2        => (valueOf)
console.log(Number(a)); // 2        => (valueOf)
console.log(a + "22"); // '222'    => (valueOf)
console.log(a == 2); // true     => (valueOf)

总结

valueOf 偏向于运算,toString 偏向于显示。
1. 在进行对象转换时,将优先调用 toString 方法,如若没有重写 toString,将调用 valueOf 方法;如果两个方法都没有重写,则按 Object 的 toString 输出。
2. 在进行强转字符串类型时,将优先调用 toString 方法,强转为数字时优先调用 valueOf。
3. 使用运算操作符的情况下,valueOf 的优先级高于 toString。

Symbol.toPrimitive

MDN:Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。
const object1 = {
  [Symbol.toPrimitive](hint) {
    if (hint === "number") {
      return 42;
    }
    return null;
  },
};

console.log(+object1); // 42
  • 该函数被调用时,会被传递一个字符串参数 hint ,表示要转换到的原始值的预期类型。 hint 参数的取值是 "number"、"string" 和 "default" 中的任意一个。
  • 作用:同 valueOf()和 toString()一样,但是优先级要高于这两者,下面的例子展示了 Symbol.toPrimitive 属性是如何干扰一个对象转换为原始值时输出的结果的。
// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"

// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    return true;
  },
};
console.log(+obj2); // 10      -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true"  -- hint 参数值是 "default"
调用查看
class A {
  constructor(count) {
    this.count = count;
  }
  valueOf() {
    return 2;
  }
  toString() {
    return "哈哈哈";
  }
  // 我在这里
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "Hello Libai";
    }
    return true;
  }
}

const a = new A(10);

console.log(`${a}`); // 'Hello Libai' => (hint == "string")
console.log(String(a)); // 'Hello Libai' => (hint == "string")
console.log(+a); // 10            => (hint == "number")
console.log(a * 20); // 200           => (hint == "number")
console.log(a / 20); // 0.5           => (hint == "number")
console.log(Number(a)); // 10            => (hint == "number")
console.log(a + "22"); // 'true22'      => (hint == "default")
console.log(a == 10); // false        => (hint == "default")

面试题

1. 如何让(a === 1 && a === 2 && a === 3)的值为 true?
class A {
  constructor(value) {
    this.value = value;
  }
  valueOf() {
    return this.value++;
  }
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
  console.log("Hi Libai!");
}
// 'Hi Libai!'
原因:双等号(==):会触发隐式类型转换,所以可以使用 valueOf 或者 toString 来实现。每次判断都会触发 valueOf 方法,同时让 value+1,使得下次判断成立。
全等(===):严格等于不会进行隐式转换,这里使用 Object.defineProperty 进行数据劫持
let value = 1;
Object.defineProperty(window, "a", {
  get() {
    return value++;
  },
});
if (a === 1 && a === 2 && a === 3) {
  console.log("Hi Libai!");
}
劫持全局 window 上面的 a,当 a 每一次做判断的时候都会触发 get 属性获取值,进行一次自增,最后会让全等式成立。
2. 实现一个无限累加函数
add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10

// 以此类推
function add(a) {
  function sum(b) { // 使用闭包
    a = b ? a + b : a; // 累加
    return sum;
  }
  sum.toString = function() { // 只在最后一次调用
    return a;
  }
  return sum; // 返回一个函数
}

add(1)              // 1
add(1)(2)           // 3
add(1)(2)(3)        // 6
add(1)(2)(3)(4)     // 10
  • add 函数内部定义 sum 函数并返回,实现连续调用
  • sum 函数形成了一个闭包,每次调用进行累加值,再返回当前函数 sum
  • add()每次都会返回一个函数 sum,直到最后一个没被调用,默认会触发 toString 方法,所以我们这里重写 toString 方法,并返回累计的最终值 a
  • add(10): 执行函数 add(10),返回了 sum 函数,注意这一次没有调用 sum,默认执行 sum.toString 方法。所以输出 10;
  • add(10)(20): 执行函数 add(10),返回 sum(此时 a 为 10),再执行 sum(20),此时 a 为 30,返回 sum,最后调用 sum.toString()输出 30。add(10)(20)...(n)依次类推。
3. 柯里化实现多参累加
add(1)(3, 4)(3, 5); // 16
add(2)(2)(3, 5); // 12
function add() {
  // 1 把所有参数转换成数组
  let args = Array.prototype.slice.call(arguments);
  // 2 再次调用add函数,传递合并当前与之前的参数
  let fn = function () {
    let arg_fn = Array.prototype.slice.call(arguments);
    return add.apply(null, args.concat(arg_fn));
  };
  // 3 最后默认调用,返回合并的值
  fn.toString = function () {
    return args.reduce(function (a, b) {
      return a + b;
    });
  };
  return fn;
}

// ES6写法
function add() {
  let args = [...arguments];
  let fn = function () {
    return add.apply(null, args.concat([...arguments]));
  };
  fn.toString = () => args.reduce((a, b) => a + b);
  return fn;
}
4. 为什么Number({}) 的结果是 NaN, Number([]) === 0?
转换过程
Number({})
// 分解
var a = {}
var b = a.valueOf() // b = {},复杂数据类型
var c = b.toString() // c = "[object Object]",简单数据类型(原始类型)
Number(c) // NaN

Number([])
// 分解
var a = []
var b = a.valueOf() // b = [],复杂数据类型
var c = b.toString() // c = "",简单数据类型(原始类型)
Number(c) // 0, 空字符串会被转为 0
posted @ 2021-05-07 23:52  Frank-Link  阅读(11)  评论(0编辑  收藏  举报