JavaScript内核与高级编程之:`JavaScript`的`Coercion`(类型强制转换):`ToPrimitive`和`ToString`的内部算法。

嘿,各位靓仔靓女,咱们今天来聊聊JavaScript的“七十二变”——类型强制转换!

大家好!今天咱们不谈风花雪月,就来聊聊JavaScript里那些“暗箱操作”——类型强制转换。 相信大家或多或少都遇到过一些看似“不讲道理”的JS行为,比如 [] == ![] 居然是 true, 还有 1 + "1" 变成了 "11"。 这些都跟JS的类型强制转换脱不了干系。

别怕,今天咱们就来扒一扒JS内部的“变身”机制,特别是ToPrimitiveToString这两个关键的内部算法,让你以后再遇到类似的问题,也能心里有数,淡定应对。

啥是类型强制转换(Coercion)?

简单来说,类型强制转换就是JS在某些情况下,自动把一个数据类型转换成另一个数据类型。 这种转换可能是显式的(explicit coercion),比如用 Number()String() 这样的函数;也可能是隐式的(implicit coercion),比如在 == 比较或者 + 运算中。

之所以要理解类型强制转换,是因为它直接影响你的代码逻辑,搞不清楚的话,bug 就会像雨后春笋一样冒出来。

咱们先来认识一下 ToPrimitive

ToPrimitive 是一个抽象操作,它负责把一个非原始类型的值(比如对象、数组)转换成原始类型的值(比如字符串、数字、布尔值)。 记住,这是一个内部操作,你不能直接在代码里调用它,但理解它的工作方式至关重要。

ToPrimitive 接受两个参数:

  1. input: 要转换的值。
  2. PreferredType (可选): 期望转换成的类型。 它可以是 NumberString。 如果没有提供,则由JS引擎自己决定。

ToPrimitive 的转换流程大致如下:

  1. 如果 input 已经是原始类型,直接返回 input。 这没啥好说的,人家本来就是原始类型,还转个啥?
  2. 否则,如果 PreferredType 是 Number:
    • a. 调用 input.valueOf()。 如果结果是原始类型,返回该结果。
    • b. 否则,调用 input.toString()。 如果结果是原始类型,返回该结果。
    • c. 否则,抛出一个 TypeError 错误。
  3. 否则,如果 PreferredType 是 String:
    • a. 调用 input.toString()。 如果结果是原始类型,返回该结果。
    • b. 否则,调用 input.valueOf()。 如果结果是原始类型,返回该结果。
    • c. 否则,抛出一个 TypeError 错误。
  4. 否则,如果 PreferredType 没有提供:
    • 如果 input 是 Date 对象, PreferredType 被设置为 String
    • 否则, PreferredType 被设置为 Number
    • 然后按照上面的规则进行转换。

重点:

  • valueOf()toString() 是对象自带的方法,可以被重写。
  • Date 对象在没有指定 PreferredType 时,默认转换为字符串。
  • 如果 valueOf()toString() 都没法返回原始类型,JS 就罢工了,直接抛出错误。

来,上代码!

// 一个简单的对象
let obj = {
  valueOf: function() {
    console.log("调用了 valueOf");
    return 10;
  },
  toString: function() {
    console.log("调用了 toString");
    return "hello";
  }
};

//  ToPrimitive(obj, Number)
let num = Number(obj); // 调用了 valueOf
console.log(num); // 10

// ToPrimitive(obj, String)
let str = String(obj); // 调用了 toString
console.log(str); // hello

// 一个更复杂的例子
let obj2 = {
  valueOf: function() {
    console.log("valueOf 被调用");
    return {}; // 返回一个对象,不是原始类型
  },
  toString: function() {
    console.log("toString 被调用");
    return {}; // 返回一个对象,不是原始类型
  }
};

try {
  Number(obj2); // valueOf 被调用  toString 被调用
} catch (e) {
  console.log(e); // TypeError: Cannot convert object to primitive value
}

// Date 对象的例子
let date = new Date();

// 没有指定 PreferredType,Date 对象默认转换为字符串
console.log(String(date)); // "Tue Oct 24 2023 16:34:56 GMT+0800 (China Standard Time)"
console.log(Number(date)); // 1698146096000  (时间戳)

接下来,深入了解 ToString

ToString 顾名思义,就是把一个值转换成字符串。 这个算法相对简单一些,但也有一些需要注意的地方。

ToString 的转换规则如下:

类型 结果
Undefined "undefined"
Null "null"
Boolean true 变成 "true", false 变成 "false"
Number 按照数字的字符串表示形式转换,比如 123 变成 "123"NaN 变成 "NaN"Infinity 变成 "Infinity"
String 直接返回该字符串
Symbol 抛出一个 TypeError 错误
Object 1. 调用 ToPrimitive(input, String)

重点:

  • undefinednull 直接变成对应的字符串。
  • Number 类型的转换涉及到一些细节,比如科学计数法、精度等等。
  • Symbol 类型不能直接转换为字符串,会报错。
  • Object 类型的转换会调用 ToPrimitive,并且 PreferredType 总是 String

继续上代码!

console.log(String(undefined)); // "undefined"
console.log(String(null)); // "null"
console.log(String(true)); // "true"
console.log(String(123)); // "123"
console.log(String(NaN)); // "NaN"
console.log(String(Infinity)); // "Infinity"

let sym = Symbol("mySymbol");
try {
  String(sym);
} catch (e) {
  console.log(e); // TypeError: Cannot convert a Symbol value to a string
}

let obj3 = {
  toString: function() {
    return "这是一个对象";
  }
};

console.log(String(obj3)); // "这是一个对象"

let arr = [1, 2, 3];
console.log(String(arr)); // "1,2,3"

类型强制转换的常见场景

现在我们已经了解了 ToPrimitiveToString 的基本原理,接下来看看它们在实际开发中的应用场景。

  1. 加法运算符 (+)

    • 如果其中一个操作数是字符串,另一个操作数会被转换为字符串,然后进行字符串拼接。
    • 如果两个操作数都不是字符串,那么它们会被转换为数字,然后进行加法运算。
  2. 比较运算符 (==, !=, >, <, >=, <=)

    • 如果两个操作数类型不同,JS会尝试将它们转换为相同的类型,然后再进行比较。
    • nullundefined 比较特殊,它们只和自身以及彼此相等。
    • 对象会先调用 ToPrimitive 转换为原始类型,然后再进行比较。
  3. 逻辑运算符 (&&, ||, !)

    • 逻辑运算符会把操作数转换为布尔值,然后再进行运算。
    • 0""NaNnullundefined 会被转换为 false,其他值会被转换为 true
  4. 条件语句 (if, while)

    • 条件语句会把条件表达式转换为布尔值,然后再判断是否执行代码块。

举几个栗子:

// 加法运算符
console.log(1 + "1"); // "11" (字符串拼接)
console.log(1 + 1); // 2 (数字加法)
console.log(1 + true); // 2 (true 被转换为 1)
console.log(1 + null); // 1 (null 被转换为 0)
console.log(1 + undefined); // NaN (undefined 被转换为 NaN)

// 比较运算符
console.log(1 == "1"); // true ("1" 被转换为 1)
console.log(0 == false); // true (false 被转换为 0)
console.log(null == undefined); // true
console.log(null === undefined); // false (类型不同,严格相等)
console.log([] == ![]); // true  (这个比较复杂,后面详细分析)
console.log([1] == 1); // true

// 逻辑运算符
console.log(!!0); // false
console.log(!!""); // false
console.log(!!NaN); // false
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!{}); // true
console.log(!![]); // true
console.log(!!1); // true

// 条件语句
if (0) {
  console.log("不会执行");
}

if ("hello") {
  console.log("会执行");
}

重点分析:[] == ![]true 的原因

这个表达式是类型强制转换的经典例子,也是面试常考题。 咱们来一步步分析:

  1. ! 的优先级高于 ==,所以先计算 ![][] 是一个对象,转换为布尔值是 true,所以 ![] 的结果是 false
  2. 现在表达式变成了 [] == false
  3. 根据 == 的规则,如果其中一个操作数是布尔值,会先把布尔值转换为数字。 false 转换为 0,所以表达式变成了 [] == 0
  4. 根据 == 的规则,如果其中一个操作数是对象,另一个操作数是数字,会先把对象转换为原始类型。 [] 调用 ToPrimitive 算法,PreferredType 为 Number
    • [].valueOf() 返回 [],不是原始类型。
    • [].toString() 返回 "" (空字符串),是原始类型。
  5. 所以 [] 被转换为 "",表达式变成了 "" == 0
  6. 根据 == 的规则,如果其中一个操作数是字符串,另一个操作数是数字,会先把字符串转换为数字。 "" 转换为 0,表达式变成了 0 == 0
  7. 0 == 0 的结果是 true

总结:

[] == ![] => [] == false => [] == 0 => "" == 0 => 0 == 0 => true

是不是感觉绕来绕去,头都大了? 记住这个过程,下次面试的时候就可以侃侃而谈了。

如何避免类型强制转换带来的坑?

类型强制转换虽然灵活,但也容易出错。 为了避免踩坑,可以遵循以下建议:

  1. 尽量使用严格相等 (===) 和严格不等 (!==) 严格相等不会进行类型转换,可以避免很多意想不到的结果。
  2. 避免使用简写形式的类型转换 比如 if (obj), 尽量明确地写成 if (obj !== null && obj !== undefined)
  3. 明确指定类型转换 如果需要把一个值转换为数字或字符串,使用 Number()String() 函数。
  4. 了解类型强制转换的规则 多做实验,加深理解。
  5. 使用代码检查工具 ESLint 等工具可以帮助你发现潜在的类型转换问题。

总结

今天我们深入探讨了 JavaScript 的类型强制转换,重点讲解了 ToPrimitiveToString 这两个内部算法。 类型强制转换是 JS 的一个重要特性,理解它有助于你写出更健壮、更可维护的代码。 希望通过今天的讲解,大家对类型强制转换有了更清晰的认识。

记住,理解这些“暗箱操作”是为了更好地掌控代码,而不是被它们所迷惑。 以后遇到类型转换的问题,不要慌,冷静分析,一步步拆解,相信你一定能找到答案!

下课! 感谢大家的聆听! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注