JavaScript 类型转换

分享

前言

这次的 why what or how 主题:JavaScript 类型转换。

什么是类型装换,以及为什么要进行类型转换?相信绝大多数熟悉面向对象的开发者们都知道是为了匹配类型!

但,这是 JavaScript 的世界,可以说原本就是没有类型系统的,那么为什么 JavaScript 的世界也有类型转换?

为了容错!并且 JavaScript 世界的类型转换也不是发生在对象上,而是在基础类型!

类型!

这不是刚说完 JavaScript 没有类型,怎么开始说类型了?

这里指的是基础类型,相信大家对于 JavaScript 数据类型已经充分了解了,这里也不啰嗦主要有以下 7 种。

  • 数值: 11.01e10
  • 字符串:"string""foo""bar"
  • 布尔值: truefalse
  • null
  • undefined
  • 对象
  • SymbolES6 新出的类型,用于标志唯一值

转换场景

OK 类型 7 种,很简单,那么转换发生的条件是什么?

  1. 不同类型的多个数据进行了一定的操作如:比较、数学运算(四则运算与位运算)、逻辑运算(&& ||)、拼接字符串。
  2. 单元运算符:!+-++--~(位运算中的取反)。

可以用一个词来概括这些场景:冲突。当冲突发生时,便会发生类型转化,而冲突又可以概括为以下两点:

  1. 数据间的冲突。
  2. 数据与运算符的冲突。

举几个常见的例子:

let num = 1;
let str = 'foo';
let bool = true;
let nu = null;
let nd = undefined;
let obj = {foo: 'bar'};

// 比较:不同数据间的冲突
num < str && bool == nu || nd > obj

// 四则运算:数据的冲突 & 数据与运算符的冲突
num + str - bool * nu / (nd + obj)

// 位运算:数据与运算符的冲突
num ^ str | bool & nu | nd >>> obj

// 拼接字符串:数据与运算符的冲突
`bala${num}bala${str}bala${bool}${nu}bala${nd}${obj}`

// 单元运算:数据与运算符的冲突
!num
+str
-bool
++nu
nu++
--nd
nd--
~obj

以上场景都需要进行类型转换,因为不同类型的数据是不能进行同一操作的,但这些都是符合 JavaScript 语法的,这就产生了冲突,就需要解决冲突,那么如何解决冲突呢?就需要进行类型转换,就要谈到一个策略:

在 JavaScript 中,是不太希望发生错误的,换句话说,JavaScript 解析器会尽量满足你的需求。

如何理解这句话?这就需要谈到一个词:偏向性。

偏向性

从最简单的拼接字符串来讨论这个问题。思考以下问题:

在拼接字符串时,你最希望得到的结果是什么?

这是一个很傻逼的问题,字符串啊!当然必须是字符串啊!通过一个例子来说明:

let num = 1;
let string = 'string';
let boolean = false;

`${num}${string}${boolean}`

上述的代码中,由于 (``) 操作符返回一定是字符串,因此 numboolean 就被 JavaScript 解析器转换成字符类型。

这种从操作符去推测其数据应该转换成什么类型,我称之为:偏向性。所有的操作符都有其偏向性,总结如下

操作符 偏向性
``(模板字符串) 字符
四则运算(排除 + 数值
位运算 数值
逻辑运算(&&||! 布尔值
+ 字符 < 数值
比较运算(排除相等于不相等) 字符 < 数值
===!== 引用值
==!= 引用值 < 数值

注:

  1. 操作符的偏向性可能不止于一种,如 + 运算符,其最终的偏向性由运算符两侧的数据类型所确定。
  2. 偏向性内的 < 符号表示偏向性的优先级,即若前者不能满足要求则使用后者。

确定偏向性

这里仅讨论偏向性不确定的操作符,以及如何确定操作符的偏向性。

+ 号运算符

  1. + 号两侧的类型中有字符类型,则其偏向性为字符串。
  2. 转换为数值进行比较。

参考以下例子:

// 字符串 + 数值
'a' + 1             // 'a1'
1 + 'a'             // '1a'
'10' + 1            // '101'
1 + '10'            // '110'

// 字符串 + 其他
'a' + true          // 'atrue'
'a' + false         // 'afalse'
'a' + null          // 'anull'
'a' + undefined     // 'aundefined'

// 不含字符串
true + null         // 1
false + undefined   // NaN 因为 Number(undefined) 为 NaN,而 NaN 的任何四则运算都为 NaN。
10 + true           // 11

'10' + 1 + 1        // '1013'
1 + '10' + 1        // '1101'
1 + 1 + '10'        // '210'
// 连加操作可以认为是两次 + 的集合,如 '10' + 1 + 1 => ('10' + 1) + 1

比较运算

比较运算:>>=<<= ,这里排除相等判断。

  1. 若比较运算两侧都为字符类型,则其偏向性为字符串。
  2. 转换为数值进行比较。

可参考一下例子:

// 都为字符,则使用字符的字典顺序比较
'b' > 'a'           // true

// 表达式下的注释为推导过程
'a' > 1             // false
// => Number('a') > 1       => NaN > 1      => false
'a' < 1             // false
// => Number('a') < 1       => NaN < 1      => false
// NaN 与任何值比较都为 false,这也证明了 'a' 转换成了数值
'10.1' > 1          // true
// => Number('10.1') > 1    => 10.1 > 1     => true
'10.1a' > 1         // false
// => Number('10.1a') > 1   => NaN > 1      => false
// 这说明 js 解析器确实使用 Number 来转换类型,而不是使用 parseInt 或是 parseFloat

true > 0            // true
// => Number(true) > 0      => 1 > 0        => true
false < 1           // true
// => Number(false) < 1     => 0 < 1        => true
true > false        // true
null < 1            //true
// => Number(null) < 1      => 0 < 1        => true
undefined < 1       // false
undefined > 1       // false
// => Number(undefined) > 1 => NaN > 1      => false

'a' > true          // false

注: + 运算符只要有一侧是字符类型其偏向性为字符类型,而比较运算必须为两次都为字符类型,其偏向性才为字符类型。

纵观上述代码,还可以得出一个结论,只要不是数字型的字符串,与任何非字符串比较都为 false,其原因在于 Number('a')NaNNaN 与任何数值比较都为 false

相等判断

相等:==!= 这个是重头戏,但其实内容也不难,对于什么是引用值,如果不清楚可以查看我写的另外一篇文章:JS 变量存储?栈 & 堆?NONONO!。变量引用的地址值内数据即为引用值。

其偏向性的判断如下:

  1. nullundefined 互相相等,但与其他类型都不等。
  2. 如果操作符两侧的数据为同种类型,那么比较两侧数据的引用值。
  3. 转换为数值进行比较。

可参考以下例子:

// 类型一致,其实不需要验证
'a' == 'b'          // false
1 == 2              // false
true == false       // false

// 对象比较
let a = {};
let b = a;
a == {}             // false    引用值不一致
a == b              // true     引用值一致
// ...

// null 与 undefined
null == undefined   // true
null == 0           // false
undefined == 0      // false
null == false       // false
undefined == false  // false
undefined == 'a'    // false

// 不同类型
'1' == 1            // true
// => Number('1') == 1              => 1 == 1       => true
'a' == 1            // false
// => Number('a') == 1              => NaN == 1     => false
'a' == NaN          // false
// => Number('a') == NaN            => NaN == NaN   => false
// 这个例子可以看出 NaN != NaN 是有实际意义存在的。
'0' == false        // true
// => Number('0') == Number(false)  => 0 == 0       => true
'0' == null         // true

附上基础类型转换规则表,以及转换后最终的值

- undefined null Number String Boolean 转换调用的函数
转换为字符 "undefined" "null" String(xxx) 无需转换 "true"/"false" String
转换为布尔值 false false 0 => false; NaN => false; 其他为 true '' => false; 其他为 true 无需转换 Boolean
转换为数值 NaN 0 无需转换 Number(xxx) true => 1; false => 0 Number

对象转换

上述内容讨论了 JavaScript 中基础类型之间的转换规则,及如何进行转换,那么现在思考一下,对象是如何与基础值进行操作的?请先思考下以下代码的执行结果。

let demo1 = {};
`${demo1}`
demo1 + ''
demo1 + 1
!demo1

let demo2 = {
    toString(){
        return 'demo2';
    },
    valueOf(){
        return 2;
    }
}
`${demo2}`
demo2 + ''
demo2 + 1
!demo2

let demo3 = {};
Object.setPrototypeOf(demo3, null);
`${demo3}`
demo3 + ''
demo3 + 1
!demo3

请先确保心中大致有个答案哦,不妨用记事本记下你的答案,接下来公布转换的规则:

  1. 如果操作符的偏向性为布尔值,那么直接转换为 true
  2. 如果操作符仅有字符的偏向性,比如:``,调用对象下的 toString 方法,如果没有该方法会报错。
  3. 其他情况一律调用 valueOf 方法,如果没有该方法会报错。
  4. 根据上诉获得的基础类型的数据,进行基础类型转换,获得结果。

那以上 12 个的最终结果确定过程及结果如下:

`${demo1}`  => `${demo1.toString()}`    => `${"[object Object]"}`   => "[object Object]"
demo1 + ''  => demo1.valueOf() + ''     => "[object Object]" + ''   => "[object Object]"
demo1 + 1   => demo1.valueOf() + 1      => "[object Object]" + 1    => "[object Object]1"
!demo1      => !true                    => false

`${demo2}`  => `${demo2.toString()}`    => `${"demo2"}`             => "demo2"
demo2 + ''  => demo2.valueOf() + ''     => 2 + ''                   => "2"
demo2 + 1   => demo1.valueOf() + 1      => 2 + 1                    => 3
!demo2      => !true                    => false

`${demo3}`  => `${demo3.toString()}`    => 没有 toString 方法,报错
demo3 + ''  => demo2.valueOf() + ''     => 没有 valueOf 方法,报错
demo3 + 1   => demo1.valueOf() + 1      => 没有 valueOf 方法,报错
!demo3      => !true                    => false

因此对象是先转换成基础类型,在进行后续操作,其关键方法为 toStringvalueOf,至于空对象为什么有 toStringvalueOf 方法,在设置了 setPrototypeOf(xxx, null) 后这两方法就没有了,请查看JavaScript 对象 & 原型

当我以为得到真理时,一个判断的结果却让我大呼惊讶:

'a' > []        // true

这个判断返回了 true!,根据前面说的:字符串与非字符串比较时,其返回的结果永远是 false 吗?这点已经通过了验证,不会错。问题就出在这个 [] 上,这个 [] 被转换成了什么?

首先可以确定:[] 被转换成了字符。但这又和对象的转换规则相冲了,比较运算的偏向性为数值和字符,为什么调用了 toString 而不是 valueOf 呢?
为了确定这个问题,我把 Array 原型下的 toStringvalueOf 稍加了修改:

let valueOf = Array.prototype.valueOf;
Array.prototype.valueOf = function(...args){
    console.log('触发 valueOf');
    return valueOf.apply(this, args);
}
let toString = Array.prototype.toString;
Array.prototype.toString = function(...args){
    console.log('触发 toString');
    return toString.apply(this, args);
}
'a' > []
// 触发 valueOf
// 触发 toString
// true

没错,我劫持了 valueOftoString 方法,然后执行一遍大呼过瘾,在转换类型时,数组确实先执行了 valueOf 而后有执行了 toString 方法。那么为什么转换过程中会同时执行这两个方法呢?会不会和 valueOf 返回值有关?

[].valueOf()
// []
[].toString()
// ""

[] valueOf 方法的返回值就是它本身,一个空数组,这显然不是一个基础类型,而 toString 返回空字符串,是个基础类型。那这时候我又想到一个问题:如果转换的结果始终得不到基础类型,会发生什么?会报错吗?

let demo = {
    toString() {
        return {}
    },
    valueOf() {
        return {}
    }
}
demo > 1
// Uncaught TypeError: Cannot convert object to primitive value

果不其然,成功的报错了,这也给了我一个启示:为了保证 JavaScript 能稳定的运行下去,toString 方法必须要遵从语义,返回一个字符串。

最后根据以上内容,将对象进行运算操作时,处理步骤更新如下

  1. 如果操作符的偏向性为布尔值,那么直接转换为 true
  2. 如果操作符仅有字符的偏向性,比如:``,调用的 toString 方法,没有该方法或是该方法未返回基础类型,则调用 valueOf,如果没有 valueOf 方法或是 valueOf 方法未返回基础类型,就会报错。
  3. 其他情况一律调用 valueOf 方法,没有该方法或是该方法没用返回基础类型,则调用 toString 如果没用 toString 方法或是 toString 方法没用返回基础类型,就会报错。
  4. 根据上诉过程获得的基础类型的数据,进行基础类型转换,获得结果。

小练习

请判断出以下内容的结果:

// == 操作
!'0' == '0'
!''  == 1
''   == 0
!'a' == 0
![]  == []
![]  == 0
[]   == 0
!![] == [1]
!''  == [1]
''   == !'a'
!''  == ''
null == []
null == ![]
null == false
null == true

// 比较操作
'a'  > null
null > 'a'
'1'  > null
'a'  > []

参考

扩展阅读

为 falsy 的对象!

在相等判断中:nullundefined 互相相等,但与其他类型都不等。
在对象转换规则中:如果操作符的偏向性为布尔值,那么直接转换为 true

这两条不完全正确。

在浏览器的实现中,有一类对象:document.all,它是可以与 nullundefined 相等的,并且这一类对象代表的是 false

null == document.all            // true
undefined == document.all       // true
!document.all                   // true

但这无关痛痒,仅为了文章的正确性,在这里提一下,知不知道都无所谓,开发时用不到,并且,如果有面试官提了这个问题,就让他谈谈这个的具体用处,评论给我,我也想了解了解,或者你反问:JavaScript 中代表 false 都有哪些值,如果他忘了 document.all 就狠狠的嘲笑一番。

ps:document.all 已经在 HTML5 标准中被移除了,因此这个认知就变得更不重要了。

逻辑运算

有较真的网友可能发现,其实对于逻辑运算符(&&||)的描述,其实也是有不对之处。比如以下代码:

let a = true && 0 && 1;
let b = false || 1;

a 的值为 0b 的值为 1 并不是布尔值。这涉及到 JavaScript 的求值问题,可以理解为:在 JavaScript 中逻辑运算按照优先级运算,并由前往后一步一步进行求值(有可能不会进行到最后一步),如果需要进行下一步判断,则将当前步的数据转为布尔值进行运算,如果不需要进行下一步,则返回当前步的值。参考以下例子:

0 && 1 && 'a'               // 0
1 && 1 && 'a'               // 'a'
1 && 1 && 0                 // 0
0 || 0 || 'a' || 'b'        // 'a'
0 || 0 && 'a' || 'b'        // 'b' && 操作符的优先级高于 || 操作符

如果你能真确理解并得出结果,那应该是没问题了。

最后的最后

该系列所有问题由 minimo 提出,爱你哟~~~

阅读更多

JavaScript 对象 & 原型

前言 这次的 why what or how 主题:JavaScript 对象 & 原型。 此类文章在百度上一搜一大把,其实不用再写了,但是本着把这个问题解释的清清楚楚明明白白,还是开始写了。 原因如下: 1. 面试宝典类文章,弄张图片一糊弄,让人觉得自己理解了。 2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。 3. 很少有文章单独解释这个点!但这个点是基础!真的很重要! 所以本篇文章想说一说对象 & 原型,但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!,因为该篇文章从变量存储的角度来解释 JavaScript 对象 & 原型,请确保看完,在看这篇文章。 什么是对象? 既然要说清楚,

By Breeze

浏览器下的 Event Loop

前言 javascript 是以单线程的形式运行在宿主环境下,javascript 采用了回调的形式来解决异步任务。 为什么是单线程? javascript 的最开始的出现是为了给 web 页面增添一些动态的效果,那么就避免不了获取页面上的元素信息,如果 javascript 是以多线程的形式运行在浏览器内,如果两个线程内的 javascript 同时去获取/修改,某个页面上的元素,那么浏览器该让哪个 javascript 线程拥有获取/修改该元素的权限呢?由于元素的信息会经常性的发生变化,那么又改如何去同步各个线程内所保存的元素信息呢? 所以综合以上问题, javascript 是单线程的原因就显示意见了,单线程在执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有的问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要经过大量时间运算或是等待的代码,在浏览器的环境下,常见的就是 http 任务,比如:资源的加载(图片的 onload 事件),ajax 的请求(XMLHttpRequest 的 onLoad 事件)还有页面元素的点击事件以及

By Breeze

node 下的 Event Loop

前言 通过 浏览器下的 Event Loop ,可以得知 javascript 是一门事件驱动的语言, javascript 主线程通过不断的调用事件队列中的事件来完成异步任务。那么 Node 下是否也是如此? Node 下的事件队列 在 Node 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick() 。 该文章主要讲述了 Node 中是如何处理以及实现 Node 下的 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node 下的有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5 ,那么 H5 到底相比于 HTML4 有什么区别呢? 升级版?标准版! HTML5 做为 HTML 标准的第 5 版,于 2014 年发布,相信关注过 HTML5 的发展史的朋友都知道该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果,其中的绝大部分规范都由 WHATWG 组织所制定,之后由 W3C 采纳并入标准中。

By Breeze