文章引用:http://0313.name/archives/480

前言

类型转换在各个语言中都存在,而在 JavaScript 中由于缺乏对其的了解而不慎在使用中经常造成bug被人诟病。为了避免某些场景下的意外,甚至推崇直接使用 Strict Equality( === )来代替 ==。这确实能避免很多bug,但更是一种对语言不理解的逃避(个人观点)。

引入

先抛出在 You Don’t Know JavaScript (中) 看到的一个例子

  [] == [] // false
[] == ![] // true
{} == !{} // false
{} == {} // false

是不是很奇怪?本文将从书中看到的知识与规范相结合,来详细说明一下JavaScript在类型转换时候发生的故事。

类型转换

很多人喜欢说显示类型转换与隐式类型转换,但个人感觉只是说法上的不同,实质都在发生了类型转换而已,故不想去区分他们了(感觉一万个人有一万种说法)

仅在6大基本类型 null undefined number boolean string object 作讨论 symbol未考虑

  • 举个栗子

    var a = String(1)
    var b = Number('1')
    var c = 1 + ''
    var d = +'1'

    a,b直接调用了原生函数,发生了类型转换。c,d使用了+运算符的一些规则,发生了类型转换。这些是很简单的也是我们常用的。

    其实真正起作用的,是语言内部对规范中抽象操作的实现,接下来我们所说的 ToString, ToNumber, ToBoolean等都是抽象操作,而不是JS里对应的内置函数

  • ToString – 规范9.8

    按照以下规则转化被传递的参数

    Argument Type Result
    Undefined “undefined”
    Null “null”
    Boolean true -> “true”
    false – > “false”
    Number NaN -> “NaN”
    +0 -0 -> “0”
    -1 -> “-1”
    infinity -> “Infinity”
    较大的数科学计数法 (详见规范9.8.1)
    String 不转换 直接返回
    Object 1. 调用ToPrimitive抽象操作, hint 为 String 将返回值作为 value
    2. 返回ToString(value)
    String(undefined) // "undefined"
    String(null) // "null"
    String(true) // "true"

    ToPrimitive 抽象操作下面会提及

  • ToNumber – 规范9.3

    按照以下规则转换被传递参数

    Argument Type Result
    Undefined NaN
    Null +0
    Boolean true -> 1
    false -> +0
    Number 直接返回
    String 如果不是一个字符串型数字,则返回NaN(具体规则见规范9.3.1)
    Object 1. 调用ToPrimitive抽象操作, hint 为 Number 将返回值作为 value
    2. 返回ToNumber(value)
  • ToBoolean – 规范9.2

    按照以下规则转换被传递参数

    Argument Type Result
    Undefined false
    Null false
    Boolean 直接返回
    Number +0 -0 NaN -> false
    其他为true
    String 空字符串(length为0) -> false
    其他为true
    Object true
  • ToPrimitive – 规范9.1

    顾名思义,该抽象操作定义了该如何将值转为基础类型(非对象),接受2个参数,第一个必填的要转换的值,第二个为可选的hint,暗示被转换的类型。

    按照以下规则转换被传递参数

    Argument Type Result
    Undefined 直接返回
    Null 直接返回
    Boolean 直接返回
    Number 直接返回
    String 直接返回
    Object 返回一个对象的默认值。一个对象的默认值是通过调用该对象的内部方法[[DefaultValue]]来获取的,同时传递可选参数hint。
  • [[DefaultValue]] (hint) – 规范8.12.8

    • 当传递的hint为 String 时候,

      • 如果该对象的toString方法可用则调用toString

        • 如果toString返回了一个原始值(除了object的基础类型)val,则返回val
      • 如果该对象的valueOf方法可用则调用valueOf方法
        • 如果valueOf返回了一个原始值(除了object的基础类型)val,则返回val
      • 抛出TypeError的异常
    • 当传递的hint为 Number 时候,
      • 如果该对象的valueOf方法可用则调用valueOf方法

        • 如果valueOf返回了一个原始值(除了object的基础类型)val,则返回val
      • 如果该对象的toString方法可用则调用toString
        • 如果toString返回了一个原始值(除了object的基础类型)val,则返回val
      • 抛出TypeError的异常
    • hint的默认值为Number,除了Date object
    • 举个栗子
    var a = {}
    a.toString = function () {return 1}
    a.valueOf = function () {return 2}
    String(a) // "1"
    Number(a) // 2
    a + '' // "2" ???????
    +a // 2
    a.toString = null
    String(a) // "2"
    a.valueOf = null
    String(a) // Uncaught TypeError: balabala

似乎我们发现了一个很不合规范的返回值,为什么 a + ''不应该返回”1″吗

  • 问题的答案其实很简单 + 操作符会对两遍的值进行 toPrimitive 操作。由于没有传递 hint 参数,那么就会先调用a.valueOf 得到2后因为+右边是字符串,所以再对2进行ToString抽象操作后与””的字符串拼接。

不要畏惧使用 ==

基础概念已经了解了,那么在 == 中到底发生了什么样的类型转换,而导致了经常产生出乎意料的bug,导致了它臭名昭著。

  • 抽象相等 – 规范11.9.3

    x == y 判断规则如下:

    1. 如果xy类型相同 (与严格相等判断一致,不赘述了,详见规范)
    2. 如果 x 为 null y 为 undefined, 返回true
    3. 如果 x 为 undefined y 为 null, 返回true
    4. 如果 x 类型为 Number, y 类型为 String, 返回 x == ToNumber(y)
    5. 如果 x 类型为 String, y 类型为 Number, 返回ToNumber(x) == y
    6. 如果 x 类型为 Boolean, 返回 ToNumber(x) == y
    7. 如果 y 类型为 Boolean, 返回 x == ToNumber(y)
    8. 如果 x 类型为 String 或 Number, y 类型为 Object, 返回 x == ToPrimitive(y)
    9. 如果 x 类型为 Object, y 类型为 String 或 Number, 返回 ToPrimitive(x) == y
    10. return false
  • 再看引入

  [] == [] // false
// 1. 两遍类型都为 Object,比较引用地址,不同返回false 搞定
[] == ![] // true
// 1. ![]强制类型转换 变为 [] == false
// 2. 根据规范第7条,返回 [] == ToNumber(false), 即 [] == 0
// 3. 根据规范第9条,返回ToPromitive([]) == 0,数组的valueOf为本身,不是原始值,则返回toString()即 "" == 0
// 4. 根据规范第5条,返回ToNumber("") == 0, 即 0 == 0
// 5. 根据规范第1条,返回 true // 下面的不赘述了,分析类似上面
{} == !{} // false
{} == {} // false

我们不难看出以下几点

  • 其实在x y类型相同的时候,== 与 === 没有任何区别。
  • 除了undefined与null, 大多数值都会转换为相同类型后进行对比,也就是说 === 是 == 某些情况下必经的步骤

引用 << 你不知道的JS(中) >> 中的2句话

  • 如果两遍的值中有 true 或者 false , 千万不要使用 == (会被转为数字0,1来进行判断,会出现一些意外的情况)
  • 如果两遍的值中有[]、””或者0,尽量不要使用 ==

抽象比较

先来看看这个例子

var a = { b: 42 }
var b = { b: 43 }
a < b // false
a == b // false
a > b // false a <= b // true
a >= b // true

是不是感觉到世界又崩塌了???

让我们来仔细分析一下

var a = { b: 42 }
var b = { b: 43 }
a < b // false
// 1. 两遍调用ToPrimitive, 返回[object Object] 两遍一致 返回 false
a == b // false
// 两遍不同的引用,返回false
a > b // false
// 同 a < b a <= b // true
// 按规范其实是处理成 !(a > b) 所以为true
a >= b // true

所以在不相等比较的时候,我们最后还是进行手动的类型转换较为安全

总结

深入了解类型转换的规则,我们就可以很容易取其精华去其糟粕,写出更安全也更简洁可读的代码。

js中为什么你不敢用 “==”的更多相关文章

  1. 5.0 JS中引用类型介绍

    其实,在前面的"js的六大数据类型"文章中稍微说了一下引用类型.前面我们说到js中有六大数据类型(五种基本数据类型 + 一种引用类型).下面的章节中,我们将详细讲解引用类型. 1. ...

  2. 【repost】JS中的异常处理方法分享

    我们在编写js过程中,难免会遇到一些代码错误问题,需要找出来,有些时候怕因为js问题导致用户体验差,这里给出一些解决方法 js容错语句,就是js出错也不提示错误(防止浏览器右下角有个黄色的三角符号,要 ...

  3. JS中给正则表达式加变量

    前不久同事询问我js里面怎么给正则中添加变量的问题,遂写篇博客记录下.   一.字面量 其实当我们定义一个字符串,一个数组,一个对象等等的时候,我们习惯用字面量来定义,例如: var s = &quo ...

  4. js中几种实用的跨域方法原理详解(转)

    今天研究js跨域问题的时候发现一篇好博,非常详细地讲解了js几种跨域方法的原理,特分享一下. 原博地址:http://www.cnblogs.com/2050/p/3191744.html 下面正文开 ...

  5. 关于js中的this

    关于js中的this this是javascript中一个很特别的关键字,也是一种很复杂的机制,学习this的第一步就是要明白this既不指向函数自身也不指向函数的词法作用域,this实际上是函数被调 ...

  6. 表值函数与JS中split()的联系

    在公司用云平台做开发就是麻烦 ,做了很多功能或者有些收获,都没办法写博客,结果回家了自己要把大脑里面记住的写出来. split()这个函数我们并不陌生,但是当前台有许多字段然后随意勾选后的这些参数传递 ...

  7. JS中 call() 与apply 方法

    1.方法定义 call方法: 语法:call([thisObj[,arg1[, arg2[,   [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象. 说明: call ...

  8. 在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查

    Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用JavaScript开发并且能够用在Node.JS环境中,易于使用,支持多SQL方言(dialect),.它当前支持M ...

  9. 分析js中的constructor 和prototype

    在javascript的使用过程中,constructor 和prototype这两个概念是相当重要的,深入的理解这两个概念对理解js的一些核心概念非常的重要. 我们在定义函数的时候,函数定义的时候函 ...

  10. 如何在Node.js中合并两个复杂对象

    通常情况下,在Node.js中我们可以通过underscore的extend或者lodash的merge来合并两个对象,但是对于像下面这种复杂的对象,要如何来应对呢? 例如我有以下两个object: ...

随机推荐

  1. Oracle数据库imp

    创建表空间 create tablespace ICITY datafile 'D:\Oracle\oradata\orcl\ICITY.DBF' size 400M autoextend on ne ...

  2. linux下jdk_tomcat+mysql配置那点事

    (1)上传环境压缩包 apache-tomcat-7.0.47.tar.gz, jdk-7u71-linux-x64.gz, mysql-5.6.14.tar.gz通过xftp工具上传到/usr/lo ...

  3. SpringBoot使用Mybatis注解进行一对多和多对多查询(2)

    SpringBoot使用Mybatis注解进行一对多和多对多查询 GitHub的完整示例项目地址kingboy-springboot-data 一.模拟的业务查询 系统中的用户user都有唯一对应的地 ...

  4. HDU 4585 平衡树Treap

    点击打开链接 题意:给出n组数,第一个数是id.第二个数是级别.每输入一个.输出这个人和哪个人打架,这个人会找和他级别最相近的人打,假设有两个人级别和他相差的一样多,他就会选择级别比他小的打架. 思路 ...

  5. Android短轮询解决方案——CountDownTimer+Handler

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/7657194.html 一:应用场景 在诸如自动售卖机之类的扫码支付场景中,客户端在获得支付二维码或者发出支付请 ...

  6. recordMyDesktop的安装与使用

    今天给大家推荐一款能让桌面录像更简单的屏幕录像的工具:  RecordMyDesktop 安装 RecordMyDesktop RecordMyDesktop 的核心是一系列命令工具,不过 Gnome ...

  7. V-rep学习笔记:Reflexxes Motion Library 2

    VREP中的simRMLMoveToPosition函数可以将静态物体按照设定的运动规律移动到指定的目标位置/姿态.If your object is dynamically enabled, it ...

  8. Arduino从DHT11读取温湿度数据并显示在1602LCD

    硬件清单 Arduino NANO1602LCD + PCF8574T模块YL-47 DHT11模块 连线 1. 连接LCD: PCF8574T模块4pin(Gnd, Vcc, SDA i2c数据, ...

  9. Ubuntu16.04下添加打印机FujiXerox CP116w

    今天要打印一份北马的成绩单, 不想重启机器了, 在Ubuntu下尝试添加打印机, 最后成功了, 记录一下 打印机型号是FujiXerox CP116w, 通过WIFI连接的, 在Ubuntu16.04 ...

  10. boost asio异步读写网络聊天程序client 实例具体解释

    boost官方文档中聊天程序实例解说 数据包格式chat_message.hpp <pre name="code" class="cpp">< ...