likes
comments
collection
share

隐式调用toString和valueOf的场景,你认为对吗

作者站长头像
站长
· 阅读数 17

前言

valueOftoString 很少出现在我们的代码里,但是他们是存在隐式调用的,所以实际上我们调用它的次数是很多的。这里从我所理解的角度来分析大部分隐式调用的场景的执行流程。

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");
}

运行结果如下:

隐式调用toString和valueOf的场景,你认为对吗

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'")

运行结果如下:

隐式调用toString和valueOf的场景,你认为对吗

这里可以看到上述运算都是调用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");
}

测试结果:

隐式调用toString和valueOf的场景,你认为对吗

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);
    }

隐式调用toString和valueOf的场景,你认为对吗

如果把test1换成

    let test1 = {
        name: "test1",
        toString() {
            return undefined;
        },
        valueOf() {
            return undefined;
        },
    };

那么还是不会和undefined、null相等(直接就返回false了)

隐式调用toString和valueOf的场景,你认为对吗

2.3.都是引用类型

这个简单,直接判断是否指向了同一个引用对象。

3. 其他运算符

对于其他运算符(-、*、/、>、<),简单的概括为,如果是基本类型x,调用Number(x)转为数字,如果是对象,那么按照valueOftoString的顺序进行调用,转为基本类型。

需要注意的点是:

  1. Symbol类型不能隐式转为数字,所以上述运算都不能参与

  2. 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'");

测试结果:

隐式调用toString和valueOf的场景,你认为对吗

4. 其他场景

4.1 使用alert调用

  • 如果参数是基本类型则会隐式调用String()转为字符串,这里还是存在Symbol类型无法隐式转换的问题。
  • 如果参数是引用类型则会按照toStringvalueOf的顺序转为基本类型,再转为字符串。

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,还是会按顺序执行对象的toStringvalueOf方法。因为这里obj1和obj2都没有定义toString方法,所以会查找原型链上的Object.prototype.toString方法来执行。

那么对于基本类型来说,结果是什么呢?

调用String()将其转为字符串再作为key。用代码来证明一下我没有扯淡:

let test = {};
test[1n] = "1";
console.log(test["1"]); // 1

这里又发现一个和上面都不同的点:这里的Symbol值是可以作为键值的,那么这里的Symbol值可以隐式转换成功了还是没有触发隐式转换,我也不得而知。