隐式调用toString和valueOf的场景,你认为对吗
前言
valueOf
和 toString
很少出现在我们的代码里,但是他们是存在隐式调用的,所以实际上我们调用它的次数是很多的。这里从我所理解的角度来分析大部分隐式调用的场景的执行流程。
1. +
运算符
存在+
运算符的场景又可以分为两种情况
1.1 左右两边都是基本类型
这里还需要分为两种情况:
- 如果存在任意一方是字符串的话,那么不是字符串的一方(x)调用String(x)来转为字符串,再进行字符串的拼接。
- 如果左右两边都不是字符串的话,那么左右两边(x,y)调用Number(x)+Number(y)转化为数字,再进行数字的加法。(
Number(undefined)
的值是NaN)
这里还需要提到的两个点:
1.bigint类型只能与string类型或者bigint类型进行+运算,否则会报错
2.symbol类型不能与七种任意基本类型进行+运算,否则会报错
测试代码如下:
const _getType = (val) => Object.prototype.toString.call(val).slice(8, -1);
const arr = ["1", 1, true, undefined, null, 1n, Symbol("1")];
let desc = "";
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
try {
desc = `${_getType(arr[i])}类型:${arr[i]} 与 ${_getType(arr[j])}类型:${arr[j]} 相加的结果:`;
} catch (e) {
desc = `${_getType(arr[i])}类型 与 ${_getType(arr[j])}类型 相加的结果:`;
}
try {
desc += (arr[i] + arr[j]);
} catch (e) {
desc += e.message;
}
console.log(desc);
}
console.log("\n");
}
运行结果如下:
1.2 存在任意一边是对象
如果存在任意一边是对象的话,那么会先调用对象的valueOf
函数,如果valueOf
函数返回的值是基本类型,那么按照基本类型来进行比较
如果valueOf
返回的是对象,那么再次调用toString
函数,如果toString
函数返回的值是基本类型,那么按照基本类型来进行比较,如果还不是基本类型,那么报错(不能转化为原始值-Cannot convert object to primitive value
)
测试代码
const _getType = (val) => Object.prototype.toString.call(val).slice(8, -1);
let test1 = {
name: "test1",
toString() {
return "test1";
},
valueOf() {
return 1;
},
};
let test2 = {
name: "test1",
toString() {
return "test2";
},
valueOf() {
return 2;
},
};
let test3 = {
name: "test1",
toString() {
return {};
},
valueOf() {
return {};
},
};
const arr = [test1, "1", 1, true, undefined, null, 1n, Symbol("1")];
let desc = "";
let left = test2;
let right;
for (let j = 0; j < arr.length; j++) {
right = arr[j];
try {
desc = `${_getType(left)}类型:${left} 与 ${_getType(right)}类型:${right} 相加的结果:`;
} catch (e) {
desc = `${_getType(left)}类型 与 ${_getType(right)}类型 相加的结果:`;
}
try {
desc += (left + right);
} catch (e) {
desc += e.message;
}
console.log(desc);
}
console.log(test3+'1',"test3+'1'")
运行结果如下:
这里可以看到上述运算都是调用valueOf
方法,最后一行代码时test3
无法转化为原始值,报错。
这里还意外发现了一个点:模板字符串和字符串的拼接是不一样的,模板字符串是调用对象的toString
,而字符串的拼接是调用对象的valueOf
。
2. ==
运算符
这里只讨论两个等号的运算符,因为三个等号是不存在隐式转换的,没有讨论的意义。
将==
运算符的场景分为三种
2.1. 都是基本类型
1)如果类型一致,直接比较值(Symbol类型比较原始值)
2)如果类型不一致
2.1 undefined只会和null相等(以下不再讨论undefined、null类型)
2.2 Symbol类型只会和Symbol类型相等(不再讨论Symbol类型)
2.3 bigint类型与number类型表现的行为一致(不再讨论bigint类型,只剩下string、boolean、number三种类型)
2.4 只要存在boolean类型,就将boolean值转为数字(只剩下数字和字符串的情况)
2.5 数字和字符串的情况下,将字符串转为数字,进行数字和数字的比较 测试代码:
const _getType = (val) => Object.prototype.toString.call(val).slice(8, -1);
const arr = ["1", 1, true, undefined, null, 1n, Symbol("1")];
let desc = "";
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
try {
desc = `${_getType(arr[i])}类型:${arr[i]} 与 ${_getType(arr[j])}类型:${arr[j]} == 的结果:`;
} catch (e) {
desc = `${_getType(arr[i])}类型 与 ${_getType(arr[j])}类型 == 的结果:`;
}
try {
desc += (arr[i] == arr[j]);
} catch (e) {
desc += e.message;
}
console.log(desc);
}
console.log("\n");
}
测试结果:
2.2. 一个基本类型、一个引用类型
如果基本类型的值是null或者undefined,那么直接返回false,不会将对象进行隐式转换。
如果基本类型的值不是null或者undefined,首先会调用对象的valueOf
函数,如果函数返回的值是基本类型,那么进行基本类型的比较,如果不是基本类型,那么再次toString
函数,如果函数返回的值是基本类型,那么进行基本类型的比较,如果还不是基本类型,那么报错(不能转化为原始值-Cannot convert object to primitive value
)
const _getType = (val) => Object.prototype.toString.call(val).slice(8, -1);
let test1 = {
name: "test1",
toString() {
return "test1";
},
valueOf() {
return 1;
},
};
const arr = ["1", 1, true, undefined, null, 1n, Symbol("1")];
let desc = "";
let left = test1;
let right;
for (let j = 0; j < arr.length; j++) {
right = arr[j];
try {
desc = `${_getType(left)}类型:${left} 与 ${_getType(right)}类型:${right} == 的结果:`;
} catch (e) {
desc = `${_getType(left)}类型 与 ${_getType(right)}类型 == 的结果:`;
}
try {
desc += (left == right);
} catch (e) {
desc += e.message;
}
console.log(desc);
}
如果把test1换成
let test1 = {
name: "test1",
toString() {
return undefined;
},
valueOf() {
return undefined;
},
};
那么还是不会和undefined、null相等(直接就返回false了)
2.3.都是引用类型
这个简单,直接判断是否指向了同一个引用对象。
3. 其他运算符
对于其他运算符(-、*、/、>、<
),简单的概括为,如果是基本类型x,调用Number(x)
转为数字,如果是对象,那么按照valueOf
,toString
的顺序进行调用,转为基本类型。
需要注意的点是:
-
Symbol类型不能隐式转为数字,所以上述运算都不能参与
-
bigint类型只能参与比较(>、<),而不能参与运算(-、*、/)
测试代码:
const _getType = (val) => Object.prototype.toString.call(val).slice(8, -1);
let test1 = {
name: "test1",
toString() {
return "test1";
},
valueOf() {
return 1;
},
};
let test2 = {
name: "test1",
toString() {
return "test2";
},
valueOf() {
return 2;
},
};
let test3 = {
name: "test1",
toString() {
return {};
},
valueOf() {
return {};
},
};
const arr = [test1, "1", 1, true, undefined, null, 1n, Symbol("1")];
let desc = "";
let left = test2;
let right;
let operate = ">";
for (let j = 0; j < arr.length; j++) {
right = arr[j];
try {
desc = `${_getType(left)}类型:${left} 与 ${_getType(right)}类型:${right} ${operate} 的结果:`;
} catch (e) {
desc = `${_getType(left)}类型 与 ${_getType(right)}类型 ${operate} 的结果:`;
}
try {
desc += (left > right);
} catch (e) {
desc += e.message;
}
console.log(desc);
}
console.log(test3 + "1", "test3+'1'");
测试结果:
4. 其他场景
4.1 使用alert调用
- 如果参数是基本类型则会隐式调用
String()
转为字符串,这里还是存在Symbol类型无法隐式转换的问题。 - 如果参数是引用类型则会按照
toString
、valueOf
的顺序转为基本类型,再转为字符串。
4.2 一个对象作为另一个对象的key
这是一道面试题中出现的知识点。
let obj1 = {name: "1"};
let obj2 = {name: "2"};
let test = {};
test[obj1] = "1";
test[obj2] = "2";
console.log(test[obj1], "test[obj1]"); // 2
如果把对象作为key,还是会按顺序执行对象的toString
、valueOf
方法。因为这里obj1和obj2都没有定义toString
方法,所以会查找原型链上的Object.prototype.toString
方法来执行。
那么对于基本类型来说,结果是什么呢?
调用String()将其转为字符串再作为key。用代码来证明一下我没有扯淡:
let test = {};
test[1n] = "1";
console.log(test["1"]); // 1
这里又发现一个和上面都不同的点:这里的Symbol值是可以作为键值的,那么这里的Symbol值可以隐式转换成功了还是没有触发隐式转换,我也不得而知。
转载自:https://juejin.cn/post/7041487859441729550