不必因 JavaScript 类型转换而烦恼

97 阅读9分钟

显式转换

JavaScript 中的显式类型转换是指将一个数据类型转换为另一个数据类型,开发者需要显式地指定转换规则。一般常用的显式转换方法有:Number String Boolean parseInt parseFloat

  • Number()

    ParameterRerurncomment
    stringnumber or NaN空字符串变为0,如果出现任何一个非有效数字字符,结果都是NaN
    boolean1 or 0true → 1 false → 0
    null0
    undefinedNaN
    SymbolTypeErrorSymbol 不能转换为数字或者布尔值
    BigIntnumber去掉 n
  • Boolean()

    boolean 类型转换只会有 true 或者 false 两种结果。除了“0/NaN/空字符串/null/undefined”五个值是false,其余都是true

    也就是说 Boolean([]) 和 Boolean({}) 结果都是 true

  • String()

    其他类型转字符串就没有什么疑惑了

    重要的是转换后的结果,比如 String({}) → “[object,object]” String(Symbol(’foo’)) → “Symbol(foo)”

  • parseInt()

    ParameterRerurncomment
    浮点数整数
    undefined、null、boolean、NaNNaN布尔值会被转换为 NaN
    string
    ‘abc123’ →
    ‘123abc’ →number or NaN
    NaN
    123该方法会忽略字符串前导空格,直到找到第一个数字字符。如果字符串中包含非数字字符,将会停止解析,并返回已经解析的数字。
  • parseFloat()

    ParameterRerurncomment
    undefined、null、boolean、NaNNaN布尔值会被转换为 NaN
    string
    ‘abc123’ →
    ‘123abc’ →number or NaN
    NaN
    123该方法会忽略字符串前导空格,直到找到第一个数字字符。如果字符串中包含非数字字符,将会停止解析,并返回已经解析的数字。

隐式转换

遇到隐式转换时的思考方式

在看过ya羽的笔记后,我对自己的凌乱的笔记产生了思考,我的笔记并不能帮我梳理清楚一个正确思考顺序

github.com/mqyqingfeng…

我现认为的思考路线是,先判断需要进行什么类型的转化,然后层层递进判断,像调用函数那样

举例


	console.log([] + []);
  • 双目加号的类型转换,双目加号的规则:

    v 当计算 value1 + value2时:

    1. lprim = ToPrimitive(value1)
    2. rprim = ToPrimitive(value2)
    3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
    4. 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

    左右尝试转换为原始值,即 toPrimitive

  • 现在进入 toPrimitive 内部规则,没有规定 preferedType,除 Date 类型外为 number

  • preferedType === number 时 的规则:

    如果是 ToPrimitive(obj, Number),处理步骤如下:

    1. 如果 obj 为 基本类型,直接返回
    2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
    3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
    4. 否则,JavaScript 抛出一个类型错误异常。
  • valueOf() 的规则是:

    默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。

    所以 [].valueOf() 返回一个空数组,不是原始值,故调用 toString()

  • [].toString 返回空字符串

  • 所以 [] + [] 经过一系列转化后,是空字符串拼接空字符串,结果依然是空字符串

运算符

  • string 遇到 * / - % ++ -- 和数字运算1都会有转换为数字的过程
let a = '1';
a++ // 2
let b = '3';
b * 2 // 6
  • 加号的隐式转换特性
    • 当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串后拼接
    console.log(5 + {} + 1) // 5[object object]1
    

条件判断

  • string 遇到 > < >= <= != == 和数字比较也会转换为数字

    ⚠️ `1 === ‘1’ false`
  • null, ‘’ , undefined, 0, NaN 才转 false, 其余皆为true 包括 {} []

  • 使用 == 比较中的5条规则

    1. NaN和其他任何类型比较永远返回false (包括它自身)

    2. Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型。

      true == 1  // true 
      true == '2'  // false, 先把 true 变成 1,而不是把 '2' 变成 true
      true == ['1']  // true, 先把 true 变成 1, ['1']拆箱成 '1', 再参考规则3
      true == ['2']  // false, 同上
      undefined == false // false ,首先 false 变成 0,然后参考规则4
      null == false // false,同上
      
    3. StringNumber比较,先将String转换为Number类型。

    4. null == undefined比较结果是true,除此之外,nullundefined和其他任何结果的比较值都为false

      null == undefined // true
      null == '' // false
      null == 0 // false
      null == false // false
      undefined == '' // false
      undefined == 0 // false
      undefined == false // false
      
    5. 原始类型引用类型做比较时,引用类型会依照ToPrimitive规则转换为原始类型。

      详细看下一个标题内容

      '[object Object]' == {} 
      // true, 对象和字符串比较,对象通过 toString 得到一个基本类型值
      '1,2,3' == [1, 2, 3] 
      // true, 同上  [1, 2, 3]通过 toString 得到一个基本类型值
      

引用值转初始值

js 引擎内部的抽象操作 ToPrimitive 有着这样的签名:

ToPrimitive(input, PreferredType?)

input 是要转换的值,PreferredType 是可选参数,可以是 Number 或 String 类型。他只是一个转换标志,转化后的结果并不一定是这个参数所值的类型,但是转换结果一定是一个原始值(或者报错)

如果 PreferredType 被标记为 Number,则会进行下面的操作流程来转换输入的值。

1、如果输入的值已经是一个原始值,则直接返回它
2、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
   如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
3、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。

如果 PreferredType 被标记为 String,则会进行下面的操作流程来转换输入的值。

1、如果输入的值已经是一个原始值,则直接返回它
2、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
3、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
   如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。

valueOf 方法和 toString 方法解析

上面主要提及到了 valueOf 方法和 toString 方法,那这两个方法在对象里是否一定存在呢?答案是肯定的。在控制台输出 Object.prototype,你会发现其中就有 valueOf 和 toString 方法,而 Object.prototype 是所有对象原型链顶层原型,所有对象都会继承该原型的方法,故任何对象都会有 valueOf 和 toString 方法。

先看看对象的 valueOf 函数,其转换结果是什么?对于 js 的常见内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function

1、Number、Boolean、String 这三种构造函数生成的基础值的对象形式,通过 valueOf 转换后会变成相应的原始值。如:

var num = new Number('123');
num.valueOf(); // 123

var str = new String('12df');
str.valueOf(); // '12df'

var bool = new Boolean('fd');
bool.valueOf(); // true

2、Date 这种特殊的对象,其原型 Date.prototype 上内置的 valueOf 函数将日期转换为日期的毫秒的形式的数值。

var a = new Date();
a.valueOf(); // 1515143895500

3、除此之外返回的都为 this,即对象本身

var a = new Array();
a.valueOf() === a; // true

var b = new Object({});
b.valueOf() === b; // true

再来看看 toString 函数,其转换结果是什么?对于 js 的常见内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function

1、Number、Boolean、String、Array、Date、RegExp、Function 这几种构造函数生成的对象,通过 toString 转换后会变成相应的字符串的形式,因为这些构造函数上封装了自己的 toString 方法。如:

Number.prototype.hasOwnProperty('toString'); // true
Boolean.prototype.hasOwnProperty('toString'); // true
String.prototype.hasOwnProperty('toString'); // true
Array.prototype.hasOwnProperty('toString'); // true
Date.prototype.hasOwnProperty('toString'); // true
RegExp.prototype.hasOwnProperty('toString'); // true
Function.prototype.hasOwnProperty('toString'); // true

var num = new Number('123sd');
num.toString(); // 'NaN'

var str = new String('12df');
str.toString(); // '12df'

var bool = new Boolean('fd');
bool.toString(); // 'true'

var arr = new Array(1,2);
arr.toString(); // '1,2'

var d = new Date();
d.toString(); // "Wed Oct 11 2017 08:00:00 GMT+0800 (中国标准时间)"

var func = function () {}
func.toString(); // "function () {}"

除这些对象及其实例化对象之外,其他对象返回的都是该对象的类型,都是继承的 Object.prototype.toString 方法。

var obj = new Object({});
obj.toString(); // "[object Object]"

Math.toString(); // "[object Math]"

从上面 valueOf 和 toString 两个函数对对象的转换可以看出为什么对于 ToPrimitive(input, PreferredType?),PreferredType 没有设定的时候,除了 Date 类型,PreferredType 被设置为 String,其它的会设置成 Number。

因为 valueOf 函数会将 Number、String、Boolean 基础类型的对象类型值转换成 基础类型,Date 类型转换为毫秒数,其它的返回对象本身,而 toString 方法会将所有对象转换为字符串。显然对于大部分对象转换,valueOf 转换更合理些,因为并没有规定转换类型,应该尽可能保持原有值,而不应该想 toString 方法一样,一股脑将其转换为字符串。

所以对于没有指定 PreferredType 类型时,先进行 valueOf 方法转换更好,故将 PreferredType 设置为 Number 类型。

而对于 Date 类型,其进行 valueOf 转换为毫秒数的 number 类型。在进行隐式转换时,没有指定将其转换为 number 类型时,将其转换为那么大的 number 类型的值显然没有多大意义。(不管是在+运算符还是==运算符)还不如转换为字符串格式的日期,所以默认 Date 类型会优先进行 toString 转换。故有以上的规则:

PreferredType 没有设置时,Date 类型的对象,PreferredType 默认设置为 String,其他类型对象 PreferredType 默认设置为 Number。

isNaN()

  • 传入的参数都会经过转换为数字处理,再看是否为 NaN
isNaN(1) // false
isNaN('123') // false
isNaN('a') // true
**isNaN(null) // false
isNaN(undefined) // true**

转换表

Untitled.png

例题

例题一

if (typeof (a) && (-true) + (+undefined) + '') {
    console.log('pass');
} else {
    console.log('not pass');
} // pass

直接输出a会报错,但是 typeof(a)‘undefined’ ,这是一条字符串,所以对应的布尔值是 true

-true 类型转换为 -1

+undefined,转换为 NaN

那么 -1 + NaN + ‘’ 结果就是 ‘NaN’,一条字符串

所以条件判断为 true

console.log(!!' ' + !!'' - !!false || '未通过'); // 1

这里主要是 || 的阻塞,如果前面的条件为 true 后边就不执行了

例题二

console.log([] == ![]); // true

! 运算优先级高于 == ,所以 ![] → false,根据第6 7条得,当有一方为 boolean时,将 boolean转换为 number再比较

  • ==规则

Untitled 1.png 所有右边就是 false → 0

对于左边就是引用类型转原始值 ToPrimitive( [] , number) ,先 valueOf() 返回数组对象,不是原始值,再 toString(),返回 ‘’ 是原始值,最后 ‘’ → 0

查过的资料

xie.infoq.cn/article/067…

github.com/mqyqingfeng…

如果是 ToPrimitive(obj, Number),处理步骤如下: