强制类型转换 -- Javascript基础探究篇(5)

446 阅读13分钟

js中的值可以从一种类型转换为另一种类型,这种行为被称为强制类型转换。

抽象操作规则

所有的规则以es6为准,和其他文章的es5版本规则稍有不同,详情请查看es6语言规范

主要介绍强制类型转换最常用的4种抽象操作:转换为基本类型ToPrimitive,转换为字符串ToString,转换为数字ToNumber,转换为布尔值ToBoolean

这一节主要抽象操作的规则,不会讲何时会触发这些操作操作。结合后续实例一起阅读效果更佳。

ToPrimitive

抽象操作ToPrimitive负责处理将原始类型转换为基本类型。

可以将ToPrimitive理解为一个形如ToPrimitive(input[, preferredType])的函数。其中input表示输入值;preferredType表示进行抽象操作时所倾向的转化类型,是一个可选值,它的值是"string""number"或者"default"之一。

ToPrimitive转换规则如下:

  • 当输入类型是基本类型(undefinednullbooleannumberstringsymbol)时,直接返回输入值
  • 当输入类型是对象类型(object)时,则:
    1. 判断对象是否具有Symbol.toPrimitive方法
      1. 如果有:如果该方法返回值是基本类型,就返回该值作为结果;如果该方法返回值是对象类型,报TypeError错误
    2. 如果preferredType被指定为"string"
      1. 判断该对象是否具有toString方法,如果有并且该方法返回基本类型值,返回该值作为结果
      2. 判断该对象是否具有valueOf方法,如果有并且该方法返回基本类型值,返回该值作为结果
      3. TypeError错误
    3. 如果preferredType被指定为"number"
      1. 判断该对象是否具有valueOf方法,如果有并且该方法返回基本类型值,返回该值作为结果
      2. 判断该对象是否具有toString方法,如果有并且该方法返回基本类型值,返回该值作为结果
      3. TypeError错误
    4. 如果preferredType未被指定类型(即为"default")
      1. 如果操作的对象是Date,则将preferredType重新指定为"string"因为Date转换为字符串的场景使用的比较多),并按以上规则转换
      2. 否则指定为"number",并按以上ToPrimitive规则转换

进行ToPrimitive操作的对象,如果不具有Symbol.toPrimitive方法,会依赖toStringvalueOf两个方法。某些对象本身就实现这些方法或者其中之一。即使没有实现,Object.prototype中就包含了toStringvalueOf这两个方法,所以一般的对象中其实都存在这两个方法(具体原因参见原型链)。不同对象调用这些方法的行为会有差别,请注意区分。

而使用Object.create(null)生成的对象是一个真空对象,什么属性方法都没有。所以对这种对象直接进行ToPrimitive操作(不在生成对象后手动添加相关方法)会报TypeError错误。

ToString

抽象操作ToString负责处理将原始类型转换为基本类型string

可以将ToString理解为一个形如ToString(input)的函数。

ToString转换规则如下:

  • string类型不转换直接返回
  • symbol会报TypeError错误
  • null转换为"null",undefined转换为"undefined"true转换为"true"false转换为""false"
  • number转换遵守通用规则,行为和new String(numberValue)一致,其中:
    • 特殊值NaN返回"NaN"Infinity返回"Infinity"-Infinity返回"-Infinity"
    • 零值0或者-0都返回"0"
    • 对于极大或者极小值使用指数形式的字符串
  • object会先将preferredType指定为"string"进行ToPrimitive抽象操作;再对得到的基本类型按照以上ToString规则转换

ToNumber

抽象操作ToNumber负责处理将原始类型转换为基本类型number

可以将ToNumber理解为一个形如ToNumber(input)的函数。

ToNumber转换规则如下:

  • number类型不转换直接返回
  • symbol会报TypeError错误
  • null转换为0undefined转换为NaNtrue转换为1false转换为0
  • string转换遵守通用规则,行为和new Number(stringValue)一致,其中:
    • 空字符串转换为0
    • 合法的二进制,八进制,十六进制的字符串数字都会被转换为十进制数字
    • 转换数字失败时返回NaN
  • object会先将preferredType指定为"number"进行ToPrimitive抽象操作;再对得到的基本类型按照以上ToNumber规则转换

ToBoolean

抽象操作ToBoolean负责处理将原始类型转换为基本类型boolean

可以将ToBoolean理解为一个形如ToBoolean(input)的函数。

ToBoolean转换规则如下:

  • 假值(falsy value),包括undefinednullfalse-00NaN""(空字符串)都会被强制转换为false
  • 真值(truthy value),即除假值之外的一切值都会被强制转换为true

以上四种抽象操作流程图:

type-conversion

显式转换

显式转换很容易识别,我们应该尽可能使用它,保证在编码时将类型转换表达清楚,提高代码可读性。

const a = new String(10); // 显式ToString转换,number转string
const b = new String(true); // 显式ToString转换,boolean转string

const c = new Number("10"); // 显式ToNumber转换,string转number
const d = new Number(true); // 显式ToNumber转换,boolean转number

const e = new Boolean("10"); // 显式ToBoolean转换,string转boolean
const f = new Boolean(0); // 显式ToBoolean转换,number转boolean

形如上述结构都是显式类型转换,会将传入的参数转换为内置构造函数对应的基础类型,并将转换后的结果存储在内部的[[PrimitiveValue]]中。

注意,显式调用toString方法和ToString抽象操作是不一样的:

const obj = {
  toString() {
    return {};
  },
};
console.log(obj.toString()); // {}
console.log(new String(obj)); // TypeError

调用toString方法时,并不一定需要返回字符串,我们可以自定义返回值。而对于ToString抽象操作来说,obj调用toString如果返回的不是基本类型会直接报错(因为当前obj没有指定Symbol.toPrimitive,且默认的valueOf方法返回的是该对象本身,同样不是基本类型)。

隐式转换

隐式转换则没有那么容易识别,主要出现在运算符操作中。

+运算符

一元形式

一元形式的+运算符(即只有一个操作数)会触发ToNumber的抽象操作。

const a = "12.5";
const b = true;
const obj = { a: 1 };

console.log(+a); // 12.5
console.log(+b); // 1
console.log(+obj); // NaN

obj.valueOf = function () {
  return "4e5";
};

console.log(+obj); // 400000

按照ToNumber转换规则。 ab很容易理解。我们重点说说obj

  1. 由于obj是对象类型,所以首先进行ToPrimitive(obj, "number")

    1. obj自身不在Symbol.toPrimitive;继续查找valueOf并在原型链中找到,它返回结果是它本身,不是基本类型;继续查找toString并在原型链中找到,它返回结果是字符串"[object Object]",返回该值
  2. "[object Object]"继续做ToNumber转换,得到NaN

而在我们手动为obj加上valueOf方法后:

  1. 由于obj是对象类型,所以首先进行ToPrimitive(obj, "number")
  2. obj自身不在Symbol.toPrimitive;继续查找valueOf并在对象自身中找到,它返回结果是字符串"4e5",返回该值
  3. "4e5"继续做ToNumber转换,得到400000

二元形式

二元形式的+运算符,转换规则更复杂:

  1. +两边的操作数分别进行ToPrimitive转换
  2. 转换后的值,如果存在string类型的值,则对+两边转换后的值分别进行ToString转换后进行拼接操作
  3. 转换后的值如果是其他类型,则对+两边转换后的值分别进行ToNumber转换后进行相加
const a = 1 + "str" + false;
console.log(a); // "1strfalse"

const obj1 = {
  valueOf() {
    return 1;
  },
};
const obj2 = {
  toString() {
    return "a";
  },
};
console.log(1 + obj1); // 2
console.log("1" + obj2); // "1a"
console.log(obj1 + obj2); // "1a"

我们逐个分析:

  • 1 + "str" + false
    1. 都是+运算符,所以从左到右依次计算
    2. 首先是 1 + "str",两个都是基本类型,所以ToPritimive转换结果不变
    3. 因为"str"是字符串,所以都进行ToString转换,"str"结果不变,1变成"1"
    4. 进行拼接操作结果为"1str"
    5. 继续和false相加,结果与上面步骤类似,所以得到最终结果"1strfalse"
  • 1 + obj1
    1. 首先对两边的操作数分别做ToPrimitive(不指定preferredType)转换,1不变,obj1转换为1
    2. 结果为1 + 1,由于不存在string类型值,所以两边分别进行ToNumber转换,由于都是数字类型,所以结果不变,最后相加结果2
  • "1" + obj2
    1. 首先对两边的操作数分别做ToPrimitive转换,"1"不变,obj1转换为"a"
    2. 进行拼接操作结果为"1a"
  • "obj1 + obj2"转换过程类似,不再赘述

-*/运算符

这三个运算符只适用于数字,所以转换规则比起+更简单,不会有ToString。和一元+规则类似,对所有的操作数进行ToNumber转换。

const a = "12.5";
const obj = {
  a: 1,
  valueOf() {
    return this.a;
  },
};

console.log(-a); // -12.5
console.log(-obj); // -2
console.log(a - obj); // 10.2
console.log(a * obj); // 25
console.log(a / obj); // 6.25

=====运算符

宽松相等(==)和严格相等===都常用于判断两个值是否相等。我们通常认为===不仅比较“值”是否相等,还会比较”类型“是否相等,而==只比较“值”是否相等。所以有以下结果:

console.log("1"==1); // true
console.log("1"===1); // // false

实际上这并不准确,正确的解释是:==在比较时会进行隐式类型转换,而===不会。实际上==做的事更多。

==运算符的行为由“抽象相等比较算法”定义,主要规则:

  • 如果两个操作数类型相同,则按相同类型比较方法进行比较
    • 如果是number类型,判断两者值是否相同,注意特殊值的比较
    • 如果是string类型,判断两者是否是完全相同的字符序列
    • 如果是object类型,判断两者是使用引用同一个对象
  • 如果两个操作数类型不同
    1. 对两边的操作数分别进行ToPrimitive转换;如果转换后的值类型相同,按相同类型判断规则比较
    2. 如果转换后的基本值一个是undefined类型,另一个是null类型,判断为true
    3. 否则,继续对两边转换后的值分别进行ToNumber转换后再按相同类型判断规则比较

在两个操作数类型相同时,=====工作原理相同。如果两个操作数类型不同,===将直接返回false

console.log("true" == 42); // false
console.log(3 == true); // false

const obj = [1, 2, 3];
console.log(obj == "1,2,3"); // true

我们逐个分析:

  • "true" == 42
    1. 类型不同,但都是基本类型,所以ToPrimitive结果不变
    2. 都不是undefined或者null之一,继续进行ToNumber,得到结果NaN42,返回false
  • 3 == true
    1. 类型不同,但都是基本类型,所以ToPrimitive结果不变
    2. 都不是undefined或者null之一,继续进行ToNumber,得到结果31,返回false
  • obj == "1,2,3"
    • 类型不同,首先进行ToPrimitive,得到结果"1,2,3""1,2,3"
    • 转换后类型相同,按照字符串比较方式比较,返回true

github大佬dorey制作了一份图表,列出了各种相等比较的情况:

equality

注意:switchcase所使用的比较是===

><>=<=运算符

比较运算符的行为由“抽象比较算法”定义,并且没有类似于===这样的严格比较。所以它们都可能会发生隐式转换,主要规则:

  • 先将preferredType指定为"number",对两个操作数分别进行ToPrimitive转换
  • 如果转换后的值都是string类型,则按字符串比较规则判断
  • 否则,对转换后的值继续进行ToNumber转换,再对转换后的值按照数字的比较规则判断
const a = [42];
const b = ["043"];
console.log(a < b); // false
console.log(10 < a); // true

const obj1 = { a: 1 };
const obj2 = { a: 2 };
console.log(obj1 < obj2); // false

我们逐个分析:

  • a < b

    1. 首先对两边的操作数分别进行ToPrimitive(value,"number")转换,得到"42""043"
    2. 转换后的值都是string类型,按照字符串比较规则"42" > "043",返回false
  • 10 < a

    1. 首先对两边的操作数分别进行ToPrimitive(value,"number")转换,得到10"42"
    2. 转换后的值不都是string类型,继续进行ToNumber转换,得到1042
    3. 按照数字比较规则10 < 42,返回true
  • obj1 < obj2

    1. 首先对两边的操作数分别进行ToPrimitive(value,"number")转换,得到"[object Object]""[object Object]"
    2. 转换后的值都是string类型,按照字符串比较规则"[object Object]" === "[object Object]",返回false

实际上,“抽象比较算法”只会比较<>的情况;>=<=会被转换。如a >= b会被转换为!(a < b)。所以>=并不是>==结果的组合。==采用了完全不一样的比较算法。

const a = { b: 42 };
const b = { b: 43 };

console.log(a < b); // false
console.log(a > b); // false

console.log(a == b); // false

console.log(a <= b); // true
console.log(a >= b); // true

首先,不管是a < b还是a >b,对两边分别进行ToPrimitive(value,"number")转换,得到"[object Object]""[object Object]",可以看到两个字符串相等,所以a < b还是a >b都为false。而a <= b会被转换会!(a>b),显然结果是true。同理a >= b也是true

a == b是采用“抽象相等比较算法”,两者都是对象,类型相同,对于对象就要判断两者是使用引用同一个对象,显然不是,所以结果为false

隐式转换为布尔相关运算符及运算符

下面的情况会发生布尔值隐式强类型转换:

  • if (..)语句中的条件判断表达式
  • for ( .. ; .. ; .. )语句中的条件判断表达式(第二个)
  • while (..)do..while(..)循环中的条件判断表达式
  • 三元? :中的条件判断表达式
  • 逻辑运算符||&&左边的操作数
  • !!!

这些表达式和运算符都会让操作数进行ToBoolean转换,再用于判断。

const a = 10;
console.log(!a); // false
console.log(!!a); // true

!会先让操作数进行ToBoolean操作,然后再取反;而!!相当于两次取反,得到原来的转换结果,所以!!常用于将其他类型的值转换为布尔值。

if等表达式也会先对其中的操作数进行ToBoolean转换后再判断true或者false:

const a = {};
if (a) {
  // 会进入内部执行
}

const b = a ? true : false; // true

&&||虽然常被看做逻辑运算符,但其实它们更接近”短路运算符“,用于返回两个操作数中的一个,而不一定返回布尔值。

const a = 10 || "abc"; // 10
const b = 10 && "abc"; // "abc"
const c = undefined || "abc"; // "abc"
const d = undefined && "abc"; // undefined

&&||会首先对第一个操作数执行条件判断,即执行ToBoolean转换再判断true或者false:

  • &&来说:如果判断结果是true则返回第二个操作数,否则返回第一个操作数。a && b相当于a ? b : a
  • ||来说:如果判断结果是true则返回第一个操作数,否则返回第二个操作数。和&&刚好相反。a || b相当于a ? a : b

if结合&&||使用时:

// 首先(10 && "abc")返回"abc", if再对"abc"进行ToBoolean操作得到true
if (10 && "abc") {
  // 会进入内部执行
}

// 首先(0 || "")返回"", if再对""进行ToBoolean操作得到false
if (0 || "") {
  // 不会进入内部执行
}

注意

js的类型转换十分强大,可以简化代码的书写。但同时也会降低代码可读性,在使用类型转换时要十分小心。也要避免在奇怪的场景中(如图所示)过度使用隐式类型转换,这属于语言的糟粕,不要把它们当做炫技的黑魔法。我们要取其精华去其糟粕。

emoji