JavaScript中怪异的情景

86 阅读8分钟

奇怪的JavaScript

今天我们要做一个特别的帖子,专门介绍那些奇怪的JavaScript时刻,在这些时刻,事情的表现有点奇怪。

"在这个世界上,没有一个正常人能完成有意义的事情"。- 乔纳森,《奇怪的事情》(Stranger Things)。

我们将看看一些结果令人惊讶的代码片段,我们将对发生的事情做一个解释,这样我们就可以更好地理解我们心爱的编程语言。虽然它是个怪胎,但我们还是喜欢它!


情景一:['1', '7', '11'].map(parseInt)

让我们来看看第一个场景的代码

['1', '7', '11'].map(parseInt);

对于你所期望的输出结果。

[1, 7, 11]

然而,事情到了这里就有点不对劲了,实际的结果是。

[1,NaN,3]

起初,这可能看起来非常奇怪,但实际上它有一个优雅的解释。为了理解发生了什么,我们需要理解两个相关的函数,mapparseInt

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

那其他的例子呢?AToPrimitiveToString 方法在加法之前被隐含地调用了[]{} 。阅读更多关于规范中的评估过程。

值得注意的是,{} + [] 这里是一个例外。它与[] + {} 不同的原因是,在没有括号的情况下,它被解释为一个代码块,然后是一个单数+,将[] 转换为一个数字。它看到的情况如下。

{
  // a code block here
}
+[]; // -> 0

为了得到与[] + {} 相同的输出,我们可以用小括号将其包裹。

({} + []); // -> [object Object]

结论

我希望你喜欢这篇文章,就像我喜欢写这篇文章一样。JavaScript是一种神奇的语言,充满了技巧和怪异,我希望这篇文章能让你对其中一些有趣的话题有所了解,并且在下次遇到这样的事情时,你知道到底发生了什么。