除了 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 方法。为了追究真相,我们需要更严谨的实验。
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)
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