【原文地址】https://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/
一篇 2011 年的老文章,很赞同作者说的 you can’t master a language until you know it inside out – and fear and evasion are the enemies of knowledge. 阅读之,学习之。
先看几个老鸟也会觉得很 confuse 的例子
if( [ 0 ] ) {
console.log( [0] == true ); // false
console.log( !![0] ); // true
}
if( 'potato' ) {
console.log( 'potato' == false ); // false
console.log( 'potato' == true ); // false
}
上面的例子看来使用 JavaScript 进行判断的时候结果常常是出乎意料的。有个好消息是已经有标准文档规定这些表达式应该返回怎样的值,并且所有的浏览器都将遵循该规则进行实现。
JavaScript 中关于 truth
和 equality
的求值主要有以下三种情况:
- conditional statements and operators( 条件申明和操作:
if
,? :
,||
,&&
etc ) - the equal operator( 等于操作符:
==
) - the strict equals operator( 严格等于操作符:
===
)
Conditional 条件声明和操作
在 JavaScript 中,所有的条件申明操作遵循相同的取值规则。下面将以 if
为例解释这个规则。
if
声明(表达式)会使用 ES5 中定义的一个抽象方法 ToBoolean 将其声明(表达式) 转换为一个布尔类型的值。(ecma-262 (6,7th edition 在 7.1.2, 5th edition 在 9.2 ))[http://www.ecma-international.org/ecma-262/7.0/index.html#sec-toboolean]
| 参数(表达式)类型 | 结果 |
|: --- :|: --- :|
| Undefined | false |
| Null | false |
| Boolean | 传入参数值(不做类型转换)|
| Number | +0, -0 和 NaN 会被当做是 false,其余值都是 true |
| String | 空字符串(长度为0) 会被当做是 false, 其余值都是 true |
| Object | 所有对象都会被当做是 true |
这是 JavaScript 所使用的判断一个值是 truthy (eg: true
, 'potato'
, 36
, [12, 34]
) 或 falsey (eg: false
, 0
, ''
) 的规则。
看到这里就能明白为什么 if([0])
会允许进入后面的代码块执行了 (数组是一个对象,对象都被按照 true 解析)。
再来看一些或许会让你觉得奇怪,但是却是符合上面描述的规则的例子:
var trutheyTester = function(expr) {
return expr ? 'truthey' : 'falsey';
}
trutheyTester({}); // truthey( 对象都被当做true )
trutheyTester(false); // falsey
trutheyTester(new Boolean(false)); // truthey (new Boolean(false) 返回的是对象)
trutheyTester(''); // falsey
trutheyTester(new String('')); // truthey
trutheyTester(NaN); // falsey
trutheyTester(new Number(NaN)); // truthey
The Equals Operator(==) 等于操作符
==
型的等于操作符(以下称为非严格等于)是非常自由的(liberal)。两边的值有可能根本就会是不同的类型,但运算却有可能返回 true 的结果。因为非严格等于操作会对将要进行比较运算的一边或者两边进行强制类型转换再进行比较(通常都是装换为number型的值)。
很明显使用 == 操作符比较不同类型的值,且可能返回 true 的结果,很酷,但同时也很危险。我们的某一位 JavaScript 领域专家建议完全不要使用 ==
运算符。
这种避免使用 ==
的建议我并不是十分赞同,因为我认为只有深入的学习这门语言你才能算是掌握(master)了它。回避是真正掌握知识的敌人。
而且,即使我们假装 ==
操作符并不存在,你也依然无法摆脱 JavaScript 中大量存在的类型装换问题。如果能够恰当地使用 ==
进行比较,将会是一个很好的工具,用来创建出更简洁,优雅和可读的代码。
无论如何,让我们来看看 ECMA 是如何定义 ==
运算符的标准表现的。它实际上并没有想象中那么吓人,只需要记住使用 ==
比较的时候 undefined 和 null 是相等的,大多数情况下其他类型的参数会被转换成数字类型用来帮助进行更好的比较。
| type(x) | type(y) | result |
|: --- :|: --- :|: --- :|
| null | undefined | true |
| undefined | null | true |
| Number | String | x == toNumber(Y) |
| String | Number | toNumber(x) == y |
| Boolean | (any) | toNumber(x) == y |
| (any) | Boolean | x == toNumber(y) |
| String or Number | Object | x == toPrimitive(y) |
| Object | String or Number | toPrimitive(x) == y |
| (any) | (any) 和 x 类型相同 | 参考 x === y
|
| 其他一些情况 | ... | false |
只要得到的结果还是一个表达式,该算法就会重新执行,直到最终返回布尔型的结果为止。
toNumber 和 toPrimitive 是两个内置方法,按照下面的规则对传入的参数进行转换。
toNumber
| Argument Type | Result |
|: --- :|: --- :|
| Undefined | NaN |
| Null | +0 |
| Boolean | true -> 1, false -> +0 |
| Number | 传入参数,不做转换 |
| String | 'abc' -> NaN, '123' -> 123 |
| Object | 先使用 toPrimitive 装换对象值,然后在对 toPrimitive 得到的值进行,toNumber 运算|
toPrimitive
| Argument Type | Result |
|: --- :|: --- :|
| Object | 在 比较运算中,如果 valueOf
方法返回一个原始类型值,返回这个值。如果 toString
方法返回 原始类型值,返回这个值。如果前面两个方法都不返回原始类型值,抛出一个错误 |
| otherwise ... | 不进行类型转换,直接返回传入的参数 |
学习了上面的规则,来看几个实际的例子:
[0] == true;
[0] == true; // false
// 伪码
// 1. 布尔型 使用 toNumber 转换
[0] == 1;
// 2. 对象使用 valueOf 或 toString 获得原始类型值
'0' == 1;
// 3. string 使用 toNumber 装换
0 == 1; // false
'potato' == true;
'potato' == true; // false
// 伪码
// 1. 布尔型 使用 toNumber 转换
'potato' == 1;
// 2. string 使用 toNumber 转换
NaN == 1; // false
'potato' == false;
'potato' == false; // false
// 伪码
// 1. 布尔型 使用 toNumber 转换
'potato' == 0;
// 2. string 使用 toNumber 转换
Object with valueOf
crazyNumeric = new Number(1);
crazyNumeric.toString = function() { return '2' };
crazyNumeric == 1; // true
// 伪码
// 1. 对象 使用 valueOf 或 toString 转换
// 1 == 1; // true
Object with toString
crazyNumeric = {
toString: function() { return '2' };
}
crazyNumeric == 1;
// 伪码
// 1. 对象 使用 valueOf 或 toString 转换
// 2 == 1; // false
两边类型装换总是按照这样的顺序进行 Boolean > Object( valurOf > toString ) > String(译者参考后面提供的 javascript == 操作符比较流程工具 添加的内容)
The Strict Equals Operator( === ) 严格等于
这种情形会比较简单。如果是对不同类型进行比较,结果永远都会是 false。
如果比较两边是同一数据类型的将使用一个直观的规则进行比较: 对象比较,必须两边都是同一对象的引用时才会得到 true 的结果; 字符串必须包含完全相同的字符集(contain identical character sets)才会得到 true 的结果; 其他的原始类型必须值相同才能返回 true 的结果。
NaN
, null
, undefined
三者都不会存在一个严格相等的组合。 NaN
甚至都不等于它自身(NaN 是 JavaScript 中唯一不等于自身的值)。
一些本不需要使用 ===
的场景
// unnecessary
if ( typeof myVar === 'function' ) { /* ... */ }
// better
if ( typeof myVar == 'function' ) { /* ... */ }
typeof
表达式返回一个 string 类型的值,这个操作永远都是比较两个字符串。因此==
在这种情况下永远都能得到正确的比较结果。
// unnecessary
var missing = ( myVar === undefined || myVar === null );
// better
var missing = ( myVar == null );
==
中undefined
和null
彼此相等。
在某些 ES 实现中,undefined
是可以修改的, 因此和 null 进行比较更为安全。
// unnecessary
if ( myArr.length === 3 ) { /* ... */ }
// better
if ( myArr.length == 3 ) { /* ...*/ }
都是数字的比较,
==
在这种情况下永远都能得到正确的比较结果。
扩展阅读:
- Peter van der Zee: JavaScript coercion tool
一个很好的==
比较流程总结,并提供一个javascript == 操作符比较流程工具以供更好的理解。 - Andrea Giammarchi: JavaScript Coercion Demystified
- ECMA-262 5th Edition