Chapter4-coercion
You Don't Know JS: Types & Grammar
Chapter 4: Coercion
现在我们更充分地理解了JavaScript的类型和价值,我们将注意力转向一个非常有争议的话题:强制。
正如我们在第1章中提到的那样,关于强制是否是一个有用的特征或语言设计中的缺陷(或介于两者之间的某个方面的争论)的讨论自第一天起就肆虐。如果你已经读过关于JS的其他流行书籍,你就会知道那里绝大多数流行的信息是强制是神奇的,邪恶的,令人困惑的,而且只是一个坏主意。
在本书系列的整体精神中,不要因为其他人的所作所为,或者因为你被某些怪癖所困扰而逃避胁迫,我认为你应该朝着你不理解的方向奔跑并寻求更充分地理解它。
我们的目标是充分探索强制的利弊(是的,有优点!),以便你可以在程序中做出明智的决定。
Converting Values
将值从一种类型转换为另一种类型通常在显式完成时称为“类型转换”,在隐式执行时称为“强制”(由使用值的规则强制执行)。
注意: 这可能并不明显,但JavaScript强制总是会产生基本类型(参见第2章)中的一个值,如string
, number
, 或 boolean
。强制的结果没有会像object
或function
这样复杂的值。第3章介绍了“拳击”,它将原始值包含在它们的对象中,但这并不是真正的强制性。
这些术语经常被区分的另一种方式如下:“类型转换(type casting或type conversion)”在编译时以静态类型语言出现,而“类型强制(type coercion)”是动态类型语言的运行时转换。
然而,在JavaScript中,大多数人将所有这些类型的转换称为强制,因此我更喜欢区分的方式是说“隐式强制(implicit coercion)”与“明确强制(explicit coercion)”。
差异应该是显而易见的:“显式强制”是指通过查看代码时,有意而为之 发生的类型转换,而“隐性强制”是指类型转换将作为其他一些有意操作的不明显的副作用而发生的。
例如,考虑这两种强制方法:
对于b
,发生的强制(coercion, 笔:这里应该叫做conversion(转换)会更贴切)是不明显的,因为+
运算符与其中一个操作数结合为string
值(""
)将操作为string
连接(将两个字符串加在一起),作为(隐藏的)副作用将强制a
中的42
值被强制为其等效的string
:"42"
。
相比之下,String(..)
函数很明显的,它很明确的的将a
中的值转换为string
表示形式。
这两种方法都可以达到同样的效果:"42"
来自42
。但是,这是关于JavaScript强制的激烈争论的核心。
注意: 从技术上讲,除了风格差异之外,还有一些微妙的行为差异。我们将在本章后面的“隐式:字符串< -- >数字”部分中详细介绍。
术语“显性”和“隐含”或“明显”和“隐藏的副作用”是相对的。
如果你确切地知道a + ""
正在做什么,并且您有意这样做以强制转换为string
,你可能会觉得该操作足够“明确”。相反,如果你从未见过用于强制成string
的String(..)
函数,它的行为可能看起来足够隐蔽,以至于感觉“隐含”给你。
但是我们正在根据大众的,合理的,但不是专家或JS规范的奉献者开发者的意见来讨论“显性”与“隐含”。无论你的程度如何,或是在这个范畴内没有找到适合的位置,你都需要根据我们在这里的观察方式,相应地调整你的角度。
请记住:我们编写代码并且是唯一阅读代码的人,这通常是很少见。即使你是JS的所有细节的专家,考虑一下你的经验不足的队友在阅读你的代码时会有什么感受。他们对于“明确的”还是“隐含的”的理解,是否和你一样?
Abstract Value Operations
在我们探索 显式 与 隐式 强制之前,我们需要学习一些基本规则,是它们控制着值如何 变成 一个 string
、number
、或 boolean
的。ES5规范的第9节中定义了几个带有值转换规则的“抽象操作”(花哨规范,对于“仅内部操作”)。我们会特别注意: ToString
, ToNumber
, 和 ToBoolean
, 并提及下ToPrimitive
。
ToString
当任何非string
值被强制转换为string
表示时,转换将由规范的第9.8节中的ToString
抽象操作处理。
置原始值具有自然的字符串化:null
变成"null"
,undefined
变成"undefined"
,true
变成"true"
。number
通常以你所期望的自然方式表示,但正如我们在第2章中讨论的那样,非常小或非常大的数字以指数形式表示:
对于常规对象,除非你指定自己的对象,否则默认的toString()
(位于Object.prototype.toString()
中)将返回内部[[Class]]
(参见第3章),例如"[object Object]"
。
但是如前所示,如果一个对象上有自己的toString()
方法,并且你以类似string
的方式使用该对象,它的toString()
将自动被调用,并且将使用该调用的string
结果。
注意: 对象被强制转换为string
的方式在技术上通过ToPrimitive
抽象操作(ES5规范,第9.1节),但是本章后面的ToNumber
部分将详细介绍这些细微差别的细节,因此我们将在此处跳过它们。
数组有一个重写的默认值toString()
,它将所有值的字符串(字符串)串联起来(每个字符串化自己),每个值之间都有","
:
同样,toString()
可以显式调用,也可以在string
上下文中使用非string
时自动调用。
JSON Stringification
与ToString
非常相关的另一个操作是当你使用JSON.stringify()
将值序列化为与JSON兼容的string
值时。
重要的是要注意,这种字符串化与强制不完全相同。但由于它与上面的ToString
规则有关,因此我们将略微转移以涵盖JSON字符串化行为。
对于大多数简单值,JSON字符串化的行为与toString()
转换的行为基本相同,只是序列化结果始终是一个string
:
任何JSON安全值都可以通过JSON.stringify(..)
进行字符串化。但什么是JSON安全?任何可以用 JSON 表现形式合法表达的值。
考虑 非 JSON安全的值可能更容易。一些例子:undefined
,function
,symbol
(ES6+)和具有循环引用的object
(其中对象结构中的属性引用通过彼此创建永不停止的循环)。这些都是标准JSON结构的非法值,主要是因为它们不能移植到使用JSON值的其他语言。
JSON.stringify(..)
在遇到undefined
,symbol
, function
值的时候会自动忽略。如果在array
中找到这样的值,则该值将替换为null
(以便不更改数组位置信息)。如果发现是object
的属性,则只会排除该属性。
考虑下面代码:
但是如果你尝试对带有循环引用的object
使用 JSON.stringify()
,则会引发错误。
JSON字符串化具有以下特殊行为:如果object
值定义了toJSON()
方法,则首先调用此方法以获取用于序列化的值。
如果你JSON字符串化一个包含非法JSON值的对象,或者object
中有个不适合序列化的值,那么你就应该为他定义一个toJSON()
方法,它返回一个JSON安全版本的object
。
例如这样:
这是一个非常常见的误解,即toJSON()
应该返回一个JSON字符串化表示。这可能是不正确的,除非你想要实际字符串化string
本身(通常不是!)。toJSON()
应该返回适当的实际常规值(任何类型),JSON.stringify(..)
本身将处理字符串化。
换句话说,toJSON()
应解释为“适用于字符串化的JSON安全值”,而不是“JSON字符串”,正如许多开发人员错误地假设的那样。
考虑下面代码:
在第二次调用中,我们将返回的string
而不是array
本身进行字符串化,这可能不是我们想要做的。
在我们讨论JSON.stringify(..)
时,让我们讨论一些鲜为人知的功能,这些功能仍然非常有用。
JSON.stringify(..)
的第二个参数是可选的,叫做 replacer 。这个参数可以是object
或function
。它用于通过提供过滤机制来定制object
的递归序列化,过滤机制是应不应该包含属性,其方式与toJSON()
如何为序列化准备值类似。
如果 replacer 是一个array
,它应该是一个string
的array
,每个字符串都将指定一个允许包含在object
序列化中的属性名称。如果存在不在此列表中的属性,则将跳过该属性。
如果 replacer 是一个function
,它将为object
本身调用一次,然后object
中的每个属性被调用一次,并且每次传递两个参数,key和value。要跳过序列化中的键(key),请返回undefined
。否则,返回提供的值。
注意: 在function
作为 replacer 的情况下,第一次调用时 key 参数 k
是 undefined
(而对象 a
本身会被传入)。if
语句 过滤掉 名为"c"
的属性。字符串化是递归的,因此[1,2,3]
数组将其每个值(1
,2
和3
)作为v
传递给 replacer ,索引(0
,1
和2
)为k
。
第三个可选参数也可以传递给JSON.stringify(..)
,称为 空间(space) ,它用作缩进,用于更漂亮的人性化输出。space 可以是一个正整数,表示每个缩进级别应使用多少空格字符。或者,space 可以是一个string
,在这种情况下,每个缩进级别将使用其值的前十个字符。
请记住,JSON.stringify(..)
不是直接强制的形式。然而,我们在这里介绍了它的两个原因,它将其行为与ToString
强制联系起来:
1 string
, number
, boolean
,和null
值基本上与它们通过ToString
抽象操作的规则强制转换为string
值的方式相同。
2 如果将object
值传递给JSON.stringify(..)
,并且该object
上有一个toJSON()
方法,则会在字符串化之前自动调用toJSON()
(有点)“强制”该值为JSON安全的。
ToNumber
如果任何非number
值的使用方式要求它是一个number
,例如数学运算,ES5规范在第9.3节中定义了ToNumber
抽象操作。
例如,true
变为1
,false
变为0
.undefined
变为NaN
,但(奇怪地)null
变为0
。
string
值的ToNumber
基本上适用于数字字面量的 规则/语法 (参见第3章)。如果失败,则结果为NaN
(而不是与number
文字一样的语法错误)。一个不同的例子,在此操作中,0
前缀八进制数不作为八进制数处理(正常基数为10的小数),尽管这些八进制数作为number
文字有效(参见第2章)。
注意: number
字面量语法和string
值上的ToNumber
之间的差异是微妙且高度细微的,因此这里不再进一步讨论。有关更多信息,请参阅ES5规范的9.3.1节。
对象(和数组)将首先转换为它们等价的原始值,并且根据刚刚提到的ToNumber
规则将结果值(如果是基本类型但不是number
)强制转换为数字。
要转换为等价的原始值,ToPrimitive
抽象操作(ES5规范,第9.1节)将查询这个值(使用内部DefaultValue
操作 - ES5规范,第8.12.8节),以查看它是否具有valueOf()
方法。如果valueOf()
可用并返回原始值,返回的值用于强制。如果没有valueOf()
,但toString()
可用,将由它提供强制的值。
如果两个操作都不能提供原始值,则抛出TypeError
。
从ES5开始,你可以创建这样一个不可强制的对象 - 即没有valueOf()
和toString()
-- 如果它的[[Prototype]]
是null
值,通常使用Object.create(null)
创建。有关[[Prototype]]
的更多信息,请参阅本系列的this&Object Prototypes标题。
注意: 我们将在本章后面详细介绍如何强制转换至number
,但是对于下一个代码片段,只需假设Number(..)
函数这样做了。
考虑下面代码:
ToBoolean
接下来,让我们聊聊一下boolean
在JS中的表现。关于这个话题 有很多混乱和误解 ,所以请密切关注!
首先,JS的实际关键字为true
和false
,它们的行为与你对boolean
值的预期完全一致。这是一个常见的误解,即值1
和0
与true
/false
相同。虽然在其他语言中可能是一回事,但在JS中,number
是number
而boolean
是boolean
。你可以强制1
为true
(反之亦然)或0
至false
(反之亦然)。但他们不是一回事。
Falsy Values
但这不是故事的结局。我们需要讨论, 当你强制使用它们的boolean
等价时,除了两个boolean
值之外的值是如何表现的。
所有JavaScript的值都可以分为两类:
如果强制转换为
boolean
,它们将变为false
其他一切(显然会成为
true
)
我不只是在滑稽。JS规范定义了一个特定的,缩小的值列表,当强制转换为boolean
值时,这些值将强制为false
。
我们如何知道值列表是什么?在ES5规范中,9.2节定义了一个ToBoolean
抽象操作,当你试图将它们强制为“布尔”时,它确切地说明所有可能的值会发生什么。
从该表中,我们得到以下所谓的"falsy"值列表:
undefined
null
false
+0
,-0
, andNaN
""
是的。如果某个值在该列表中,则它是一个"falsy"值,如果强制对其进行boolean
转换,它将强制为false
。
通过逻辑结论,如果某个值不在该列表中,则它必须位于另一个列表中,我们将其称为"truthy"值列表。但JS本身并没有真正定义一个"truthy"列表。它提供了一些示例,例如明确说明所有对象都是真实的,但大多数规范只是暗示: 因此,在falsy列表中没有明确指出的任何内容都是真实的。
Falsy Objects
等一下,该部分标题甚至听起来很矛盾。我 刚刚才说过 规范称所有对象都是truthy,对吧?不应该有"falsy object"这样的东西。
这甚至可能意味着什么呢?
你可能会认为它意味着包装了"falsy"值(例如""
,0
或false
)的对象包装器(请参阅第3章)。但不要陷入那个 陷阱 。
注意: 这些可能会是一个微妙的规范笑话。
考虑下面的代码:
我们知道这里的所有三个值都是对象(见第3章),这些对象包含明显的"falsy"值。但这些对象的行为是true
还是false
?这很容易回答:
所以,这三个都是true
,因为这是唯一可以最终使得d
成为true
的方式。
提示: 注意Boolean( .. )
包围a && b && c
表达式 -- 你可能想知道为什么会这样。我们将在本章后面回到这一点,所以要记住它。如果你只是做d = a && b && c
而没有携带Boolean(..)
调用,那就试试d
会是什么!
所以,如果"falsy objects" 不是包装falsy值的对象 ,他们到底是什么?
棘手的部分是它们可以出现在你的JS程序中,但它们实际上并不是JavaScript本身的一部分。
什么!?
在某些情况下,浏览器在常规JS语义之上创建了他们自己的 外来 值行为,即"falsy objects"的概念。
"falsy object"是一个看起来像普通对象(属性等)的值,但当你将它强制转换为boolean
值时,它会强制转换为false
值。
为什么!?
最着名的案例是document.all
:由DOM(而不是JS引擎本身)提供给你的JS程序的类似数组(对象),它将页面中的元素暴露给你的JS程序。它 过去 表现得像一个普通的物体 - 它会起到truthy的作用。但不再是了。
document.all
本身从来就不是真正的“标准”,并且早已被弃用/废弃。
“那他们不能把它删掉吗?”对不起,你可以试试。希望他们能。但是依赖于使用它的遗留JS代码库太多了。
那么,为什么要让它表现出falsy
来呢?因为document.all
强制转换为boolean
值(如在if
语句中)几乎总是用作检测旧的, 非标准的IE。
IE 从很早以前就开始顺应规范了,而且在许多情况下它在推动 web 向前发展的作用和其他浏览器一样多,甚至更多。但是所有那些老旧的 if (document.all) { /* it's IE */ }
代码依然留在世面上,而且大多数可能永远都不会消失。所有这些遗留代码仍然假设它在十年前的IE中运行,这只会导致IE用户的糟糕浏览体验。
因此,我们无法完全移除document.all
,但IE不希望if (document.all) { .. }
代码再次工作,以便现代IE中的用户获得符合标准的新代码逻辑。
“我们应该做什么?” **“我知道了!让我们把JS类型系统搞砸,并假装document.all
是falsy的!”
啊。太糟糕了。大多数JS开发人员都不理解这是一个疯狂的问题。但是其它的替代方案(对上面两败俱伤的问题什么都不做)要烂得 多那么一点点。
所以...这就是我们所拥有的:浏览器在JavaScript中添加了疯狂的,非标准的"falsy objects"。好极了!
Truthy Values
回到truthy列表。哪些才是truthy值?请记住: 如果某个值不在falsy列表中,则该值是truthy的。
考虑下面代码:
在这里的d
,你期待的是什么值?它必须是true
或false
。
它是true
。为什么?因为尽管这些string
值的内容看起来像falsy,但string
值本身都是truthy,因为""
是falsy列表中唯一的string
值。
那么下面这些呢?
是的,你猜对了,d
在这里仍然是true
。为什么?和前一个一样的原因。尽管它可能看起来像falsy,但是,[]
,{}
和function(){}
不在falsy列表中,因此是truthy值。
换句话说,truthy列表是无限长的。列出这样的清单是不可能的。你只能列出一个有限的falsy清单并参考它。
花五分钟时间,在你的计算机显示器的便利贴上写下falsy的清单,或者如果你愿意,可以记住它。无论哪种方式,只要询问它是否在falsy列表中,你就可以轻松地在需要时构建虚拟truthy列表。
ruthy和falsy的重要性在于理解如果将值(显式或隐式)强制转换为boolean
值,值的行为方式。现在你已经记住了这两个列表,我们可以深入了解强制示例。
Explicit Coercion
显式 强制是指明显且明确的类型转换。对于大多数开发人员来说,存在大量的类型转换用法,显然属于明确的强制类别。
这里的目标是在我们的代码中识别模式,我们可以清楚地表明我们正在将一个值从一种类型转换为另一种类型,以便不会在以后让开发人员陷入困境。我们越明确,以后人们就越容易阅读我们的代码,并且无需话费过多的精力即可理解我们的意图。
对于 明确的 强制转换,可能很难找到任何明显的分歧,因为它与被广泛接受的静态类型语言中的类型转换的工作方式非常接近。因此,我们认为(目前) 明确的 强制转换可以被认同为不是邪恶的,或没有争议的。不过,我们稍后再讨论这个问题。
Explicitly: Strings <--> Numbers
我们将从最简单且最常见的强制操作开始:在string
和number
表示之间强制转换值。
为了强制string
和number
,我们使用内置的String()
和Number()
函数(我们在第3章中称为“原生构造函数”),但 非常重要 的是,我们不在他们面前使用new
关键字。因此,我们不会创建对象包装器。
相反,我们实际上是在两种类型之间 明确的强制 :
String(..)
使用前面讨论的ToString
操作的规则从任何其他值强制转换为原始string
值。Number()
从任何其他值强制转换为原始number
值, 他的规则就是使用前面讨论的ToNumber
操作的规则。
我称之为 明确强制 ,因为一般来说,对于大多数开发人员而言,这些操作的最终结果是适当的类型转换,这是非常明显的。
事实上,这种用法实际上看起来很像其他一些静态类型的语言。
例如,在C/C++中,你可以说(int)x
或int(x)
,并且两者都将x
中的值转换为整数。两种形式都是有效的,但许多人更喜欢后者,这看起来像一个函数调用。在JavaScript中,当你说Number(x)
时,它看起来非常相似。它实际上是JS中的函数调用是否重要?并不是的。
除了String(..)
和Number()
之外,还有其他方法可以在string
和number
之间"显式"转换这些值:
调用a.toString()
表面上是显式的(非常清"toString"的意思是"到一个字符串"),但这里有一些隐藏的隐含性。不会在42
之类的原始值上直接调用toString()
。所以JS自动为42
“装箱”(参见第3章)在对象包装器中,所以就可以针对对象调用toString()
。换句话说,你可以将其称为“显式的隐式”。
+c
这里显示+
运算符的 一元运算符 形式(只有一个操作数的运算符)。而不是执行数学加法(或字符串连接 - 见下文),一元+
明确地将其操作数(c
)强制转换为number
值。
+c
是明确的强制吗?取决于你的经验和观点。如果你知道(现在你做了什么!),一元+
明确地用于number
强制,那么它是非常明确和明显的。但是,如果你以前从未见过它,它看起来可能非常混乱,隐含和隐藏的副作用等。
注意: 开源JS社区普遍接受的观点是,一元+
是 明确强制 的公认形式。
即使你真的喜欢+c
形式,也绝对会让它看起来非常令人困惑。考虑:
一元操作符-
也像+
那样强制执行,但他会翻转数字的符号。但是,你不能把两个- -
符号彼此相邻以翻转回来,因为它被解析为减运算符。相反的,如果你需要那么做:- -"3.14"
的中间加个空格,最终被转换成3.14
。
你可能会在运算符的一元形式旁边想出各种可怕的运算符组合 (如 +
用于加法)。下面是一个疯狂的示例:
当它紧邻其他运算符时,你应该强烈考虑避免一元+
(或-
)强制。虽然上述工作,但几乎普遍被认为是一个坏主意。即使d = + c
(或d = + c
,对于那个问题!)也很容易和d + = c
混淆,这完全不同!
注意: 与另一个运算符相邻使用一元+
的另一个非常令人困惑的地方是++
递增运算符和--
递减运算符。例如:a +++b
, a + ++b
, 和 a + + +b
。有关++
的更多信息,请参阅第5章中的“表达式副作用”。
请记住,我们试图明确并 减少 混乱,而不是让事情变得更糟!
Date To number
一元+
运算符的另一个常见用法是将Date
对象强制转换为number
,因为结果是unix时间戳(自1970年1月1日00:00:00 UTC以来经过的毫秒数)表示日期/时间值:
这个习惯用法的最常见用法是将当前时刻作为时间戳,例如:
注意: 一些开发人员知道JavaScript中一种特殊的语法“技巧”,即如果没有要传递的参数,构造函数调用(使用new
调用的函数)上设置的()
是可选的。所以你可能会遇到var timestamp = + new Date;
形式。但是,并非所有开发人员都同意省略()
会提高可读性,因为它是一种不常见的语法特例,仅适用于new fn()
调用形式而不适用于常规的fn()
调用形式。
但强制不是从Date
对象中获取时间戳的唯一方法。非强制方法可能更可取,因为它更加明确:
但更优选的非强制选项是使用ES5添加的Date.now()
静态函数:
如果你想将Date.now()
填充到旧浏览器中,它很简单:
我建议跳过与日期相关的强制形式。使用Date.now()
获取当前时间戳,使用new Date(..).getTime()
获取你需要指定的特定的非现在日期/时间的时间戳。
The Curious Case of the ~
一个经常被忽视并且通常让人困惑的JS强制运算符是波浪~
运算符(又名“按位NOT”)。许多理解其作用的人往往也希望避免使用它。但是在本书和系列文章中坚持我们的精神,让我们深入研究~
是否有任何有用的东西给我们。
在第2章的“32位(有符号)整数”部分中,我们讨论了JS中的位运算符如何仅为32位运算定义,这意味着我们强制它们的操作数遵循32位值的表现形式。关于如何发生这种情况的规则由ToInt32
抽象操作控制(ES5规范,第9.5节)。
ToInt32
首先执行ToNumber
强制,这意味着如果值为"123"
,它将在应用ToInt32
规则之前首先变为123
。
虽然技术上没有强制(因为类型没有改变!),使用具有某些特殊number
值的按位运算符(如|
或〜
)会产生强制效果,从而产生不同的number
值。
例如,让我们首先考虑|
“按位OR”运算符在惯用语0 | x
中使用(如第2章所示),基本上只进行ToInt32
转换:
这些特殊数字不是32位可表示的(因为它们来自64位IEEE 754标准 - 见第2章),因此ToInt32
只是指定0
作为这些值的结果。
有争议的是,0 | __
是否是ToInt32
强制操作的显式形式,或者它是隐式的形式。从规范的角度来看,它毫无疑问是 明确的 ,但是如果你不理解这个级别的按位运算,它可能看起来更 隐蔽 神奇。然而,与本章中的其他教程说法一致,我们将其称之为 明确的 。
那么,让我们把注意力转回〜
。〜
运算符首先“强制”为32位number
值,然后执行按位求反(翻转每个位的奇偶校验)。
注意: 这与!
非常相似,不仅会将其值强制转换为boolean
值,还会翻转其奇偶校验(请参阅后面对"一元!"的讨论)
但是......什么!?为什么我们关心被翻转的位?这是一些非常专业,细致入微的东西。JS开发人员很少需要推理个别位。
另一种思考〜
定义的方法来自于旧式计算机科学/离散数学:〜
执行二进制补码。太好了,谢谢,这更加清晰!
让我们再试一次:~x
与-(x + 1)
大致相同。这很奇怪,但比较容易理解。所以:
你可能仍然想知道这些~
到底是什么,或者为什么它对于强制性讨论真的很重要。让我们快点说清楚, get到这个点。
考虑下-(x+1)
。你可以执行该操作产生0
(或技术上为-0
!)作为结果的唯一值是什么? -1
。换句话说,~
与一系列number
值一起使用会为一个输入值为-1
的值生成一个falsy的(容易强制为false
)0
值,否则将生成任何其他truthy的number
值。
为什么这有关系?
-1
通常称为“哨兵值”, 这基本上是指在相同类型(number
)的更大值集合中赋予任意语义含义的值。对于许多函数,C语言使用-1
这个"哨兵"值,它们返回>= 0
的值表示“成功”,返回-1
表示“失败”。
JavaScript在定义string
操作indexOf(..)
时采用了此先例,它搜索子字符串并且如果找到则返回其从零开始的索引位置,如果未找到则返回-1
。
尝试使用indexOf(..)
不仅仅是作为获取位置的操作,而是作为对另一个string
中子串的存在/不存在的boolean
检查,这是很常见的。
我发现查看> = 0
或== -1
有点严重。它基本上是一个“抽象漏洞”,因为它泄露了底层实现行为 - 将“哨兵-1
”表示“失败” - 带入到我的代码中。我宁愿隐藏这样的细节。
现在,终于,我们明白为什么〜
可以帮助我们!对indexOf()
使用~
,“强制”(实际上只是转换)该值是 适当的boolean
:
~
获取到indexOf(..)
的返回值并转换他:对于“失败”的是-1
,我们将得到falsy的0
,而其他每个值都是truthy的。
注意: 对于 〜
的-(x + 1)
伪算法意味着〜-1
是-0
,但实际上它是0
,因为底层操作实际上是按位的,而不是数学的。
从技术上讲,if(~a.indexOf(..))
仍然依赖于其结果0
的隐式强制为false
或非零为true
。
但总的来说,〜
仍然觉得我更像是一种 明确 的强制机制,只要你知道它在这个成语中的目的是什么。
我发现这比前面杂乱的> = 0
/ == -1
更清晰。
Truncating Bits
你遇到的代码中,可能还有一个地方会出现~
:一些开发人员使用双波浪符号~~
来截断数字的小数部分(即,将其“强制”为“整数”)。通常(虽然错误地)说这与调用Math.floor(..)
的结果相同。
~~
是怎么工作的?他是第一个〜
应用ToInt32
“强制”并按位翻转,然后第二个〜
做另一个按位翻转,将所有位翻转回原始状态。最终结果只是ToInt32
“强制”(又称截断)。
注意: ~~
的按位双翻转非常类似于奇偶校验双重否定!!
行为,稍后在“显式:* - >布尔”部分中解释。
但是,~~
需要一些 保守/澄清。首先,它只能在32位值上稳定地工作。但更重要的是,它在负数上的作用与Math.floor(..)
不同!
除了设置Math.floor(..)
之外,~~x
可以截断为(32位)整数。但是x | 0
也可以,似乎(稍微)省力 。
所以,为什么你选择~~x
而不是x | 0
,那么然后呢?运算符优先级(参见第5章):
就像这里的所有其他建议一样,只有当读/写这些代码的每个人都正确地知道这些运算符如何工作时,才使用〜
和~~
作为“强制”和值转换的显式机制!
Explicitly: Parsing Numeric Strings
将string
强制转换为number
的类似结果可以通过解析string
的字符内容中的number
来实现。但是,这种解析和我们上面检查的类型转换之间存在明显的差异。
考虑下面代码:
从字符串中解析数值可以兼容非数字字符 -- 它只是在遇到非数字字符时从左到右停止解析 -- 而强制转换是不兼容的,会失败,导致NaN
值。
解析不应被视为强制的替代品。这两项任务虽然相似,却有不同的用途。当你不知道/关心右侧可能存在的其他非数字字符时,将string
解析为number
。当数字作为唯一可接受的值时,强制string
(到一个number
),类似"42px"
之类的东西应该被拒绝作为number
。
提示: parseInt()
有个兄弟parseFloat(..)
,(听起来)是从字符串中取出一个浮点数。
不要忘记parseInt()
在string
值上操作。将number
值传递给parseInt(..)
绝对没有意义。传递任何其他类型的值也没有意义,例如true
,function() {..}
或[1,2,3]
。
如果你传递的不是一个string
,你传递的这个值将首先会被自动强制成string
(看前面的"ToString
"),这显然是一种隐藏的 隐含强制 。在程序中依赖这样的行为是一个非常糟糕的主意,所以永远不要将parseInt()
与非string
值一起使用。
在ES5之前,parseInt()
存在另一个问题,这是许多JS程序bug的根源。如果你没有传递第二个参数来指示用于解释数字string
内容的进制(是基数),那么parseInt()
将查看起始字符以进行猜测。
如果前两个字符是"0x"
或"0X"
,则猜测(按照惯例)你是希望将string
解释为十六进制(base-16)number
。否则,如果第一个字符为"0"
,则猜测(再次按照惯例)你是希望将string
解释为八进制(base-8)number
。
十六进制string
(前缀是0x
或0X
)不那么容易混淆。但八进制数猜测被证明是非常普遍的。例如:
似乎没问题,对吧?尝试选择小时是08
,分钟是09
。你将会得到0:0
。为什么?因为8
或9
都不是八进制base-8中的有效字符。
ES5之前的修复很简单,但很容易忘记:总是传递10作为第二个参数。 这完全安全:
从ES5开始,parseInt()
不再猜测八进制。除非另有说明,否则它假定为base-10(或"0x"前缀的base-16)。那更好。如果你的代码必须在ES5之前的环境中运行,请注意,在这种情况下,你仍然需要传递10
。
Parsing Non-Strings
几年前有一个讽刺js的笑话很闪眼,那就是关于parseInt()
的,很是臭名昭著。看看下面的这个例子:
假设(但完全无效)的断言是,“如果我传入Infinity,并解析出一个整数,我应该得到Infinity,而不是18。”当然,JS肯定对这个结果感到疯狂,对吧?
虽然这个例子显然是做作和不真实的,但让我们放纵一下这种疯狂,并检查JS是否真的那么疯狂。
首先,这里犯下的最明显的罪行是将非string
作为参数传递给parseInt(..)
。这是禁忌。这样做,你是在自找麻烦。但即使你这样做,JS也会礼貌地强制将你传入的string
强制转换为可以尝试解析的字符串。
有些人认为这是不合理的行为,parseInt()
应该拒绝对非string
值进行操作。或许他应该抛出一个错误?坦白的说,那就很像java了。我一想到JS应该开始在整个地方抛出错误就不寒而栗,这样几乎每一行都需要try..catch。
应该返回NaN
吗?也许。但是这个呢:
也会失败吗?他是一个非string
值。如果你希望将String
对象包装器解包为"42"
,那么42
首先成为"42"
真的很不寻常,这样可以解析出42
吗?
我认为,这种 半明确的 ,半隐式的强制 可能会发生,这通常是一件非常有用的事情。例如:
事实上,parseInt()
将值强制转换为string
以执行解析是非常明智的。如果你传入垃圾,并且垃圾回收,不要责怪垃圾桶 - 它只是忠实地完成了它的工作。
因此,如果你传递了一个像Infinity
这样的值(1 / 0
的结果明显就是), 什么样的string
表示法会对其强制最有意义?只有两个合理的选择:"Infinity"
和 "∞"
。 JS选择了"Infinity"
。我很高兴它做到了。
我认为JS中的 所有值 都有某种默认string
表示是一件好事,这样它们就不会是我们无法调试和推理的神秘黑盒子。
现在,base-19怎么样?显然,完全是伪造和做作的。没有真正的JS程序使用base-19。这太荒谬了。但是,让我们来放纵这种荒谬。在base-19中,有效的数字字符是0
-9
和a
-i
(不区分大小写)。
所以,回到我们的parseInt( 1/0, 19 )
例子。它基本上就是parseInt("Infinity", 19)
。它是如何解析的?第一个字符是"I"
, 在愚蠢的base-19中,他就是18
。第二个字符"n"
不在有效的数字字符集中,因此解析只是礼貌地停止,就像它在"42px"
中遇到"P"
时一样。
结果?18
。这应该是合理的。将我们带到这里的一系列的行为 而不是错误或Infinity
本身,对JS来说 非常重要 ,不应该轻易丢弃。
使用parseInt()
的这种行为的其他示例可能令人惊讶但是非常明智包括:
parseInt()
实际上是可预测的并且在其行为上是一致的。如果你正确使用它,你会得到正确的结果。如果你错误地使用它,你得到的疯狂的结果,那不是JavaScript的错。
Explicitly: * --> Boolean
现在,让我们检查从任何非boolean
到boolean
的强制转换。
就像上面提到的String(..)
和Number(..)
,Boolean(..)
(当然,排除使用new
)是强制ToBoolean
的明确方法:
虽然布Boolean(..)
是明确的,但它并不常见或惯用。
就像一元+
运算符强制一个值到number
值(见上文),一元!
否定运算符显式地将值强制转换为boolean
。问题 是,它也将值从真实转为虚假,反之亦然。因此,JS开发人员明确强制使用boolean
的最常见方式是使用!!
双重否定,因为第二!
将奇偶校验翻回到原来:
如果在布尔上下文(例如if(..)..
语句中使用),那么任何这些ToBoolean
强制都将在没有Boolean(..)
或!!
的情况下 隐式 发生。但是这里的目标是明确地将值强制为boolean
,以便更清楚地表明ToBoolean
强制是有意的。
显式ToBoolean
强制的另一个示例用例是,如果要在数据结构的JSON序列化中强制使用true/false
值强制:
如果你从Java转到JavaScript,你可能会认识到这个惯用语:
? :
三元运算符将测试a
的真实性,并且基于该测试将相应地为b
分配值true
或false
。
从表面上看,这个惯用语看起来像是一种 显式 ToBoolean
类型的强制形式,因为从这个操作来看,结果很明显只有true
或false
。
然而,存在隐藏的 隐式强制 ,因为必须首先将表达式a
强制转换为boolean
值以执行真实性测试。把这个惯用语称为“明确隐含的”。此外,我建议你应该在JavaScript中 完全避免使用这个惯用法。 它没有提供真正的好处,更糟糕的是,会伪装成其他的东西。
Boolean(a)
和!!a
是更好的 明显 的强制选项。
Implicit Coercion
隐式强制 是指隐藏的类型转换,具有从其他操作隐式发生的非明显的副作用。换句话说,隐式 强制是任何不明显的类型转换(对你而言)。
虽然很清楚明确强制的目标是什么(使代码明确且更容易理解),但隐式强制具有相反的目标可能是显而易见的:使代码更难理解。
从表面上看,我认为对强制的大部分愤怒来自于此。关于“JavaScript强制”的大多数投诉实际上都是针对隐含的强制行为(无论他们是否意识到)。
注意: “JavaScript:The Good Parts”一书的作者Douglas Crockford在许多会议讨论和写作中声称应该避免使用JavaScript强制。但他似乎意味着隐含的强制是不好的(在他看来)。但是,如果你阅读他自己的代码,你会发现很多强制的例子,包括 隐含 的和 明确 的!事实上,他的焦虑似乎主要是针对==
操作,但正如你在本章中所看到的那样,这只是强制机制的一部分。
那么,是隐含的强制 邪恶吗?危险吗?它是JavaScript设计中的一个缺陷吗?我们应该不惜一切代价避免它吗?
我打赌大多数读者都倾向于热情地欢呼,“是的!”
不要那么急。 听我说。
让我们从另一个角度来看待 隐含 的强制是什么,可以是什么,而不仅仅是 它是“好的显示强制的反面”,这太狭隘了,忽略了一个重要的细微差别。
我们将 隐式 强制的目标定义为:减少冗长的内容,样板和/或不必要的实现细节,这些细节会分散我们的代码,从而分散了更重要的意图。
Simplifying Implicitly
在我们讨论javascript之前,让我先从一些理论上的强类型语言中提出一些伪代码来说明:
在这个例子中,我在y
中有一些任意类型的值,我想转换为SomeType
类型。问题是,这种语言不能直接从当前的任何东西(y
)转到SomeType
。
它需要一个中间步骤,首先转换为AnotherType
,然后从AnotherType
转换为SomeType
。
现在,如果那种语言(或者你可以使用该语言自己创建的定义)让你说:
你难道不同意我们在这里简化了类型转换以减少中间转换步骤不必要的“噪音”吗?我的意思是,在代码的这一点上,是否 真的 非常重要的要去看到(处理)y
在转到SomeType
之前首先进入AnotherType
的事实?
有些人会争辩,至少在某些情况下,是的。我认为可以在许多其他情况下做出相同的论证,在这里,简化实际上通过抽象或隐藏这些细节 来提高代码的可读性, 无论是在语言本身还是在我们自己的抽象中。
毫无疑问,在幕后的某个地方,中间转换的步骤仍会发生。但是如果在这里隐藏了这个细节,我们可以让y
转到SomeType
作为通用操作并隐藏混乱的细节。
虽然不是一个完美的类比,但我将在本章的其余部分讨论的是,JS 隐式 强制可以被认为是为你的代码提供类似的帮助。
但是,这非常重要, 这不是一个无边的,绝对的说法。当然,隐式 强制背后潜藏着大量的邪恶的东西,与任何潜在的可读性改进相比,隐式强制对代码的危害要大得多。显然,我们必须学习如何避免这样的结构,以便我们不会以各种方式破坏我们的代码。
许多开发人员认为,如果一个机制可以做一些有用的事情 A ,但也可以被滥用或误用来做一些可怕的事情 Z ,那么我们应该完全抛弃这个机制,只是为了安全。
Implicitly: Strings <--> Numbers
在本章的前面,我们探讨了在string
和number
值之间 明确 强制转换。现在,让我们探讨相同的任务,但采用隐式 强制方法。但是在我们开始之前,我们必须先研究一些操作上的细微差别,这些细微差别将 隐含 的强制去强制执行。
+
运算符被重载以用于number
添加和string
连接。那么JS如何知道你想要使用哪种类型的操作?考虑下面代码:
导致"420"
和"42"
的不同之处是什么?一个常见的误解是,区别在于一个或两个操作数是否为string
,因为这意味着+
将假定string
连接。虽然这部分是对的,但比这更复杂。
考虑下面代码:
这些操作数都不是string
,但显然它们都被强制转换为string
然后string
连接。那么真正发生了什么?
(警告: 深入细节的规范即将到来,所以如果恐吓你,请跳过接下来的两段!)
根据ES5规范第11.6.1节,+
算法(当object
值是其中一个操作数时),如果任一操作数已经是string
,或者以下步骤产生string
表达形式,将会连接起来。因此,当+
接收任一操作数是object
(包括array
)时,它首先在值上调用ToPrimitive
抽象操作(第9.1节),然后使用上下文的标识(hint, 这里的hint是number)调用[[DefaultValue]]
算法(第8.12.8节)。笔者:对这里感觉有些绕的,可以看看一篇关于toPrimitive
的文章
如果你密切关注,你会注意到此操作现在与ToNumber
抽象操作处理object
的方式相同(请参阅前面的"ToNumber"
部分)。对array
的valueOf()
操作将无法生成简单的基元(基本元素),因此它将使用toString()
表示。因此,两个array
分别变为"1,2"
和"3,4"
。现在,正如你期待的那样, +
连接了两个string
: "1,23,4"
。
让我们抛开那些混乱的细节,回到之前的简化说明:如果+
的任一操作数是一个stri ng
(或者变成一个带有上述步骤!),则操作将是string
连接。否则,它总是数字加法。
注意: 一个常被引用的强制问题是[] + {}
与{} + []
,因为这两个表达式分别在"[object Object]"
和0
中产生结果。不过,还有更多内容,我们将在第5章的"Blocks"中介绍这些细节。
这对 隐含 强制意味着什么?
你可以通过添加number
和""
空string
将number
强制转换为string
:
提示: 使用+
运算符的数字加法是可交换的,这意味着2 + 3
与3 + 2
相同。带+
的字符串连接显然不是可交换的,但是 对于""
的特定情况,它实际上是可交换的,因为a + ""
和"" + a
将产生相同的结果。
使用+ ""
操作(隐式地)将number
强制转换为string
是非常常见/惯用的。事实上,有趣的是,即使是一些对隐式强制的最直言不讳的批评者仍然在他们自己的代码中使用这种方法,而不是其明确的替代方案之一。
我认为 隐式 强制的有用形式中,这是一个很好的例子, 尽管这种机制经常被批评!
a + ""
的 隐含 强制与我们之前的String(a)
的 显式 强制的例子相比,还有一个额外的怪癖需要注意。由于ToPrimitive
抽象操作的工作原理,+ ""
对a
值调用valueOf()
,然后通过内部ToString
抽象操作将其返回值最终转换为string
。但是String(a)
是直接调用toString()
。
这两种方法最终都会产生一个string
,但如果你使用的是object
而不是常规的原始number
值,则可能不一定会获得相同的string
值!
考虑下面代码:
一般来说,除非你真的试图创建令人困惑的数据结构和操作,否则这种问题不会让你感到困惑,但是如果你为某个object
定义了自己的valueOf()
和toString()
方法,你应该小心,因为你强制该值会影响结果。
另一个方向呢?我们如何隐含地从string
强制转换为number
?
-
运算符仅定义为数字减法,因此a - 0
强制将a
的值强制转换为number
。虽然很不常见,但a * 1
或a / 1
会实现相同的结果,因为这些运算符也仅为数值运算定义。
object
值使用 -
运算符会怎么样?与上面+
类似:
两个array
值都必须成为number
,但它们最终首先被强制转换为string
(使用预期的toString()
序列化),然后被强制转换为number
,以便-
减法执行。
那么,string
和number
值的 隐含 强制是你一直听到的关于恐怖故事中丑陋邪恶吗?我个人并不这么认为。
对比b = String(a)
(明确的)到b = a + ""
(隐含的)。我认为可以使两种方法在你的代码中都有用。当然,b = a + ""
在JS程序中更为常见,无论对一般情况下 隐含 强制的优点或危害的感受如何,都证明了它自己的实用性。
Implicitly: Booleans --> Numbers
我认为 隐式 强制可以真正发挥作用的一种情况是将某些类型的复杂boolean
逻辑简化为简单的数字加法。当然,这不是一种通用技术,而是针对特定情况的特定解决方案。
考虑下面代码:
如果其中一个参数为true
/ truthy,则onlyOne()
应仅返回true
。它对truthy检查使用隐式强制,对其他检查使用显式强制,包括最终返回值。
但是,如果我们需要该实用程序能够以相同的方式处理四个,五个或二十个标志呢?很难想象实现能够处理所有这些比较排列的代码。
但是这里将boolean
值强制转换为number
(显然为0
或1
)可以极大地帮助:
注意: 当然,在onlyOne()
中, 你可以更简洁地使用es5 reduce()
实用程序,而不是只使用for
循环,但我不想因此模糊概念。
我们在这里所做的是依靠1
来实现true
/truthy 的强制,并在数字上将它们全部加起来。sum += arguments[i]
使用 隐式 强制来实现这一点。如果arguments
列表中的一个且只有一个值为true
,则数字和将为1
,否则总和将不为1
,因此不满足所需条件。
我们当然可以通过明确的强制来做到这一点:
我们首先使用!!arguments[i]
来强制值变成true
或false
。这样你就可以传递非boolean
值,比如onlyOne( "42", 0 )
,并且它仍然可以按预期工作(除此之外你最终可能会遇到string
连接,逻辑会不正确)。
一旦我们确定它是一个boolean
值,我们用Number()
做另一个 明确 的强制,以确保该值为0
或1
。
这种工具的 明确 强制形式是“更好”的吗?它确实避免了代码注释中解释的NaN
陷阱。但是,最终,这取决于你的需求。我个人认为以前的版本,依赖于 隐式 强制更优雅(如果你不会传递undefined
或NaN
),而且显式版本不必要地,显得更加冗长。
但就像我们在这里讨论的几乎所有内容一样,这是一个主观的判断。
注意: 无论采用隐式还是显式方法,只需将最终比较分别从1
更改为2
或5
,就可以轻松地仅生成onlyTwo(..)
或onlyFive(..)
变体。这比添加一堆&&
和||
要容易得多表达式。因此,一般来说,强制在这种情况下非常有用。
Implicitly: * --> Boolean
现在,让我们把注意力转向隐式强制到boolean
值,因为它是迄今为止最常见的,也是迄今为止最具潜在性的麻烦。
请记住,当你以强制转换值的方式使用值时,隐式 强制就会发生。对于数字和string
操作,很容易看出强制如何发生。
但是,什么样的表达式操作 需要/强制(隐式)boolean
强制?
if(..)
语句中的表达式for ( .. ; .. ; .. )
中while (..)
和do..while (..)
循环? :
三元表达式左侧操作数到
||
(逻辑或)和&&
(逻辑和)运算符
在这些上下文中使用的任何不是boolean
值的值将使用本章前面介绍的ToBoolean
抽象操作的规则 隐式 强制转换为boolean
值。
让我们来看一些例子:
在所有这些上下文中,非boolean
值被 隐式 强制转换为它们的boolean
等价物以进行测试决策。
Operators ||
and &&
||
and &&
很可能你已经看过了你使用的大多数或所有其他语言中的||
(逻辑或)和&&
(逻辑和)运算符。因此,很自然地假定它们在JavaScript中的工作原理与其他类似语言中的工作原理基本相同。
这里有一些鲜为人知但非常重要的细微差别。
事实上,我认为这些运算符甚至不应被称为“逻辑_运算符”,因为该名称在描述它们的作用时并不完整。如果我给他们一个更准确(如果更笨拙)的名字,我会称他们为“选择器操作符”,或者更完整地称为“操作数选择器操作符”。
为什么?因为它们实际上并没有在JavaScript中产生逻辑值(也就是boolean
),就像在其他语言中那样。
那么它们会导致什么?它们导致两个操作数中的一个(且仅一个)值。换句话说,他们 选择两个操作数值中的一个。
引用第11.11节中的ES5规范:
由&&或运算符生成的值不一定是布尔类型。生成的值始终是两个操作数表达式之一的值。
让我们来说明下:
等等,什么!? 考虑一下。在像C和PHP这样的语言中,这些表达式会导致true
或false
,但在JS(以及Python和Ruby,就此而言!)中,结果来自值本身。
两个||
和&&
运算符对 第一个操作数(a
或c
)执行boolean
测试。如果操作数不是boolean
值(这里不是),则会发生正常的ToBoolean
强制,以便可以执行测试。
对于||
运算符,如果测试为true
,则||
表达式将第一个操作数(a
或c
)的值作为结果。如果测试结果为false
,则||
表达式将第二个操作数(b
)的值作为结果。
相反,对于&&
运算符,如果测试为true
,则&&
表达式产生第二个操作数(b
)的值作为结果。如果测试为false
,则&&
表达式将生成第一个操作数(a
或c
)的值作为结果。
一个||
的结果或&&
表达式始终是其中一个操作数的基础值,而 不是 测试的(可能是强制的)结果。在c && b
中,c
为null
,因此是falsy的。但是&&
表达式本身导致null
(c
中的值),而不是测试中使用的强制false
。
你现在看到这些操作符如何充当“操作数选择器”吗?
另一种思考这些运算符的方法:
注意: 我调用a || b
"大致相当于"a ? a : b
因为结果相同,但存在细微差别。在a ? a : b
中,如果a
是一个更复杂的表达式(例如可能具有调用function
等副作用的表达式),那么a
表达式可能会被测试两次(如果第一次测试是truthy的)。相比之下,对于a || b
,a
表达式仅计算一次,该值既用于强制测试,也用于结果值(如果适用)。同样的细微差别适用于a && b
和a ?b : a
表达式。
此行为的一个非常常见和有用的用法, 你很有可能以前使用过, 但尚未完全理解, 它是:
a = a || "hello"
的惯用法(有时被说成C#“null合并操作符”的JavaScript版本)用于测试a
,如果它没有值(或只是一个不需要的falsy值),则提供默认值("hello"
)。
不过 要小心!
看出问题了吗?""
作为第二个参数,是一个falsy值(看早期章节的ToBoolean
部分),所以b = b || "world"
测试失败,而"world"
进行取代,即使意图可能是明确传递""
是分配给b
的值。
这个||
惯用法非常普遍,非常有用,但只有在应该跳过 所有falsy值 时才必须使用它。否则,你需要在测试中更明确,并且可能使用? :
三元符去代替他。
这个 默认 值赋值习惯是如此常见(并且很有用!)即使那些公开和激烈地谴责JavaScript强制的人也经常在他们自己的代码中使用它!
那么关于&&
呢?
还有另一种习惯用法,但是不是很常见,通常是手工创作的,但是JS 压缩器经常使用它。当且仅当第一个操作数测试为truthy时,&&
运算符“选择”第二个操作数,这个用法有时被称为"守护操作员"(也见第5章中的"Short Circuited") - 第一个表达式测试"守护"第二个表达式:
仅当a
是truthy的时候才调用foo()
。如果该测试失败,那么这个a && foo()
表达式语句就会默默地停止 - 这被称为“短路” - 并且永远不会调用foo()
。
同样,人们创作这样的东西并不常见。通常,他们会使用if (a) { foo(); }
去代替他。但JS 压缩器选择使用a && foo()
,因为它更短。所以,现在,如果你必须破译这样的代码,你就会知道它在做什么以及为什么。
好的,所以||
和&&
有一些巧妙的伎俩,只要你愿意允许 隐含 的强制进入组合。
注意: a = b || "something"
和a && b
惯用法依赖短路行为,我们将在第5章中详细介绍。
事实上,这些操作符实际上并没有产生true
和false
,这可能会让你的头脑有点混乱。你可能想知道所有if
语句和for
循环是如何工作的,如果它们包含复合逻辑表达式,如 a && (b || c)
。
不要担心!天不会塌下来。你的代码(可能)没问题。只是你可能以前从未意识到在评估复合表达式 之后 有一个隐含 的强制boolean
值。
考虑下面代码:
除了一个微妙的额外细节之外,这段代码仍然以你一直以为的方式运行。a && (b || c)
实际的结果是"foo"
,不是true
。因此,if
语句然后强制"foo"
值强制转换为boolean
值,这当然是true
。
看到?没理由害怕。你的代码可能仍然安全。但是现在你更了解它是如何做到的。
现在你也意识到这样的代码正在使用 隐式 强制。如果你仍处于“避免(隐含)强制阵营”,你将需要返回并使所有这些测试更 明确 :
祝你好运!对不起,只是开个玩笑。
Symbol Coercion
到目前为止,显式 和 隐式 强制之间几乎没有可观察到的结果差异 - 只有代码的可读性受到影响。
但ES6符号引入了我们需要简要讨论的强制系统。由于远远超出我们将在本书中讨论的范围的原因,允许将symbol
显式 强制转换为string
,但不允许对其进行 隐式 强制, 会抛出错误。
考虑下面代码:
symbol
值根本不能强制转换为number
(以任何方式抛出错误),但奇怪的是它们可以 显式 地和 隐式 地强制转换为boolean
(总是为true
)。
一致性总是更容易学习,并且异常从来都不是有趣的事情,但我们只需要小心新的ES6symbol
值以及我们如何强制它们。
好消息:你需要强制一个symbol
值,这可能是非常罕见的。他们通常使用的方式(见第3章)可能不会要求在正常情况下进行强制。
Loose Equals vs. Strict Equals
松散(宽松)的等于是==
运算符,严格的等于是===
运算符。两个运算符用于比较“相等”的两个值,但“松散”与“严格”表示两者之间的行为有着 非常重要 的差异,特别是它们如何决定“相等”。
关于这两个运算符的一个非常常见的误解是:“==
检查值是否相等,===
检查值和类型是否相等。”虽然这听起来不错而且合理,但这是不准确的。无数备受推崇的JavaScript书籍和博客都说得很清楚,但遗憾的是它们都 错了 。
正确的描述是:“==
允许在比较中强制,===
不允许强制。”
Equality Performance
停下来思考第一个(不准确)解释与第二个(准确)解释之间的区别。
在第一个解释中,似乎很明显===
比==
做更多的工作,因为它还必须检查类型。在第二个解释中,==
是一个做更多工作的人,因为如果类型不同,它必须遵循强制的步骤。
不要像许多人那样陷入这样的陷阱: 认为这与性能有关, 就好像==
将以任何相关的方式慢于 ===
。虽然可以测量强制确实需要一点处理时间,但它只是微秒(是的,这是百万分之一秒!)。
如果你要比较相同类型的两个值,==
和===
使用相同的算法,除了引擎实现的细微差别之外,它们应该执行相同的工作。
如果你要比较不同类型的两个值,则性能不是重要因素。你应该问自己的是:在比较这两个值时,我是否想要强制?
如果你想强制,使用==
松散的平等,但如果你不想强制,使用===
严格的平等。
注意: 这里的含义是==
和===
检查其操作数的类型。不同之处在于,如果类型不匹配,它们会如何响应。
Abstract Equality
==
运算符的行为在ES5规范的第11.9.3节中定义为“抽象等式比较算法”。列出的内容是一个全面但简单的算法,明确说明了每种可能的类型组合,以及每种组合应如何进行强制(如果必要)。
警告: 当(隐含的)强制被指责为过于复杂和缺陷而不是有用的好的部分时,正是这些“抽象平等”规则受到了谴责。一般来说,对于开发人员来说,它们过于复杂和不具有实际意义,以至于无法实际学习和使用,而且它们更容易导致JS程序中的错误,而不是使代码具有更高的可读性。我相信这是一个有缺陷的前提——你的读者是有能力的开发者,他们整天会写(读)和理解!算法(又名代码)。所以,下面简单地阐述了“抽象平等”。但我恳请你也阅读ES5规范第11.9.3节。我想你会对它的合理性感到惊讶。
基本上,第一个条款(11.9.3.1)说,如果被比较的两个值属于同一类型,它们可以通过标识按照你的预期进行简单而自然的比较。例如,42
仅等于42
,"abc"
仅等于"abc"
。
正常期望的一些小例外情况:
NaN
永远不会等于他自己(看第二节)+0
和-0
彼此相等(看第二节)
第11.9.3.1节中的最后一条规则是用于object
的==
(包括function
和array
)松散相等比较。如果它们都是对完全相同的值的引用,则两个这样的值仅相等。这里没有强制。
注意: ===
严格相等比较的定义与11.9.3.1相同,包括有关两个object
值的规定。这是一个鲜为人知的事实,在比较两个object
的情况下,==
和===
的行为相同!
11.9.3中算法的其余部分指定如果使用==
松散相等来比较不同类型的两个值,则需要 隐式 强制其中的一个或两个值。种强制发生使得两个值最终都以相同的类型结束,然后可以使用简单的值标识直接比较它们的相等性。
注意: !=
松散的不相等操作的定义与你期望的完全相同,因为它实际上是完整地执行的==
操作比较,然后是结果的否定。对于!==
严格的不相等操作也是如此。
Comparing: strings to numbers
为了说明==
强制,让我们首先构建本章前面的string
和number
示例:
正如我们所期望的那样,a === b
失败,因为不允许强制,实际上42
和"42"
值是不同的。
但是,第二个比较a == b
使用松散相等,这意味着如果类型碰巧不同,则比较算法将对一个或两个值执行 隐式强制。
但是到底发生了什么样的强制?是a
的值42
变成了string
,或者是b
的值"42"
变成了number
?
在ES5规范中,条款11.9.3.4-5说:
如果Type(x) 是 Number 并且 Type(y) 是 String, 返回的比较是 x == ToNumber(y).
如果 Type(x) 是 String 并且 Type(y) 是 Number, 返回对比的结果是 ToNumber(x) == y.
规范使用Number
和String
作为类型的正式名称,而本书更喜欢原始类型的number
和string
。不要让规范中的数字大小写混淆了Number()
原生函数。就我们的目的而言,类型名称的大写是无关紧要的 - 它们具有基本相同的含义。
很明显,该规范称"42"
值被强制为一个number
用于比较。早期已经涵盖了这种强制的方式,特别是ToNumber
抽象操作。在这种情况下,很明显,得到的两个42
值是相等的。
Comparing: anything to boolean
当你试图直接将值与true
或false
进行比较时,会出现一个 隐含 强制==
松散平等的最大的问题。
考虑下面代码:
等等,这里发生了什么?我们知道"42"
是一个truthy的值(参见本章前面的内容)。那么,为什么它不是==
松散等于true
?
原因既简单又具有欺骗性。这很容易被误解,许多JS开发人员从来没有花费足够的注意力来完全掌握它。
让我们再次引用规范,条款11.9.3.6-7:
如果 Type(x) 是 Boolean, 返回的比较是 ToNumber(x) == y.
如果 Type(y) 是 Boolean, 返回的比较是 x == ToNumber(y).
让我们打破它,去理解他。首先:
Type(x)
确实是一个Boolean
,所以他执行ToNumber(x)
,这样会强制true
到1
。现在,1 == "42"
将被评估进行对比。但是类型仍然不同,所以(基本上是递归地)我们重建算法,如上所述将"42"
强制为42
,而1 == 42
显然是false
。
翻转他,我们仍然得到相同的结果:
这次Type(y)
是Boolean
,所以ToNumber(y)
得到0
。"42" == 0
变成了42 == 0
,这当然是false
。
换句话说,42
既不是== true
也不是== false
。 起初,这种说法可能看起来很疯狂。一个值既不是truthy也不是falsy的?
但那就是问题所在!你完全是在问一个错误的问题。这不是你的错,真的。你的大脑在欺骗你。
"42"
确实是truthy,但是"42" == true
根本没有执行boolean 测试/强制, 无论你的大脑说什么。"42"
没有被强制成boolean
(true
),但是对于true
,他被转换成了1
,然后"42"
被转换成了42
。
无论我们是否喜欢,ToBoolean
甚至都不参与其中,因此"42"
的真实性或虚假性与==
操作无关!
相关的是理解==
比较算法如何表现所有不同的类型组合。因为它关于==
两侧的boolean
值,所以boolean
值总是首先强制转换为number
。
如果你觉得很奇怪,别怕,不是你一个人这么认为。我个人建议在任何情况下都不要使用==true
或==false
。永远。
但是请记住,我在这里讨论的仅仅是==
相关的。=== true
和=== false
不允许强制,所以他们不会有隐藏的ToNumber
强制,因此是安全的。
考虑下面代码:
如果你避免在代码中使用== true
或者== false
(也叫 与boolean
的松散相等),那么你就不必担心这种真实性/虚假性。
Comparing: nulls to undefineds
另一个 隐式 强制的例子可以在 null
和undefined
值之间的==
松散相等来看到。再次引用ES5规范第11.9.3.2-3条:
如果 x 是 null 并且 y 是 undefined, return true.
如果 x 是 undefined 并且 y 是 null, return true.
null
和undefined
,当使用==
松散相等的时候,它们是互相等价(也就是互相强制转换)的,而且在整个语言中不会等价于其他值了。
这意味着,如果使用==
宽松相等运算符允许它们之间的 隐式 强制,则可以将null
和undefined
值视为不可区分的。
null
和undefined
值之间的强制是安全的和可预测的,并且在这种检查中,没有其他值可以给出误判。我建议使用这种强制方式来允许null
和undefined
值不可区分,从而将其视为相同的值。
看下面大例子:
仅当doSomething()
返回null
或者undefined
,a == null
将通过检查,其他的值将不会通过检查,即使返回的是一些其他的falsy值,比如""
, 0
和false
。
这个检查的明确形式,不允许任何这样的强制,是(我认为)不必要地更加丑陋(可能性能稍差一点!):
在我看来,a == null
的形式是 隐式 强制可以提高代码可读性的另一个例子,但是以可靠安全的方式这样做。
Comparing: objects to non-objects
如果object
/function
/array
与一个简单基本标量(string
, nbumber
, boolean
)进行比较,ES5规范在第11.9.3.8-9节中说明:
如果 Type(x) 是 String 或者 Number 并且 Type(y) 是 Object, 返回的结果就是 x == ToPrimitive(y).
如果 Type(x) 是 Object 并且 Type(y) 是 String 或者 Number, 返回的结果就是 ToPrimitive(x) == y.
注意: 你可能会注意到这些子句仅提及String
和Number
,但不提及Boolean
。这是因为,正如前面引用的那样,第11.9.3.6-7条规定了首先将任何出现的Boolean
操作数强制转换成Number
。
考虑下面代码:
[42]
值调用其ToPrimitive
抽象操作(参见前面的“抽象值操作”部分),他的结果就是值"42"
。从那里开始,它只是42 == "42"
,正如我们已经讲解的那样变为42 == 42
,因此a
和b
被发现是强制相等的。
提示: 我们在本章前面讨论过的ToPrimitive
抽象操作的所有特性(toString()
,valueOf()
)都如你所期望的那样在这里应用。如果你有一个复杂的数据结构,想要在其上定义一个自定义的valueof()
方法,以便为相等比较提供一个简单的值,那么这非常有用。
在第3章中,我们介绍了"开箱",其中展开了一个原始值的object
包装器(例如来自new String("abc")
),并返回基础原始值("abc"
)。此行为与==
算法中的ToPrimitive
强制有关:
a == b
是true
,因为b
通过ToPrimitive
强制(也叫"开箱"展开),到其底层的简单标量原始值"abc"
,该值与a
中的值相同。
但是,由于==
算法中的其他重写规则,有些值不是这种情况。考虑下面代码:
null
和undefined
不可以被装箱 -- 他们没有对象包装器 -- 因此Object(null)
就像Object()
一样,只是产生一个普通对象。
NaN
可以等效的装入其 Number
对象包装器,但当==
导致取消装箱(开箱)时,NaN == NaN
比较失败,因为NaN
永远不等于自身(参见第2章)。
Edge Cases
现在我们已经彻底检查了= =
松散平等的 隐含 强制如何起作用(以合理和令人惊讶的方式),让我们试着说出最糟糕,最疯狂的角落案例,这样我们就可以看到我们需要避免的事情,不要被强制性的错误所困扰。
首先,让我们来看看修改内置原生原型如何产生疯狂的结果:
A Number By Any Other Value Would...
警告: 2 == 3
不会陷入此陷阱,因为2
和3
都不会调用内置的Number.prototype.valueOf()
方法,因为两者都是原始number
值,可以直接比较。但是,new Number(2)
必须经过ToPrimitive
强制,因此调用valueOf()
。
邪恶,对吧?当然是的。任何人都不应该做这样的事情。你可以这样做,但是有时这被用作对强制和==
批评的事实。但那是错误的挫败感。JavaScript不会因为你可以做这样的事情而 不好,如果开发人员做了这些事情就很糟糕。不要陷入“我的编程语言应该保护我自己”的谬论。
接下来,让我们考虑另一个棘手的例子,它将前一个例子中的邪恶带到另一个层次:
你可能认为这是不可能的,因为a
永远不会同时等于2
和3
。但是“同时”这个说法是不准确的,因为第一个表达式a == 2
严格地发生在a == 3
之前。
那么,如果我们使a.valueOf()
每次调用时都有副作用,那么第一次返回2
时,第二次调用它会返回3
?满容易:
再次的,这个是一个邪恶的伎俩。不要这样做。但也不要将它们用作反对强制的依据。无意识的滥用某种机制并不足以谴责该机制。只是避免这些疯狂的伎俩,只坚持有效和正确使用强制。
False-y Comparisons
在==
比较中,最常见的反对 隐式 强制的抱怨来自falsy值相互比较时的表现。
为了说明,让我们看一下围绕falsy值比较的角落列表,看看哪些是合理的,哪些是麻烦的:
在列表的24个对比中,其中17个是非常合理和可预测的。例如,我们知道""
和NaN
根本不是相等的值,事实上,他们并没有被强制成宽松相等,而"0"
和0
是合理等价的,而且确实强制转换为宽松相等。
然而,七个比较标有“UH OH!”。因为误报,他们更有可能陷入困境。""
和0
绝对是明显不同的值,并且你很少想把它们视为等同的,所以它们的相互强制是很麻烦的。请注意,这里没有任何误报。
The Crazy Ones
不过,我们不必止步于此。我们可以继续寻找更麻烦的强制转换:
噢,这看起来更疯狂,对吧!?你的大脑可能会欺骗你,你正在比较一个truthy的值和falsy值,所以true
的结果是令人惊讶的,因为我们知道一个值永远不会同时是truthy和falsy!
但这不是实际发生的事情。让我们去分解他。我们对!
一元运算符了解吧?他使用ToBoolean
规则显示的强制值成为boolean
类型(他也会翻转等价性)。所以在[] == ![]
被处理之前,他实际已经被翻译成了[] == false
。我们已经在上面的列表中看到了这个(false == []
),所以它的惊喜结果对我们来说并不陌生。
其他角落案例怎么样?
正如我们之前在ToNumber
讨论中所说的那样,右侧[2]
和[null]
值将经历ToPrimitive
强制,因此它们可以更容易地与左侧的简单基元(分别为2
和""
)进行比较 。由于array
值的valueOf()
只返回array
本身,因此强制降为字符串化array
。
[2]
将变成"2"
,然后在第一次比较中被ToNumber
强制为2
以获得右侧值。 [null]
直接变为""
。
所以2 == 2
和"" == ""
是完全可以理解的。
如果你的直觉仍然不喜欢这些结果,那么你的挫败感实际上并不像你可能认为的那样是强制性的原因。它实际上是对默认array
值'强制转换为string
值的ToPrimitive
行为的抱怨。更可能的是,你只是希望[2] .toString()
没有返回"2"
,或者[null] .toString()
没有返回""
。
但是这些string
强制究竟会导致什么结果呢?除了"[2]"
之外,我无法真正想到[2]
的强制结果不是"2"
的任何其他适当的string
强制 -- 但在其他情况下这可能会非常奇怪!
你可以正确地说,因为String(null)
变为"null"
,所以String([null])
也应该变为"null"
。这是一个合理的断言。所以,这才是真正的罪魁祸首。
隐含 强制本身在这里并不邪恶。即使对[null]
明确强制string
也会导致""
。有争议的是,对于array
值来说,将字符串化为等效的内容是否合理,以及究竟是如何发生的。因此,请你对String([..])
的规则感到沮丧,因为这就是疯狂源于此的地方。也许根本不应该对array
进行字符串强制?但是在语言的其他部分会有很多其他缺点。
另一个著名的引用问题:
正如我们之前讨论的空""
,"\n"
(或" "
或任何其他空白组合)通过ToNumber
强制,结果为0
。希望空白强制到其他的什么number
值?明确的Number("")
产生0
是否困惑你?
真的是空字符串或空白字符串可以强制使用的唯一其他合理number
值是NaN
。但这真的会更好吗?比较"" == NaN
当然会失败,但目前还不清楚我们是否真的已经解决了任何潜在的问题。
现实世界的JS程序因为0 == "\n"
导致失败的可能性非常罕见,并且这种极端情况很容易避免。在任何预言中,类型转换 始终 有极端的情况 - 没有特定于强制。这里的问题是关于第二次猜测一组特定的极端情况(也许是正确的!?),但这并不是反对整体强制机制的显着论据。
底线:几乎任何你可能遇到的正常值之间的疯狂强制(除了早先有意为之的valueOf()
或者toString()
)将归结为我们上面已经确定的七个强项强制列表。
为了对抗这24个可能的强制陷阱嫌疑人,请考虑另一个这样的列表:
在这些非虚假的,非角色的情况下(我们可以在这个列表上进行无数次比较),强制结果是完全安全,合理和可解释的。
Sanity Check
好吧,当我们深入研究 隐式 强制时,我们肯定发现了一些疯狂的东西。难怪大多数开发者声称强制是邪恶的,应该避免,对吧!?
但让我们退后一步,做一个健全检查。
通过数量比较,我们列出了七个麻烦的强制,但我们有另一个(至少17个,但实际上是无限的)强制列表,完全是理智和可解释的。
如果你正在寻找一本教科书中的例子“把孩子和洗澡水一起扔出去”,就是这样:因为一个字面上只有7个坑的列表,放弃所有的强制(无限大的安全和有用的行为列表)。
更谨慎的反应就是问:“我怎样才能使用无数强制的强制部分,但要避免一些不好的部分?”
让我们再看一下坏名单:
这个列表中的七个项目中有四个涉及==false
比较,我们之前说过你应该 总是 避免。这是一个很容易记住的规则。
这些合理的强制措施是你在普通的JavaScript程序中做的吗?他们会在什么条件下真的发生?
我认为你在程序中的boolean
测试中使用== []
的可能性不大,至少在你知道自己在做什么的情况下是这样。你可能改为做==""
或== 0
,如:
如果你不小心调用doSomething(0)
或doSomething([])
,你就会很惊讶。另一种情况:
同样,如果你做了类似doSomething("", 0)
或doSomething()[], "")
的事情,这可能会失败。
所以,虽然情况可能会存在,这些强制会坑你,并且你要小心它们,但它们可能在你的整个代码库中并不常见。
Safely Using Implicit Coercion
我能给你的最重要的建议是:检查你的程序,并推断出在==
比较的任何一方可以显示哪些值。为了有效避免这种比较的问题,这里有一些启发式规则要遵循:
如果比较的任何一方可以具有
true
值或false
值,请不要使用==
。如果比较的任何一方有
[]
,""
或0
值,请认真考虑不使用==
。
在这些情况下,几乎可以肯定,最好使用===
而不是==
,以避免不必要的强制。遵循这两个简单的规则,几乎所有可以合理地伤害你的强制陷阱都会被有效地避免。
在这些情况下使用更明确/更详细的强制将使你免于许多麻烦。
==
vs===
的问题确实被恰当地理解为:你是否应该允许强制进行比较?
在很多情况下,这种强制可能会有所帮助,允许你更简洁地表达一些比较逻辑(例如,使用null
和undefined
)。
在整体方案中,隐性 强制真正危险的案例相对较少。但在那些地方,为了安全起见,一定要使用===
。
提示: 保证强制不要坑你的另一个地方是typeof
操作符。typeof
总是会返回七个字符串中的一个(参见第1章),并且它们都不是空的""
字符串。因此,在任何情况下,检查某些值的类型都不会违反 隐式 强制。typeof x == "function"
与typeof x === "function"
一样100%安全可靠。从字面上看,规范说这种算法在这种情况下是相同的。所以,不要盲目地到处使用===
,如果你这样做都是因为这是你的代码工具告诉你要这样做,或者(最糟糕的是)因为你在某本书中被告知 不要考虑它 。你拥有代码的质量。
隐式 强制是邪恶的并且危险的?在少数情况下,是的,但绝大多数,没有。
成为负责任且成熟的开发人员。学习如何有效和安全地使用强制力(显性 和 隐性 )。并教导你周围的人也这样做。
这是Alex Dorey(GodHub上的@dorey)制作的一个方便的表格,用于可视化各种比较:
原文: https://github.com/dorey/JavaScript-Equality-Table
Abstract Relational Comparison
虽然 隐式 强制的这一部分往往得不到很多关注,但重要的是要考虑a < b
比较会发生什么(类似于我们刚刚检查a == b
的深度)。
ES5第11.8.5节中的“抽象关系比较”算法基本上分为两部分:如果比较涉及string
值(后半部分)或其他任何内容(前半部分),该怎么办。
注意: 该算法仅针对a < b
定义。因此,a > b
被处理为b < a
。
该算法首先对两个值调用ToPrimitive
强制,如果任一调用的返回结果不是string
(笔者:两个都不是string
或者其中有一个不是string
),则使用ToNumber
操作规则将这两个值强制转换为数值,并进行数字比较。
比如下面的例子:
注意: 类似于-0
和NaN
的警告在这里适用,就像它们在前面讨论的==
算法中所做的那样。
但是,如果两个值都是<
比较的string
,则执行字符的简单字典(自然字母)比较:
a
和b
没有强制成number
,因为在两个数组上的ToPrimitive
强制之后它们都以字符串结尾。因此,"42"
逐字符地比较"043"
,分别从第一个字符"4"
和"0"
开始。由于"0"
在词典上小于"4"
,因此比较返回false
。
完全相同的行为和推理:
在这里,a
变成了"4,2"
, b
变成了"0,4,3"
,这些按字典顺序与前一个片段完全相同。
关于下面这个会如何:
a < b
也是false
,因为a
变成了[object Object]
,并且b
变成[object Object]
,所以很明显,a
不是按字典顺序小于b
。
但奇怪的是:
为什么a == b
不是true
?他们是相同的string
值("[object Object]"
),所以它们似乎应该是平等的,对吧?不。回想一下之前关于==
如何使用object
引用的讨论。
但是如果a<b
和a == b
和a> b
都是false
的,那么a<= b
和a> = b
如何得到true
呢?
因为规范说明a<= b
,它实际上首先会评估b <a
,然后否定该结果。由于b <a
也是false
的,因此a<= b
的结果为true
。
这可能与你目前解释<=
所做的完全相反,这可能是字面上的意思:“小于或等于”。JS更准确地认为a <= b
作为“不大于”(!(a>b)
,JS视为!(b <a)
)。此外,a >= b
通过首先将其视为b <= a
,然后应用相同的推理。
不幸的是,没有“严格的关系比较”,因为存在平等。换句话说,除了在进行比较之前明确确保a
和b
具有相同类型之外,没有办法防止像a<b
这样的关系比较发生 隐式 强制。
使用我们之前的==
与===
完整性检查讨论相同的推理。
果强制是有帮助且相当安全的,例如在42 < "43"
比较中,使用它。 另一方面,如果您需要对关系比较保持安全,请在使用<
(或其对应项)之前先 明确 强制执行值。
Review
在本章中,我们将注意力转向JavaScript类型转换的发生方式,称为强制,可以表征为 显式 或 隐式 。
强制说得很糟糕,但在许多情况下它实际上非常有用。负责任的JS开发人员的一项重要任务是花时间学习强制的所有细节,以确定哪些部分有助于改进他们的代码,以及他们应该避免哪些部分。
显式 强制是一种代码,显然其目的是将值从一种类型转换为另一种类型。其好处是通过减少混淆来提高代码的可读性和可维护性。
隐性 强制是一种“隐藏”的强制,它被视为某种其他操作的副作用,而类型转换的发生并不明显。虽然隐式强制可能看起来与显式相反,因此很糟糕(实际上很多人都这么认为!),实际上隐式强制也是为了提高代码的可读性。
特别是对于隐含的,必须以负责任和有意识的方式使用强制。知道你为什么要编写你正在编写的代码,以及它是如何工作的。努力编写其他人很容易从中学习和理解的代码。
Last updated