先抛出3个问题:
undefined + 10
和undefined + "10"
分别等于多少?
- 输出
2 > {}
结果是多少?
- 几种方法让
a == 1 && a == 2 && a == 3
等于true
?
这些都是常见的面试题,看似五花八门,但其本质都是面试官在考我们JavaScript中的 隐式类型转换
!
基本类型值的隐式类型转换
首先要知道的是JS隐式类型转换发生在我们使用 + - * / == < >
这些运算符时,这些运算符只能操作基本数据类型,对于对象类型需要先调ToPrimitive算法转换成基本类型。 我们下面先说基本类型值的隐式类型转换规则
:
+
操作符两边至少有一个值是string类型时,都转为string;否则都转为number;
// 都转为string 1 -> "1"
console.log(1 + "23"); // "123"
// 都转为number,false -> 0
console.log(1 + false); // 1
// 都转为string,false -> "false"
console.log("1" + false); // "1false"
// 都转为number,true -> 1,false -> 0
console.log(true + false); // 1
- * /
操作符两边都转为number;
console.log(25 - "23"); // 2
console.log(1 * false); // 0
// Number("a")为NaN
console.log(1 / "a"); // NaN
==
操作符两边都转为number;
// true -> 0
console.log(2 == true); // false
// "0" -> 0, false -> 0
console.log("0" == false); // true
// "0" -> 0
console.log("0" == 0) ; // true
- 对于
> <
,全是字母比较按字母顺序,其余的都转换成number在比较;
// 按字母顺序g比f大
console.log("g" > "f"); // true
// 按字母顺序f比g小
console.log("fh" > "ga"); // false
// 转换成number,"12" -> 12
console.log("12" < 13); // true
// 转换成number,false -> 0
console.log(false < -1); // false
// 转换成number,"a" -> NaN, NaN做比较都是false
console.log("a" < 9999); // false
至此我们就可以回答第1个问题
了:
// 转换为number类型,undefined -> NaN
console.log(undefined + 10); // NaN
// 转换为string类型,undefined -> "undefined"
console.log(undefined + "10");
ToPrimitive算法
ToPrimitive,是JS中每个值隐含的自带的方法,他可以将任何类型的值转换为基本类型的值。这是一个内部算法,是JS在内部执行时遵循的一套规则。
既然这些运算符只能操作基本数据类型,那必然有能将引用类型转换成基本类型机制,这个机制就是 ToPrimitive
算法。通过此算法将值转换成基本类型后,再进行上节中的基本类型值的比较。
调用ToPrimitive函数时,会被传递一个字符串参数 hint
,表示要转换到的原始值的预期类型。 hint
参数的取值是 "number"
、"string"
和 "default"
中的任意一个。
-
当hint为numbr或default时:
- 调用Object的valueOf方法,如果为原始值,则直接返回;否则进行下一步;
- 调用Object的toString方法,如果为原始值,则直接返回;否则进行下一步;
- 抛出TypeError异常;
-
当hint为string时:
- 调用Object的toString方法,如果为原始值,则直接返回;否则进行下一步;
- 调用Object的valueOf方法,如果为原始值,则直接返回;否则进行下一步;
- 抛出TypeError异常;
那么hint参数是依据什么规则确定的呢? 有如下几点:
- 一般情况下,
+
连接运算符传入的参数是defaul; - 当是
- * / > < ==
操作符时,hint为number; - 对于
String(str)
和模版字符串${str}
等情况,传入的参数是string;
接下来声明一个对象,手动赋予 Symbol.toPrimitive
属性,再来查看输出结果:
var obj1 = {
[Symbol.toPrimitive](hint) {
if (hint == "number") {
return 10;
}
if (hint == "string") {
return "WEB实习僧";
}
if (hint == "default") {
return 1;
}
}
};
console.log(`Hi, ${obj1}`); // "Hi, WEB实习僧" hint参数为string
console.log(obj + 5); // 6 hint参数为default
console.log(obj - 5); // 5 hint参数为number
至此我们就可以回答第2个问题
了:
/**
* 1. {}不是基本数据类型,所以调用ToPrimitive
* 2. hint为number,首先调用valueOf方法,返回{}
* 3. {}还不是基本数据类型,调用toString()方法,返回"[object Object]"
* 4. "[object Object]"是基本类型,结束ToPrimitive
* 5. 比较2 > "[object Object]",需要都转换为number类型
* 6. Number("[object Object]")为NaN
* 7. 比较2 > NaN,返回false,结束
*/
console.log(2 > {}); // false
等等,好像第3个问题也能解决啦?每次进行 ==
比较时,都会调用toPrimitive,所以可以在它身上做手脚!
const a = {
i: 1,
// 当然替换成valueOf或toString都是可以的
[Symbol.toPrimitive]() {
return this.i++
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('WEB实习僧'); // WEB实习僧
}
toString与valueOf
toString()
:它的作用是返回一个反映这个对象的字符串;
valueOf()
:它的作用是返回它相应的原始值;
上面说到调用ToPrimitive方法后,会根据hint情况调用toString和valueOf方法来返回最终值,那么toString和valueOf是根据什么规则转化的呢? 下表展示了各种数据类型调用toString和valueOf方法后的输出值:
类型 | toString | valueOf |
---|---|---|
Object | 返回"[object ObjectName]",其中 ObjectName 是对象类型的名称。 | 对象本身。这是默认情况。 |
String | 返回 String 对象的值 | 字符串值。 |
Number | 返回数值的字符串表示。 | 数字值。 |
Boolean | 如果布尔值是true,则返回"true"。否则返回"false"。 | Boolean 值。 |
Array | 将 Array 的每个元素转换为字符串,并将它们依次连接起来,两个元素之间用英文逗号作为分隔符进行拼接。 | 数组本身 |
Date | 返回日期的文本表示。 | 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC |
Function | 返回如下格式的字符串, "function functionname() { [native code] }" | 函数本身。 |
下面看一些示例:
let a = { age: 18 };
let b = [1, 2, 3];
let c = new Date();
let d = '123';
console.log(a.toString()); // "[object Object]"
console.log(b.toString()); // "1,2,3"
console.log(c.valueOf()); // 1651984111386
console.log(d.valueOf()); // 123
关于问题3
当然,让 a == 1 && a == 2 && a == 3
为 true
还有很多解法,这里不做详细解释,答案或附在文章结尾。
对于 ==
有很多解法,但是对于===
呢,没有了隐式类型转换,还有没有可能等于true
? 我还没想出类~
对于问题3,更多解法如下:
// 方法1: 从vue3的响应式中获取灵感,使用Proxy
let a = new Proxy({ i: 1 }, {
get(target) {
return () => target.i++
}
})
// 方法2: 和方法1同理,也是vue2实现响应式的核心
let a = 1
Object.defineProperty(window, 'a', {
get() {
return a++
}
})
// 方法2: 使用with
let a = 1
with ({
get a() {
return i++
}
}) {
if (a == 1 && a == 2 && a == 3) {
console.log('MDN不建议使用with');
}
}