奇怪的JavaScript
今天我们要做一个特别的帖子,专门介绍那些奇怪的JavaScript时刻,在这些时刻,事情的表现有点奇怪。
"在这个世界上,没有一个正常人能完成有意义的事情"。- 乔纳森,《奇怪的事情》(Stranger Things)。
我们将看看一些结果令人惊讶的代码片段,我们将对发生的事情做一个解释,这样我们就可以更好地理解我们心爱的编程语言。虽然它是个怪胎,但我们还是喜欢它!
情景一:['1', '7', '11'].map(parseInt)
让我们来看看第一个场景的代码
['1', '7', '11'].map(parseInt);
对于你所期望的输出结果。
[1, 7, 11]
然而,事情到了这里就有点不对劲了,实际的结果是。
[1,NaN,3]
起初,这可能看起来非常奇怪,但实际上它有一个优雅的解释。为了理解发生了什么,我们需要理解两个相关的函数,map 和parseInt 。
map()
map() 对数组中的每个元素按顺序调用一个提供的 函数,并从结果中构建一个新的数组。 只对数组中已赋值(包括callback callback 未定义)的索引进行调用。
现在,上面提到的callback 函数将接收一些特殊的参数,让我们以它的输出为例。
[1, 2, 3].map(console.log)
1 0 > (3) [1, 2, 3]
2 1 > (3) [1, 2, 3]
3 2 > (3) [1, 2, 3]
可以看出,map函数不仅传递了项的值,而且还传递了索引和每次迭代时完整数组的副本。这很重要,也是影响我们之前结果的部分原因。
parseInt()
parseInt() 函数解析一个字符串参数并返回一个指定弧度的整数(数学数字系统中的基数)。
所以现在,根据定义,parseInt(string [, radix]) 希望有两个参数,即我们要解析的字符串和弧度。
解开谜团
现在我们对这两个函数有了足够的了解,让我们试着理解在我们的案例中发生了什么,我们将从我们的原始脚本开始,一步一步地解释它。
['1', '7', '11'].map(parseInt);
正如我们所知,map 函数的callback ,将接收3个参数,所以我们就这样做。
['1', '7', '11'].map((currentValue, index, array) => parseInt(currentValue, index, array));
开始了解一下发生了什么?当我们添加参数时,很明显,parseInt 函数正在接收额外的参数,而不仅仅是数组中项目的实际值,所以现在我们可以采取测试函数对这些值的每个组合会做什么,但我们也可以忽略数组参数,因为它将被parseInt 函数丢弃。
parseInt('1', 0)
1
parseInt('7', 1)
NaN
parseInt('11', 2)
3
因此,现在可以解释我们最初看到的数值,parseInt 函数的结果被redix 参数改变了,该参数决定了转换的基数。
有什么办法可以得到最初预期的结果吗?
现在知道了它的工作原理,我们就可以很容易地修正我们的脚本,得到预期的结果。
['1', '7', '11'].map((currentValue) => parseInt(currentValue));
> (3) [1, 7, 11]
情景二:('b'+'a'+'a'+'a').toLowerCase() ==='banana'
你可能会想,上面的表达式是假的,毕竟我们在表达式的左边建立的字符串中没有字母'n',或者不是吗?让我们来弄清楚。
('b'+'a'+ + 'a' + 'a').toLowerCase() === 'banana'
true
好吧,你可能已经意识到发生了什么,但如果没有,让我在这里快速解释一下。让我们把注意力放在表达的左边,右边没有什么奇怪的,相信我。
('b'+'a'+ + 'a' + 'a').toLowerCase()
"banana"
有趣的是,我们正在形成 "香蕉 "这个词,所以问题似乎在这里,让我们去掉小写的转换,看看会发生什么。
('b'+'a'+ + 'a' + 'a')
"baNaNa"
Bingo!我们现在发现了一些'N',看起来我们实际上在字符串中发现了一个NaN ,也许它来自于+ + ,让我们假装一下,看看我们会得到什么。
b + a + NaN + a + a
不太好,我们有一个额外的a ,所以让我们试试其他的东西。
+ + 'a'
NaN
啊,我们走吧......+ + 操作本身并没有被评估,但是当我们在末尾添加字符'a'时,它全部进入了NaN ,现在适合我们的代码。然后,NaN 表达式作为一个字符串与其余的文本连接起来,我们最终得到banana 。相当奇怪!
情景三:甚至无法命名
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]] === 'fail'
这到底是怎么回事?一堆括号怎么会形成失败这个词?相信我,JS并没有失败,我们实际上得到的是字符串fail 作为输出。
让我们试着解释一下,在那一堆东西中,有一些东西形成了一个模式。
(![] + [])
该模式评估为字符串false ,这很奇怪,但它是语言的一个属性,原来是false + [] === 'false' ,这种转变与JS内部如何映射内部调用有关,我们不会详细讨论这到底为什么会发生。
一旦你形成了字符串false ,剩下的就很容易了,只要寻找你需要的字母的位置,除了一种情况,即字母i ,它不是单词false 的一部分。
为此,原始表达式发生了一些变化,让我们看看([![]] + [][[]]) ,它评估为字符串falseundefined 。因此,基本上我们强迫一个未定义的值,并把它与我们知道如何得到的false 字符串连接起来,剩下的就是历史了。
到目前为止,你喜欢它吗?让我们再做一些。
情景四:是真实还是虚伪,这是一个问题。
诚实还是真实,这是个问题。是假的还是假的,这就是问题所在。
什么是truthy和falsy?为什么它们与真或假不同?
JavaScript中的每一个值都有自己的布尔值(truthy/falsy),这些值被用在期望有布尔值但没有给出的操作中。很可能你至少曾经做过这样的事情。
const array = [];
if (array) {
console.log('Truthy!');
}
在上面的代码中,array ,尽管值是 "truthy",但它不是一个布尔值,而且表达式将导致执行下面的console.log 。
我怎么知道什么是truthy,什么是falsy?
所有不是虚假的东西都是真实的。糟糕的解释吗?足够公平,让我们进一步研究一下。
Falsy是具有继承布尔值的数值false ,数值如。
- 0
- -0
- 0n
- '' 或 ""
- 空
- 未定义
- NaN
其他一切都将是真实的。
情景五:数组平等
JS中的一些东西很奇怪,这是语言设计的方式,我们接受它的方式。让我们看看一些奇怪的数组相等。
[] == '' // -> true
[] == 0 // -> true
[''] == '' // -> true
[0] == 0 // -> true
[0] == '' // -> false
[''] == 0 // -> true
[null] == '' // true
[null] == 0 // true
[undefined] == '' // true
[undefined] == 0 // true
[[]] == 0 // true
[[]] == '' // true
[[[[[[]]]]]] == '' // true
[[[[[[]]]]]] == 0 // true
[[[[[[ null ]]]]]] == 0 // true
[[[[[[ null ]]]]]] == '' // true
[[[[[[ undefined ]]]]]] == 0 // true
[[[[[[ undefined ]]]]]] == '' // true
如果你对为什么感兴趣,你可以在规范的第7.2.13节抽象等价比较中阅读。尽管我必须警告你,这不是为正常人准备的 :p.
情景六:数学就是数学,除非....
在我们的现实世界中,我们知道数学就是数学,我们知道它是如何工作的,我们从小就被教导如何做数字加法,而且如果你把相同的数字相加,你总是会得到结果,对吗?那么......对于JavaScript来说,这并不总是正确的......或者说是有点......让我们来看看。
3 - 1 // -> 2
3 + 1 // -> 4
'3' - 1 // -> 2
'3' + 1 // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'
'222' - -'111' // -> 333
[4] * [4] // -> 16
[] * [] // -> 0
[4, 4] * [4, 4] // NaN
最初,一切开始都很好,直到我们到了。
'3' - 1 // -> 2
'3' + 1 // -> '31'
当我们做减法的时候,字符串和数字是作为数字互动的,但是在做加法的时候,两者都作为一个字符串,为什么呢?嗯......它是这样设计的,但是有一个简单的表格可以帮助你理解JavaScript在每种情况下会做什么。
Number + Number -> addition
Boolean + Number -> addition
Boolean + Boolean -> addition
Number + String -> concatenation
String + Boolean -> concatenation
String + String -> concatenation
那其他的例子呢?AToPrimitive 和ToString 方法在加法之前被隐含地调用了[] 和{} 。阅读更多关于规范中的评估过程。
值得注意的是,{} + [] 这里是一个例外。它与[] + {} 不同的原因是,在没有括号的情况下,它被解释为一个代码块,然后是一个单数+,将[] 转换为一个数字。它看到的情况如下。
{
// a code block here
}
+[]; // -> 0
为了得到与[] + {} 相同的输出,我们可以用小括号将其包裹。
({} + []); // -> [object Object]
结论
我希望你喜欢这篇文章,就像我喜欢写这篇文章一样。JavaScript是一种神奇的语言,充满了技巧和怪异,我希望这篇文章能让你对其中一些有趣的话题有所了解,并且在下次遇到这样的事情时,你知道到底发生了什么。