Js 中的相等判断及类型转换

咱们都知道 JavaScript 是一种弱类型或者说动态语言

这个特性就注定了我们需要在不断的踩坑中得到真知,接下来说一些 JavaScript 中判断相等、全等,某些类型的识别,以及隐式转换这些问题。

👍🏻首先先强推一个网站:JavaScript loose comparison (==) step by step,在里面,我们可以更详细的、动态的了解 == 比较的过程。

获取数据类型

为了让我们更好的了解什么是数据类型,我们可以通过以下方式得到数据类型:

Object.prototype.toString.call(value).slice(8, -1)
// Object.prototype.toString.call(value) 可以得到 [object TYPE](大写开头字符串)

value.__proto__.constructor.name
// 可以得到 TYPE(大写开头字符串)

typeof value
// typeof 可以得到的结果为 "number"、"string"、"boolean"、"function"、"object"、"undefined"、"symbol"(全小写字符串)

=== 全等判断规则

  1. 如果「类型」不同,就不相等

  2. 如果其中至少有一个是 NaN,那么不相等

  3. 如果两个值都是「数值」,并且是同一个值,那么相等;(! 例外)

  4. 如果两个值都是「字符串」,且每个位置的字符都一样,那么相等

  5. 如果两个值都是 true,或者都是 false,那么相等

  6. 如果两个值都是 null,或者都是 undefined,那么相等

  7. 如果两个值都是「对象」,且都引用自同一个对象或函数,那么相等

全等总结

  1. 不同类型的值

    如果两个值的类型不同,直接返回 false

  2. 同一类型的原始类型

    同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回 true,值不同就返回 false。

    ❗️需要注意的是,NaN任何值都不相等(包括自身)

    ❗️另外,+0 等于 -0,即:+0 === -0 // true

  3. 同一类型的复合类型值

    同一类型的复合类型的值(对象、数组、函数)比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象( 是否为同一引用 )。

  4. undefinednull

    undefinednull自身严格相等,但是两者不严格相等

== 相等判断规则

如果两个值「类型」相同,进行 === 比较

如果两个值「类型」不同,他们可能相等。会根据下面规则进行类型转换再比较(其中包含隐式转换):

  1. 如果一个是「字符串」,一个是「数值」,把字符串转换成数值后再进行比较;

  2. 如果任一值是 true,把它转换成 1 再比较;如果任一值是 false,把它转换成 0 再比较;

  3. 如果一个是 null、一个是 undefined,那么相等

  4. 如果一个是「对象」,另一个是「数值」或「字符串」,通过把 对象转换成基础类型 后,再对两个基础类型的值再比较。

任何其他组合,都「不相等」。

相等总结

  1. 原始类型的值

    对于原始类型的数据,都会转换成数值(Number)类型再进行比较。(如 相等判断规则 1、2 点)

  2. 对象原始类型值比较

    进行 对象转换成基础类型 操作。

  3. undefinednull

    undefinednull其他类型的值比较 == 时,结果都为 false

    它们互相比较时结果为 true,因为都表示「无」,但两者不「严格相等」,因为本质上还是不一样的,一个是「未定义」,一个是「空」。

Object.is(value1, value2)

判断两个值是否是相同的值;相比于 == 比较,Object.is 不会做类型转换(包括隐式转换)。

Object.is 对待数字比较特殊,会区分 +0-0 以及 NaNNaN 判断为相等。

  • 两个值都是 undefined
  • 两个值都是 null
  • 两个值都是 true 或者都是 false
  • 两个值是由相同个数的字符按照相同的顺序组成的字符串;
  • 两个值指向同一个对象;
  • 两个值都是数字并且:;
    • 都是正零 +0
    • 都是负零 -0
    • 都是 NaN
    • 都是除零和 NaN 外的其它同一个数字。
Object.is('foo', 'foo');     // true
Object.is(window, window);   // true

Object.is('foo', 'bar');     // false
Object.is([], []);           // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is(null, null);       // true

// 特例
Object.is(0, -0);            // false
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true
Object.is Polyfill
if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) { // Steps 1-5, 7-10
      // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // Step 6.a: NaN == NaN
      return x !== x && y !== y;
    }
  };
}

详见:Object.is() - JavaScript | MDN

对象转换成基础类型

如果运算子是对象,会转为原始类型的值,再进行比较。

「对象」转换成「原始类型」的值,算法如下:

  1. 先调用对象的 valueOf() 方法,得到返回值,返回值为原始类型则转换成功;

  2. 如果返回的不是原始类型,再接着调用的 toString() 方法,得到原始类型;

  3. 如果上面两步都得不到原始类型,则报错

null 和 undefined

常见的 nullundefined 的判断:

// 全等、相等比较
null == undefined // true
null === undefined // false

// null 和 undefined 布尔化后都为 false
!!null === false
!!undefined === false

// 转化为数值
+null === 0 // true
+undefined // NaN

// 和其他值比较都为 false
undefined == false // false
null == false // false

// typeof
typeof null // "object"
typeof undefined // "undefined"

其实我们在开发的过程中,感受到的区别并不是那么大,但是为什么要有两个不同的类型呢?

阮老师说:

最开始的时候 null 像在 Java 里一样,被当成一个对象,可以自动转为 0

但是,JavaScript 的数据类型分成原始类型(primitive)和复合类型(complex)两大类,Brendan Eich觉得表示「无」的值最好不是对象

其次,JavaScript 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败

Brendan Eich 觉得,如果 null 自动转为0,很不容易发现错误。

因此,Brendan Eich 又设计了一个 undefined

null

表示「没有对象」,表示该值被定义了,被定义为「空」

典型用法是:

  1. 作为函数的参数,表示该函数的参数不是对象

  2. 作为对象原型链的终点

undefined

表示「缺少值」,表示该值还没有定义,所以一个值被定义为 undefined 是不合适的。

典型用法是:

  1. 变量被声明了,但没有赋值时,就等于 undefined

  2. 调用函数时,应该提供的参数没有提供,该参数等于 undefined

  3. 对象没有赋值的属性,该属性的值为 undefined

  4. 函数没有返回值时,默认返回 undefined

❗️注意:

当我们将一个变量或值与 undefined 比较时,实际上是与 window 对象的 undefined 属性比较。

这个比较过程中,JavaScript 会搜索 window 对象名叫 undefined 的属性,然后再比较两个操作数的引用指针是否相同。

在需要频繁与 undefined 进行比较的函数中,这可能会是一个性能问题点。

function anyFunc() { 
  var undefined // 自定义局部 undefined 变量,不赋值就等于 `undefined`,下面正常使用

  if(x === undefined) { console.log(undefined) } // 作用域上的引用比较 
  
  while(y !== undefined) { console.log(undefined) } // 作用域上的引用比较 
}

这就是许多前端 JS 框架为什么常常要自己定义一个局部 undefined 变量的原因。

jQuery:jquery · GitHub

( function( window, undefined ) {
  // core code with using undefined
  // ...
} )( window );

NaN

NaN(Not a Number),表示不是一个数字,它是 Number 对象上的一个静态属性,可以通过 Number.NaN 来访问。

// 转换为 Number 类型时产生
+'abc'      // NaN
+{}         // NaN
+undefined  // NaN

// 计算得出 NaN
0/0           // NaN
undefined + 1 // NaN
'abc' * 5     // NaN

typeof NaN // "number"

判断 NaN

  • 使用 Object.isObject.is(value, NaN)

  • 使用 Number.isNaNNumber.isNaN(value)

  • 使用自身特性:value !== value

隐式转换

其实隐式转换可以单独拿出来写一篇文章,但是为了更系统的介绍隐式转换,所以将它归到这里的一个二级标题。

运算符优先级

关于运算符优先级,我这里就不重复搬运了,见 MDN 参考:运算符优先级 - JavaScript | MDN

这里说几个常用的,从上到下从高到低排序,同级内按书写时从左到右排序:

  1. () 子表达式;
  2. []. 成员访问,() 函数调用;
  3. ...++...-- 后置递增、递减;
  4. ~ 按位非、! 逻辑非、+... 一元加法、-... 一元减法、--...++... 前置递增、递减;
  5. * 乘法、/ 除法、% 取模;
  6. +- 加减法;
  7. ==!====!== (非)相等、全等判断符;
  8. && 逻辑与;
  9. || 逻辑或;
  10. ...?...:... 三元运算;

加号运算符

  1. 首先执行代码,调用 对象转换成基础类型 方法得到原始值;

  2. 如果两个原始值都是「数字」,则直接相加得出结果。

  3. 如果两个原始值都是「字符串」,把第二个字符串连接到第一个上,也就是相当于调用 concat 方法。

  4. 如果只有一个原始值是「字符串」,调用 toString 方法把另一个运算数转换成「字符串」,结果是两个字符串连接成的字符串。

总结:只要是加号运算符,两边都转为基础类型;但是需要注意 {} 是对象还是代码块

{} 是对象还是代码块

先举一些例子:

{a: 1}[1] // [1]

{a: 1}['a'] // ["a"]

({a: 1})['a'] // 1

({a: 1,})['a'] // 1

{a: 1,}['a'] // SyntaxError

{a, 1}[] // []

特点:

  • {} 前面有运算符号的时候,+-*/() 等等,{} 都会被解析成「对象字面量」;

  • {} 前面没有运算符时候但; 结尾的时候,或者浏览器的自动分号插入机制{} 后面插入分号 ; 的时候,此时 {} 都会被解析成「代码块」。

其实 {a: 1} 这种代码块的写法,相当于 goto 语句,😑

来验证下吧:

{}      // {} | 对象
{};     // undefined | 代码块
+{}     // NaN | 对象
{}+{}   // "[object Object][object Object]" | 对象+对象
{}+{};  // NaN | 代码块+对象

也就是说 {} 后有 ; 结尾,前无符号,{} 才会被翻译为「代码块」

类型转换举例

[] + {} // "[object Object]"
{} + [] // 0

{} + 0 // 0
0 + {} // "0[object Object]

其中 {} + [] = 0 开头 {}不是空对象的字面量,而是被当作空的代码块

事实上这个表达式的值就是 +[] 的结果,即 Number([].join(',')),即为 0

接下来咱们来看看下面的内容:

{}+1 // 1
// 正确:{} 后有自动插入分号,且前无运算符,看做代码块

({}+1) // "[object Object]1"
// 错误:1,把 {} 看做代码块
// 正确:{} 前有括号,看做对象,调用对象的 `({}.toString)` 得到 `"[object Object]"`

1+{} // “1[object Object]”
// 正确:{} 前有 `+` 号,看做对象,调用对象的 `({}.toString)` 得到 `"[object Object]"`

[]+1 // "1"
// 正确:`+` 号两边转为原始类型,`[].toString()`,得到 `""`

1+[] // "1"
// 错误:1,把 [] 转换成了 Number
// 正确:`+` 号两边转为原始类型,`[].toString()`,得到 `""`

1-[] // 1
// 正确:`-` 号两边均转为 Number 类型,`Number([])` 得到 `0`

1-{} // NaN
// 正确:`-` 号两边均转为 Number 类型,`Number({})` 得到 `NaN`

1-!{} // 1
// 正确:`!` 一元逻辑非的优先级高于 `-` 二元减法,所以先进行 `![]` 运算,得到 `false`,`1-false` 将 `false` 转为 `Number` 得到 `0`

1+!{} // 1
// 正确:`!` 一元逻辑非的优先级高于 `-` 二元加法,所以先进行 `![]` 运算,得到 `false`,`1+false` 将 `false` 转为 `Number` 得到 `0`

1+"2"+"2" // "122"
// 正确:只要加号两边有一个字符串就全部转为字符串进行拼接

1+ +"2"+"2" // "32"
// 错误:"122",当做 1+ (2+"2")
// 正确:`1+` 后有空格,第一个 `+"2"` 前的 `+` 为一元加法,优先级高,相当于进行 `1+(+"2")+"2"`,得到 `1+2+"2"` 相当于 `3+"2"`

1++"2"+"2" // ReferenceError
// 错误:NaN
// 正确:++后面需要跟一个引用类型


[]==![] // true
// 错误:false,当做两边均转为 Number,一边 1 一边 0
// 正确:先进行布尔运算,得到 `[]==false` => `""==false` => `0==0`

[]===![] // false
// 正确:先进行布尔运算,得到 `[]===false`

反思:不知道 []{} 什么时候该当做字面量,什么时候该当做数字,什么时候该当做布尔值,什么时候该当做字符;对执行顺序也不够掌握。

再来举一个比较少见的例子:

{}+[]==[]+{} // true
{}+[]==[]+{}; // false

首先我们可以确定的是 + 二元加法运算符是比 == 相等判断符的优先级高的;

至于为什么会这样,我们可以做几个实验:

// 针对 {}+[]==[]+{} 为 true
({}+[])==[]+{}  // true 1. 这个可以说明前面的 `{}+[]` 中 `{}` 被解析为「对象」
{}+[]==[]       // true 2. 结合 4,可以得知这里的 `{}` 为「代码块」
+[]==[]         // true 3. 可以得知 2 中的 `{}` 是「代码块」
({}+[])==[]     // false 4. 结合 2、3 可以说明,在后面没有 `{}` 的时候,第一个 `{}` 被解析为「代码块」

// 针对 {}+[]==[]+{}; 为 false
({}+[])==([]+{})  // true 5. 证明原式不是两个对象。

({}+[])==[]+{};   // true 6. 可以得知此时后面的 `{}` 的是「对象」,也就是原式中第二个 `{}` 也是「对象」
{}+[]==([]+{});   // false 7. 可以得知此时前面的 `{}` 的是「代码块」,也就是原式中第一个 `{}` 也是「代码块」

{}+[]=={}+[]      // false 8. 配合 9 可以得知这里第一个 `{}` 为「代码块」
{}+[]=={}+[];     // false 9. 配合 8、10 可以得知这里第一个 `{}` 为「代码块」
({}+[])=={}+[];   // true 10. 

+[]==[]+{}        // false 11. 
+[]==[]+{};       // false 12. 配合 11,结合原式,可以得知原式的第一个 `{}` 是「代码块」

针对 {}+[]==[]+{}true,我们结合 1234 可以得知,第一个 {} 和第二个 {} 都被解析为了「对象」;

针对 {}+[]==[]+{};true,可以得知第一个 {} 是「代码块」,第二个 {} 是「对象」;

现在因为最后的分号 ;,让第一个 {} 改变为「代码块」。

至于为啥会这样,等日后知识更加丰富后再来解答。

验证分析 ++[[]][+[]]+[+[]]==10

  1. 根据优先级拆分出 + 二元加号

    (++[[]][+[]])
    +
    ([+[]])
    
  2. 先分析左边:++[[]][+[]]

    1. [] 成员访问的优先级高,先计算 [[]][+[]]

    2. +[] 相当于 Number([]),得到 0;原式相当于 ++[[]][0]

    3. [[]][0] 显而易见,结果是取 [[]] 数组中的第一位,即 [];原式相当于 ++[]

    4. 最后来分析 ++[]

      第一眼看到确实不知道是什么,放到控制台也报错:

      报错

      Uncaught ReferenceError: Invalid left-hand side expression in prefix operation

      [引用 jawil 的 从+++++==10?深入浅出弱类型JS的隐式转换]:

      在英文版的 ECMAScript 规范中要求(前缀、后缀)自增的内容,必须是一个引用,才不会报出上面的错。

      [[]][0] 是对象的属性访问,拿到的 [] 自然是一个引用;

      所以,++[] 相当于 Number([])+1,得到 1

    5. 所以左边 ++[[]][+[]] = 1

  3. 再分析右边:[+[]]

    这个就比较简单了,上面也分析过,+[]0,结果为 [0]

  4. 结合左右两边式子,得到 1+[0],其中 [0] 进行 ToPrimitive 对象转换成基础类型,得到 "0",原式相当于 1+"0",故得 "10"

  5. "10" == 10 就没什么好说的啦!

要点快速总结

  1. 只要是加号运算符...+...),两边都转为基础类型;但是需要注意 {} 是对象还是代码块

  2. == 对于原始类型的数据,都会转换成数值(Number)类型再进行比较;(如 相等判断规则 1、2 点)

  3. 判断 {} 是否是「代码块」只需要看后面有没有分号(或自动插入的分号),{} 前无运算符号;其他情况就是「对象字面量」;

  4. NaNNumber 对象上的一个静态属性;与任何值都不相等;

  5. undefinednull自身严格相等,但是两者不严格相等;与任何其他值都不相等;

  6. 熟悉 运算符优先级

参考资料

JavaScript 中「相等运算符」和「严格相等运算符」的规则和关系 | Lin’s Blog

JavaScript 中的相等性判断 - JavaScript | MDN

JavaScript 数据类型和数据结构 - JavaScript | MDN

undefined与null的区别 - 阮一峰的网络日志

JS基础之undefined与null的区别分析 - 你志哥 - SegmentFault 思否

从+++++==10?深入浅出弱类型JS的隐式转换 · Issue #5 · jawil/blog · GitHub

JavaScript 奇怪事件簿 - 掘金

上次更新: 3/19/2019, 12:36:26 PM