JS深入04 关于[] == false是true的解析

我之前一直被一个问题所困扰 已知[]和{}都是true如下

if ([] && {}) {
      console.log(1);
    } else {
      console.log(2);
    }

打印如下

那为什么在if语句中还会出现[]==false为true的效果呢?现在我们来讨论一下,要弄懂这个问题我们需要一些前置知识,有了这些知识你一定会完全了解这个问题

第 1 章强制类型转换

强制类型转换是一个非常有争议的话题

1.1 值类型转换

将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐 式的情况称为强制类型转换(coercion)。

也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在 强制类型转换 | 47 动态类型语言的运行时(runtime)。

然而在 JavaScript 中通常将它们统称为强制类型转换,我个人则倾向于用“隐式强制类型 转换”(implicit coercion)和“显式强制类型转换”(explicit coercion)来区分。

二者的区别显而易见:我们能够从代码中看出哪些地方是显式强制类型转换,而隐式强制 类型转换则不那么明显,通常是某些操作产生的副作用。

例如:

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

对变量 b 而言,强制类型转换是隐式的;由于 + 运算符的其中一个操作数是字符串,所以 是字符串拼接操作,结果是数字 42 被强制类型转换为相应的字符串 "42"。

而 String(..) 则是将 a 显式强制类型转换为字符串。

两者都是将数字 42 转换为字符串 "42"。然而它们各自不同的处理方式成为了争论的焦点。

这里的“显式”和“隐式”以及“明显的副作用”和“隐藏的副作用”,都是相对而言的。 要是你明白 a + "" 是怎么回事,它对你来说就是“显式”的。

相反,如果你不知道 String(..) 可以用来做字符串强制类型转换,它对你来说可能就是“隐式”的。 

1.2 抽象值操作

该章目的是掌握字符串、数字和布尔值之间类型转换

1.2.1 ToString

规范的 9.8 节中定义了抽象操作 ToString,它负责处理非字符串到字符串的强制类型转换。

基本类型值的字符串化规则为:null 转换为 "null",undefined 转换为 "undefined",true 转换为 "true"。数字的字符串化则遵循通用规则,不过那些极小和极大的 数字使用指数形式:

// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"

对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回 内部属性 [[Class]] 的值,如 "[object Object]"。

然而如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起来: 

var a = [1,2,3];
a.toString(); // "1,2,3"

toString() 可以被显式调用,或者在需要字符串化时自动调用。

JSON 字符串化

工具函数 JSON.stringify(..) 在将 JSON 对象序列化为字符串时也用到了 ToString。

请注意,JSON 字符串化并非严格意义上的强制类型转换,因为其中也涉及 ToString 的相 关规则,所以这里顺带介绍一下

对大多数简单值来说,JSON 字符串化和 toString() 的效果基本相同,只不过序列化的结果总是字符串:

JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有双引号的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"

所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

为了简单起见,我们来看看什么是不安全的 JSON 值。undefined、function、symbol (ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。

JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在 数组中则会返回 null(以保证单元位置不变)。

例如:
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify(
 [1,undefined,function(){},4]
); // "[1,null,null,4]"
JSON.stringify(
 { a:2, b:function(){} }
); // "{"a":2}"

对包含循环引用的对象执行 JSON.stringify(..) 会出错。

如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回 值来进行序列化。

如果要对含有非法 JSON 值的对象做字符串化,或者对象中的某些值无法被序列化时,就 需要定义 toJSON() 方法来返回一个安全的 JSON 值。

例如:
var o = { };
var a = { 
 b: 42,
 c: o,
 d: function(){}
};
50 | 第 4 章
// 在a中创建一个循环引用
o.e = a;
// 循环引用在这里会产生错误
// JSON.stringify( a );
// 自定义的JSON序列化
a.toJSON = function() {
 // 序列化仅包含b
 return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"

很多人误以为 toJSON() 返回的是 JSON 字符串化后的值,其实不然,除非我们确实想要对字符串进行字符串化(通常不会!)。toJSON() 返回的应该是一个适当的值,可以是任何类型,然后再由 JSON.stringify(..) 对其进行字符串化。

也就是说,toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回 一个JSON 字符串”。

例如:


var a = {
 val: [1,2,3],
 // 可能是我们想要的结果!
 toJSON: function(){
 return this.val.slice( 1 );
 }
};
var b = {
 val: [1,2,3],
 // 可能不是我们想要的结果!
 toJSON: function(){
 return "[" +
 this.val.slice( 1 ).join() +
 "]"; 
 }
};
JSON.stringify( a ); // "[2,3]"
JSON.stringify( b ); // ""[2,3]""

这里第二个函数是对 toJSON 返回的字符串做字符串化,而非数组本身。

2.2.2 ToNumber

有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了 抽象操作 ToNumber。

其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。

ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法。处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的 十六进制数并不按十六进制处理(而是按十进制)。

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先 (通过内部操作 DefaultValue,检查该值是否有 valueOf() 方法。 如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

例如:

var a = {
 valueOf: function(){
 return "42";
 }
};
var b = {
 toString: function(){
 return "42";
 }
};
var c = [4,2];
c.toString = function(){
 return this.join( "" ); // "42"
};
Number( a ); // 42
Number( b ); // 42
Number( c ); // 42
Number( "" ); // 0
Number( [] ); // 0
Number( [ "abc" ] ); // NaN

1.2.3 ToBoolean

下面介绍布尔值,关于这个主题存在许多误解和困惑,需要我们特别注意。

首先也是最重要的一点是,JavaScript 中有两个关键词 true 和 false,分别代表布尔类型 中的真和假。我们常误以为数值 1 和 0 分别等同于 true 和 false。在有些语言中可能是这 样,但在 JavaScript 中布尔值和数字是不一样的。虽然我们可以将 1 强制类型转换为 true, 将 0 强制类型转换为 false,反之亦然,但它们并不是一回事。

1. 假值(falsy value)

我们再来看看其他值是如何被强制类型转换为布尔值的。

JavaScript 中的值可以分为以下两类:

(1) 可以被强制类型转换为 false 的值

(2) 其他(被强制类型转换为 true 的值)

JavaScript 规范具体定义了一小撮可以被强制类型转换为 false 的值。 ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的 结果。

以下这些是假值: 

• undefined
• null
• false
• +0、-0 和 NaN
• "

假值的布尔强制类型转换结果为 false。

从逻辑上说,假值列表以外的都应该是真值(truthy)。但 JavaScript 规范对此并没有明确 定义,只是给出了一些示例,例如规定所有的对象都是真值,我们可以理解为假值列表以 外的值都是真值。

2. 真值(truthy value)

真值就是假值列表之外的值。

例如:
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d;

这里 d 应该是 true 还是 false 呢?

答案是 true。上例的字符串看似假值,但所有字符串都是真值。不过 "" 除外,因为它是 假值列表中唯一的字符串。

再如:
var a = []; // 空数组——是真值还是假值?
var b = {}; // 空对象——是真值还是假值?
var c = function(){}; // 空函数——是真值还是假值?
var d = Boolean( a && b && c );
d;

d 依然是 true。

还是同样的道理,[]、{} 和 function(){} 都不在假值列表中,因此它们都 是真值。

也就是说真值列表可以无限长,无法一一列举,所以我们只能用假值列表作为参考。

你可以花五分钟时间将假值列表写出来贴在显示器上,或者记在脑子里,在需要判断真 / 假值的时候就可以派上用场。

掌握真 / 假值的重点在于理解布尔强制类型转换(显式和隐式),在此基础上我们就能对强制类型转换示例进行深入介绍。

1.3   宽松相等和严格相等

宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,但是它们之间有一个很重要的区别,特别是在判断条件上。

常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而 还不够准确。很多 JavaScript 的书籍和博客也是这样来解释的,但是很遗憾他们都错了。

正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”

1.3.1 抽象相等

ES5 规范 11.9.3 节的“抽象相等比较算法”定义了 == 运算符的行为。该算法简单而又全面,涵盖了所有可能出现的类型组合,以及它们进行强制类型转换的方式。

其中第一段(11.9.3.1)规定如果两个值的类型相同,就仅比较它们是否相等。例如,42 等于 42,"abc" 等于 "abc"。

有几个非常规的情况需要注意

• NaN 不等于 NaN。

• +0 等于 -0。

== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之 一或两者都转换为相同的类型后再进行比较。

1. 字符串和数字之间的相等比较

我们沿用本章前面字符串和数字的例子来解释 == 中的强制类型转换:

var a = 42;
var b = "42";
a === b; // false
a == b; // true

因为没有强制类型转换,所以 a === b 为 false,42 和 "42" 不相等。

而 a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型 转换。

具体怎么转换?是 a 从 42 转换为字符串,还是 b 从 "42" 转换为数字?

ES5 规范 11.9.3.4-5 这样定义:

(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。

(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

根据规范,"42" 应该被强制类型转换为数字以便进行相等比较。相关规则,特别是 ToNumber 抽象操作的规则前面已经介绍过。本例中两个值相等,均为 42。

2. 其他类型和布尔类型之间的相等比较

== 最容易出错的一个地方是 true 和 false 与其他类型之间的相等比较。

例如:
var a = "42";
var b = true;
a == b; // false

我们都知道 "42" 是一个真值(见本章前面部分),为什么 == 的结果不是 true 呢?原因既简 单又复杂,让人很容易掉坑里,很多 JavaScript 开发人员对这个地方并未引起足够的重视

规范是这样说的:

(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;

(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

仔细分析例子,首先:

var x = true;
var y = "42";
x == y; // false

Type(x) 是布尔值,所以 ToNumber(x) 将 true 强制类型转换为 1,变成 1 == "42",二者的 类型仍然不同,"42" 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false。

反过来也一样:

var x = "42";
var y = false;
x == y; // false

Type(y) 是布尔值,所以 ToNumber(y) 将 false 强制类型转换为 0,然后 "42" == 0 再变成 42 == 0,结果为 false。

也就是说,字符串 "42" 既不等于 true,也不等于 false。

一个值怎么可以既非真值也非假 值,这也太奇怪了吧? 这个问题本身就是错误的,我们被自己的大脑欺骗了。

"42" 是一个真值没错,但 "42" == true 中并没有发生布尔值的比较和强制类型转换。这里 不是 "42" 转换为布尔值(true),而是 true 转换为 1,"42" 转换为 42。

这里并不涉及 ToBoolean,所以 "42" 是真值还是假值与 == 本身没有关系!

重点是我们要搞清楚 == 对不同的类型组合怎样处理。== 两边的布尔值会被强制类型转换 为数字。 很奇怪吧?我个人建议无论什么情况下都不要使用 == true 和 == false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值