本文转载于 SegmentFault 社区
作者:Eno_Yao
当我第一次看到这一题目的时候,我是比较震惊的,分析了下很不合我们编程的常理,并认为不大可能,变量 a 要在同一情况下要同时等于 1,2 和 3 这三个值,这是天方夜谭吧,不亚于哥德巴赫
1+1=1
的猜想吧,不过一切皆有可能,出于好奇心,想了许久之后我还是决定尝试解决的办法。
我的思路来源于更早前遇到的另外一题相似的面试题:
// 设置一个函数输出一下的值
f(1) = 1;
f(1)(2) = 2;
f(1)(2)(3) = 6;
当时的解决办法是使用
toString
或者
valueOf
实现的,那我们先回顾下
toString
和
valueO
f
方法,方便我们更深入去了解这类型的问题:
比如我们有一个对象,在不重写
toString()
方法和
valueOf()
方法的情况下,在 Node 或者浏览器输出的结果是这样的
class Person {
constructor() {
this.name = name;
}
}
const best = new Person("Kobe");
console.log(best); // log: Person {name: "Kobe"}
console.log(best.toString()); // log: [object Object]
console.log(best.valueOf()); // log: Person {name: "Kobe"}
console.log(best + "GiGi"); // log: [object Object]GiGi
从上面的输出我们可以观察到一个细节,
toString()
输出的是
[object Object]
,而
valueOf()
输出的是
Person
对象本身,而当运算到
best + 'GiGi'
的时候竟然是输出了
[object Object]GiGi
,我们可以初步推断是对象调用的
toStrin
g()
方法得到的字符串进行计算的,难道是运算符
+
的鬼斧神工吗?
为了验证我们上一步的推断,我们稍微做一点改变,把
valueOf
方法进行一次复写:
class Person {
constructor(name) {
this.name = name;
}
// 复写 valueOf 方法
valueOf() {
return this.name;
}
}
这次跟上面只有一处产生了不一样的结果,那就是最后的
best + 'GiGi'
前后两次结果在复写了
valueOf()
方法之后发生了改变,从中我们可以看出来,对象的本质其实没有发生根本的改变,但是当它被用作直接运算的时候,它的值是从复写的
valueOf()
中获取的,并继续参与后续的运算。
我们发现
best + 'GiGi'
还是没有发生任何改变,还是使用我们上一次复写
val
ueOf()
的结果。
其实我们重写了
valueOf
方法,不是一定调用
valueOf()
的返回值进行计算的。而是
value
Of
返回的值是基本数据类型时才会按照此值进行计算,如果不是基本数据类型,则将使用
toString()
方法返回的值进行计算。
class Person {
constructor(name) {
this.name = name;
}
valueOf() {
return this.name;
}
toString() {
return `Bye ${this.name}`;
}
}
const best = new Person({ name: "Kobe" });
console.log(best); // log: Person name: {name: "Kobe"}
console.log(best.toString()); // log: Bye [object Object]
console.log(best.valueOf()); // log: Person {name: "Kobe"}
console.log(best + "GiGi"); // log: [object Object]10

看上面的例子,现在传入的
name
是一个对象
new Person({ name: "Kobe" })
,并不是基本数据类型,所以当执行加法运算的时候取
toString()
方法返回的值进行计算,当然如果没有
valueOf()
方法,就会去执行
toString()
方法。
所以铺垫了这么久,我们就要揭开答案,我们正是使用上面这些原理去解答这一题:
class A {
constructor(value) {
this.value = value;
}
toString() {
return this.value++;
}
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
这里就比较简单,直接改写
toString()
方法,由于没有
valueOf()
,当他做运算判断
a == 1
的时候会执行
toStrin
g()
的结果。
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 Eno!");
}
当然,你也可以不使用
toString
,换成
valueOf
也行,效果也是一样的:
class A {
constructor(value) {
this.value = value;
}
valueOf() {
return this.value++;
}
}
const a = new A(1);
console.log(a);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
所以,当一个对象在做运算的时候
(比如加减乘除,判断相等)
时候,往往会有
valueOf()
或者
toString
的调用问题,这个对象的变量背后通常隐藏着一个函数。
// 设置一个函数输出一下的值
f(1) = 1;
f(1)(2) = 2;
f(1)(2)(3) = 6;
function f() {
let args = [...arguments];
let add = function() {
args.push(...arguments);
return add;
};
add.toString = function() {
return args.reduce((a, b) => {
return a + b;
});
};
return add;
}
console.log(f(1)(2)(3)); // 6
当然还没有结束,这里还会有一些特别的解法,其实在使用对象的时候,如果对象是一个数组的话,那么上面的逻辑还是会成立,但此时的
toString()
会变成隐式调用
join()
方法,换句话说,对象中如果是数组,当你不重写其它的
toString()
方法,其默认实现就是调用数组的
join()
方法返回值作为
toString()
的返回值,所以这题又多了一个新的解法,就是在不复写
toString()
的前提下,复写
join()
方法,把它变成
shift()
方法,它能让数组的第一个元素从其中删除,并返回第一个元素的值。
class A extends Array {
join = this.shift;
}
const a = new A(1, 2, 3);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
我们的探寻之路还没结束,细心的同学会发现我们题目是如何让