说明
What the f*ck JavaScript?
一个有趣和棘手的 JavaScript 示例列表。
JavaScript 是一个不错的语言。它的语法简单,生态系统也很庞大,最重要的是,它拥有最伟大的社区力量。
我们知道,JavaScript 是一个非常有趣的语言,但同时也充满了各种奇怪的行为。这些奇怪的行为有时会搞砸我们的日常工作,有时则会让我们忍俊不禁。
WTFJS 的灵感源于 Brian Leroux。这个列表受到他 在 2012 年的 dotJS 上的演讲 “WTFJS” 的高度启发。
适用于 NodeJS 的指南手册
你可以通过 npm
安装该项目的指南手册。只需运行:
然后在命令行中运行 wtfjs
,将会在命令行中打开手册并跳转至你选择的页数 $PAGER
。这不是必需的步骤,你也可以继续在这里阅读。
源码在此处: https://github.com/denysdovhan/wtfjs
翻译
如今,wtfjs 已被翻译成多种语言:
- 中文
- हिंदी
- Français
- Português do Brasil
- Polski
- Italiano
- Russian (on Habr.com)
- 한국어
注意: 翻译由该语言的译者维护,因此可能缺失部分例子,或存在过时的例子等。
Table of Contents
- [] 等于 ![]
- true 不等于 ![],也不等于 []
- true 是 false
- baNaNa
- NaN 不是 NaN
- 奇怪的 Object.is() 和 ===
- 它是 fail
- [] 是真值,但不等于 true
- null 是假值,但又不等于 false
- document.all
- 最小值大于零
- 函数不是函数
- 数组相加
- 数组的相等性是深水猛兽
- undefined 和 Number
- parseInt
- true 和 false
- HTML 注释在 JavaScript 中有效
- NaN
不是一个数值 - [] 和 null
- 神奇的数字增长
- 0.1 + 0.2
- 扩展数字的方法
- 三个数字的比较
- 有趣的数学
- 正则表达式的加法
- 字符串不是 String
- 用反引号调用函数
- 到底 call 了谁
- constructor
- 将对象做为另一个对象的 key
- 访问原型 __proto__
- `${{Object}}`
- 使用默认值解构
- 点和扩展运算符
- 标签
- 嵌套标签
- 阴险的 try..catch
- 这是多重继承吗?
- yield 返回自身的生成器
- 类的类
- 不可转换类型的对象
- 棘手的箭头函数
- 箭头函数不能作为构造函数
- arguments
- 棘手的返回
- 对象的链式赋值
- 使用数组访问对象属性
- Number.toFixed()
- min 大于 max
- 比较 null 和 0
- 相同变量重复声明
- Array.prototype.sort() 的默认行为
- resolve() 不会返回 Promise 实例
- {}{}
- arguments
- 来自地狱的 alert
- 没有尽头的计时
- setTimeout
- 点点运算符
- 再 new 一次
- 你应该用上分号
- 用空格分割(split)字符串
- 对字符串 stringify
- 对数字和 true
💪🏻 初衷
只是因为好玩
— “只是为了好玩:一个意外革命的故事”, Linus Torvalds
这个列表的主要目的是收集一些疯狂的例子,并尽可能解释它们的原理。我很喜欢学习以前不了解的东西。
如果您是初学者,您可以根据此笔记深入了解 JavaScript。我希望它会激励你在阅读规范上投入更多时间和精力。
如果您是专业开发人员,您将从这些例子中看到人见人爱的 JavaScript 也充满了非预期的边界行为。
总之,古人云:三人行,必有我师焉。我相信这些例子总能让你学习到新的知识。
✍🏻 符号
// ->
// >
表示 console.log
等输出的结果。例如:
//
👀 例子
[] 等于 ![]
数组等于一个数组取反:
💡 说明:
抽象相等运算符会将其两端的表达式转换为数字值进行比较,尽管这个例子中,左右两端均被转换为 0
,但原因各不相同。数组总是真值(truthy),因此右值的数组取反后总是为 false
,然后在抽象相等比较中被被类型转换为 0
。而左值则是另一种情形,空数组没有被转换为布尔值的话,尽管在逻辑上是真值(truthy),但在抽象相等比较中,会被类型转换为数字 0
。
该表达式的运算步骤如下:
了解更多:[] 是真值,但并非 true.
true 不等于 ![],也不等于 []
数组不等于 true
,但数组取反也不等于 true
;
数组等于 false
数组取反也等于 false
:
💡 说明:
true 是 false
💡 说明:
考虑以下步骤:
baNaNa
这是用 JavaScript 写的老派笑话,原版如下:
💡 说明:
这个表达式可以转化成 'foo' + (+'bar')
,但无法将'bar'
强制转化成数值。
NaN 不是 NaN
💡 说明:
规范严格定义了这种行为背后的逻辑:
- 如果
Type(x)
不同于Type(y)
,返回false。- 如果
Type(x)
数值, 然后
- 如果
x
是NaN,返回false。- 如果
y
是NaN,返回false。- ……
根据 IEEE 对 NaN 的定义:
有四种可能的相互排斥的关系:小于、等于、大于和无序。当比较操作中至少一个操作数是 NaN 时,便是无序的关系。换句话说,NaN 对任何事物包括其本身比较都应当是无序关系。
— StackOverflow 上的 “为什么对于 IEEE754 NaN 值的所有比较返回 false?”
奇怪的 Object.is() 和 ===
Object.is()
用于判断两个值是否相同。和 ===
操作符像作用类似,但它也有一些奇怪的行为:
💡 说明:
在 JavaScript “语言”中,NaN
和 NaN
的值是相同的,但却不是严格相等。NaN === NaN
返回 false 是因为历史包袱,记住这个特例就行了。
基于同样的原因,-0
和 0
是严格相等的,但它们的值却不同。
关于 NaN === NaN
的更多细节,请参阅上一个例子。
- 这是 TC39 中关于 Object.is 的规范
- MDN 上的[相等比较与相同值比较]](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness)
它是 fail
你可能不会相信,但……
💡 说明:
将大量的符号分解成片段,我们注意到,以下表达式经常出现:
所以我们尝试将 []
和 false
加起来。但是因为一些内部函数调用(binary + Operator
- >ToPrimitive
- >[[DefaultValue]
]),我们最终将右边的操作数转换为一个字符串:
将字符串作为数组,我们可以通过[0]
来访问它的第一个字符:
剩下的部分以此类推,不过此处的 i
字符是比较讨巧的。fail
中的 i
来自于生成的字符串 falseundefined
,通过指定序号 ['10']
取得的。
更多的例子:
- 烧脑预警:疯狂的 JavaScript
- 写个句子干嘛要用字母 — 用 JavaScript 生成任意短语
[] 是真值,但不等于 true
数组是一个真值,但却不等于 true
。
💡 说明:
以下是 ECMA-262 规范中相应部分的链接:
null 是假值,但又不等于 false
尽管 null
是假值,但它不等于 false
。
但是,别的被当作假值的却等于 false
,如 0
或 ''
。
💡 说明:
跟前面的例子相同。这是一个相应的链接:
document.all 是一个 object,但又同时是 undefined
⚠️ 这是浏览器 API 的一部分,对于 Node.js 环境无效 ⚠️
尽管 document.all 是一个类数组对象(array-like object),并且通过它可以访问页面中的 DOM 节点,但在通过 typeof
的检测结果是 undefined
。
同时,document.all
不等于 undefined
。
但是同时,document.all
不等于 undefined
:
不过:
💡 说明:
document.all
作为访问页面 DOM 节点的一种方式,在早期版本的 IE 浏览器中较为流行。尽管这一 API 从未成为标准,但被广泛使用在早期的 JS 代码中。当标准演变出新的 API(例如 document.getElementById
)时,这个 API 调用就被废弃了。因为这个 API 的使用范围较为广泛,标准委员会决定保留这个 API,但有意地引入一个违反 JavaScript 标准的规范。
这个有意的对违反标准的规范明确地允许该 API 与 undefined
使用严格相等比较得出 false
而使用抽象相等比较 得出 true
。— “废弃功能 - document.all” at WhatWG - HTML spec
— YDKJS(你不懂 JS) - 类型与语法 中的 “第 4 章 - ToBoolean - 假值
最小值大于零
Number.MIN_VALUE
是最小的数字,大于零:
💡 说明:
Number.MIN_VALUE
是 5e-324
,即可以在浮点精度内表示的最小正数,也是在该精度内无限接近零的数字。它定义了浮点数的最高精度。现在,整体最小的值是
Number.NEGATIVE_INFINITY
,尽管这在严格意义上并不是真正的数字。— StackOverflow 上的“为什么在 JavaScript 中 0 小于 Number.MIN_VALUE?”
函数不是函数
⚠️ V8 v5.5 或更低版本中出现的 Bug(Node.js <= 7) ⚠️
大家都知道 undefined 不是 function 对吧?但是你知道这个吗?
💡 说明:
这不是规范的一部分。这只是一个缺陷,且已经修复了。所以将来不会有这个问题。
Super constructor null of Foo is not a constructor (Foo 的超类的构造函数 null 不是构造函数)
这是前述缺陷的后续行为,在现代环境中可以复现(在 Chrome 71 和 Node.js v11.8.0 测试成功)。
💡 说明:
这并不是缺陷,因为:
若当前类没有构造函数,则在构造该类时会顺次调用其原型链上的构造函数,而本例中其父类没有构造函数。补充一下,null
也是一个 object
:
因此,你可以继承 null
(尽管在面向对象编程的世界里这是不允许的),但是却不能调用 null
的构造函数。若你把代码改成这样:
将会报错:
但是当你加上 super
时:
JS 抛出错误:
- @geekjob 发布的对该问题的解释
数组相加
如果你尝试将两个数组相加:
💡 说明:
数组之间会发生串联。步骤如下:
数组中的尾逗号
假设你想要创建了一个包含 4 个空元素的数组。如下所示,最终只能得到一个包含三个元素的数组,原因在于尾逗号:
💡 说明:
尾逗号 (trailing commas,有时也称为“最后逗号”(final commas)) 在向 JavaScript 代码中添加新元素、参数或属性时非常有用。如果您想添加一个新属性,若前一行已经有尾逗号,你无需修改前一行,只要添加一个新行并加上尾逗号即可。这使得版本控制历史较为干净,编辑代码也很简单。
— MDN 上的 尾逗号
数组的相等性是深水猛兽
数组之间进行相等比较是 JS 中的深水猛兽,看看这些例子:
💡 说明:
仔细阅读上面的例子!规范中的 7.2.13 抽象相等比较 一节描述了这些行为。
undefined 和 Number
无参数调用 Number
构造函数会返回 0
。我们知道,当函数没有接受到指定位置的实际参数时,该处的形式参数的值会是 undefined
。因此,你可能觉得当我们传入 undefined
时应当同样返回 0
。然而实际上传入 undefined
返回的是 NaN
。
💡 说明:
根据规范:
- 若无参数调用该函数,
n
将为+0
。 - 否则,
n
将为?ToNumber(value)
。 - 如果值为
undefined
,ToNumber(undefined)
应该返回NaN
。
这是相应的部分:
parseInt 是一个坏蛋
parseInt
以它的怪异而出名。
💡 说明:
这是因为 parseInt
会持续解析直到它解析到一个不识别的字符,'f*ck'
中的 f
是 16 进制下的 15
。
解析 Infinity
到整数也很有意思……
也要小心解析 null
:
💡 说明:
它将
null
转换成字符串 'null'
,并尝试转换它。对于基数 0 到 23,没有可以转换的数字,因此返回 NaN。而当基数为 24 时,第 14 个字母“n”
也可以作数字用。当基数为 31 时,第 21 个字母“u”
进入数字的行列,此时整个字符串都可以解析了。而当基数增加到 37 以上,已经超出了数字和字母所能表达的数字范围,因此一律返回 NaN
。— StackOverflow 上的 “parseInt(null, 24) === 23 什么鬼”
不要忘记八进制:
💡 说明:
当输入的字符串以“0”开始时,根据实现的不同,会被解释为八进制或十进制。ECMAScript 5 明确表示应当使用十进制,但有部分浏览器仍不支持。因此推荐在调用 parseInt
函数时总是传入表示基数的第二个参数。
parseInt
会先将参数值转换为字符串:
解析浮点数的时候要注意
💡 说明: parseInt
接受字符串参数并返回一个指定基数下的整数。parseInt
会将字符串中首个非数字字符(字符集由基数决定)及其后的内容全部截断。如 0.000001
被转换为 "0.000001"
,因此 parseInt
返回 0
。而 0.0000001
转换为字符串会变成 "1e-7"
,因此 parseInt
返回 1
。1/1999999
被转换为 5.00000250000125e-7
,所以 parseInt
返回 5
。
true 和 false 的数学运算
做一下数学计算:
嗯……🤔
💡 说明:
我们可以用 Number
构造函数将值强制转化成数值。很明显,true
将被强制转换为 1
:
一元加运算符会尝试将其值转换成数字。它可以转换字符串形式表达的整数和浮点数,以及非字符串值 true
、false
和 null
。如果它不能解析特定的值,它将转化为 NaN
。这意味着我们可以有更简便的方式将 true
转换成 1
:
当你执行加法或乘法时,将会 ToNumber
方法。根据规范,该方法的返回值为:
如果
参数
是 true,返回 1。如果参数
是 false,则返回 +0。
因此我们可以将布尔值相加并得到正确的结果
相应章节:
HTML 注释在 JavaScript 中有效
你可能会感到震惊,<!--
(这是 HTML 注释格式)也是一个有效的 JavaScript 注释。
💡 说明:
震惊吗?类 HTML 注释旨在容许不理解 <script>
标签的浏览器优雅降级。这些浏览器,例如 Netscape 1.x 已经不再流行。因此,在脚本标记中添加 HTML 注释是没有意义的。
由于 Node.js 基于 V8 引擎,Node.js 运行时也支持类似 HTML 的注释。而且,它们是规范的一部分:
NaN 不是一个数值
NaN
类型是 'number'
:
💡 说明:
typeof
和 instanceof
运算符的工作原理:
[] 和 null 是对象
💡 说明:
typeof
运算符的行为在本节的规范中定义:
根据规范,typeof
操作符返回一个字符串,且必须符合 Table 37: typeof。对于没有实现 [[Call]]
的 null
、普通对象、标准特异对象和非标准特异对象,它返回字符串 "object“
。
但是,你可以使用 toString
方法检查对象的类型。
神奇的数字增长
💡 说明:
这是由 IEEE 754-2008 二进制浮点运算标准引起的。极大的数字会被四舍五入到最近的偶数。阅读更多:
- 6.1.6 数字类型
- 维基百科上的IEEE 754
0.1 + 0.2 精度计算
来自 JavaScript 的知名笑话。0.1
和 0.2
相加是存在精度错误的
💡 说明:
来自于 StackOverflow 上的问题“浮点计算坏了?”的答案:
程序中的常量
0.2
和 0.3
是最接近真实值的近似值。最接近 0.2
的 double
大于有理数 0.2
,但最接近 0.3
的 double
小于有理数 0.3
。0.1
和 0.2
的和大于有理数 0.3
,因此在程序中进行常量比较会得到假。
这个问题太过于出名,甚至有一个网站叫 0.30000000000000004.com。这不仅仅是 JavaScript 特有的问题,在其他采用浮点计算的语言中也广泛存在。
扩展数字的方法
你可以向包装对象添加自己的方法,比如 Number
或 String
。
💡 说明:
显然,在 JavaScript 中扩展 Number
对象和扩展其他对象并无不同之处。但是,扩展不符合规范的函数行为是不推荐的。以下是 Number
属性的列表:
三个数字的比较
💡 说明:
为什么会这样呢?其实问题在于表达式的第一部分。以下是它的工作原理:
我们可以用 大于或等于运算符(>=
):
详细了解规范中的关系运算符:
有趣的数学
通常 JavaScript 中的算术运算的结果可能是非常难以预料的。 考虑这些例子:
💡 说明:
前四个例子发生了什么?你可以参考此处的给出的关于 JavaScript 中的加法的对照表:
那其他例子呢?在相加之前,[]
和 {}
隐式调用 ToPrimitive
和 ToString
方法。详细了解规范中的求值过程:
不过需要注意此处的 {} + []
,这是一个例外。你可以发现它的求值结果与 [] + {}
不同,这是因为当我们不加括号时,它被当作是一个空的代码块和一个一元加法运算符,这个运算符会把其后的 []
转换为数字。具体如下:
当我们加上括号,情况就不一样了:
正则表达式的加法
你知道可以做这样的运算吗?
💡 说明:
字符串不是 String 的实例
💡 说明:
String
构造函数返回一个字符串:
再试试 new
:
对象?啥玩意?
有关规范中的 String 构造函数的更多信息:
用反引号调用函数
我们来声明一个返回所有参数的函数:
你肯定知道调用这个函数的方式应当是:
但是你知道你还可以使用反引号调用任意函数吗?
💡 说明:
其实,如果你熟悉 标签模板字面量,你会知道这不是什么魔法。在上面的例子中,f
函数是模板字面量的标签。你可以定义这个标签以使用函数解析模板文字。标签函数的第一个参数是包含字符串的数组,剩余的参数与表达式有关。例:
这也是在 React 社区很流行的库💅 styled-components的背后的秘密。
规范的链接:
到底 call 了谁
由 @cramforce 发现
💡 说明:
注意,这可能会击碎你的三观!尝试在您的头脑中重现此代码:我们使用 apply
方法调用 call
方法。阅读更多:
- 19.2.3.3 Function.prototype.call(thisArg, …args)
- **19.2.3.1 ** Function.prototype.apply(thisArg, argArray)
constructor 属性
💡 说明:
让我们逐步分解这个例子:
Object.prototype.constructor
返回一个创建示例对象的 Object
构造函数引用。当当前对象是字符串时,它是 String
;当当前对象是数字时,它是 Number
;以此类推。
将对象做为另一个对象的 key
💡 说明:
为何可以正常运行?这里我们使用的是 计算属性。当你将对象用方括号括起来当作对象的属性名时,它会将对象强制转换成一个字符串,所以我们得到属性键是 [object Object]
,其值为 {}
。
体验一下简单的“括号地狱”:
关于对象字面量,点击这里阅读更多:
- 对象初始化 at MDN
访问原型 __proto__
我们知道,原始数据(premitives)是没有原型的。但是,如果我们尝试获取原始数据的 __proto__
属性的值,我们会得到这样的一个结果:
💡 说明:
这是因为原始数据的没有原型,它将使用 ToObject
方法包装在包装器对象中。这个步骤如下所示:
以下是关于 __proto__
的更多信息:
`${{Object}}`
下面的表达式结果如何?
答案是:
💡 说明:
我们通过 简写属性表示 使用一个 Object
属性定义了一个对象:
然后我们将该对象传递给模板文字,toString
方法调用该对象。这就是为什么我们得到字符串 '[object Object]'
。
- 12.2.9 模板字面量
- MDN 上的对象初始化
使用默认值解构
考虑这个例子:
这在面试中是一个很好的问题。问 y
的值是什么? 答案是:
💡 说明:
以上示例:
- 我们声明了
x
,但没有立刻赋值,所以它是undefined
。 - 我们将
x
的值打包到对象属性x
中。 - 我们使用解构来提取
x
的值,并且要将这个值赋给y
。如果未定义该值,那么我们将使用1
作为默认值。 - 返回
y
的值。
- MDN 上的对象初始化
点和扩展运算符
数组的扩展可以组成有趣的例子。考虑这个:
💡 说明:
为什么是 3?当我们使用扩展运算符时,@@iterator
方法会被调用,而返回的迭代器用于获取要迭代的值。字符串的默认迭代器按字符展开字符串。展开之后,我们把这些字符打包成一个数组。然后再展开这个数组并再打包回数组。
一个 '...'
字符串包含 .
,所以结果数组的长度将 3
。
现在,一步一步的看:
显然,我们可以展开和包装数组的元素任意多次,只要你想:
标签
很多程序员不知道 JavaScript 中也有标签,并且很有趣:
💡 说明:
带标签的语句与 break
或 continue
语句一起使用。您可以使用标签来标识循环,然后使用 break
或 continue
语句来指示程序是否应该中断循环或继续执行它。
在上面的例子中,我们识别一个标签 foo
。然后 console.log('first');
执行,然后中断执行。
详细了解 JavaScript 中的标签:
- 13.13 标签语句
- 标签语句 at MDN
嵌套标签
💡 说明:
和上面的例子类似,请遵循以下链接:
- 12.16 逗号运算符(,)
- 13.13 标签语句
- 标签语句 at MDN
阴险的 try..catch
这个表达式将返回什么?2
还是 3
?
答案是 3
。惊讶吗?
💡 说明:
这是多重继承吗?
看下面的例子:
这是多重继承吗?不。
💡 说明:
有趣的部分是 extends
子句的值((String,Array)
)。分组运算符总是返回其最后一个参数,所以 (String,Array)
实际上只是 Array
。 这意味着我们刚刚创建了一个扩展 Array
的类。
yield 返回自身的生成器
考虑这个 yield 返回自身的生成器例子:
如您所见,返回的值是一个值等于 f
的对象。那样的话,我们可以做这样的事情:
💡 说明:
要理解为什么这样工作,请阅读规范的这些部分:
类的类
考虑这个混淆语法:
似乎我们在类内部声明了一个类。应该是个错误,然而,我们得到一个 'object'
字符串。
💡 说明:
ECMAScript 5 时代以来,允许 关键字 作为 属性名称。请看下面这个简单的对象示例:
还有 ES6 标准中的简写方法定义。此外,类也可以是匿名的。因此,如果我们删去 : function
部分,将会得到:
默认类的结果总是一个简单的对象。其类型应返回 'object'
。
在这里阅读更多
不可转换类型的对象
有一种方法可以摆脱类型的转换,那就是使用内置符号:
现在我们可以这样使用:
💡 说明:
棘手的箭头函数
考虑下面的例子:
这看起来没问题,但是如果这样呢?
💡 说明:
你可能觉得应该返回 {}
而不是 undefined
。这是因为花括号是箭头函数语法的一部分,所以 f
会返回 undefined
。不过要从箭头函数明确返回 {}
对象也是有可能的,这时你需要用括号把返回值括起来。
箭头函数不能作为构造函数
考虑下面的例子:
现在,试着用箭头函数做同样的事情:
💡 说明:
箭头函数不能作为构造函数调用,并且会在 new
的时候抛出错误。因为它具有词域 this
,而且它也没有 prototype
属性,所以这样做没什么意义。
arguments 和箭头函数
考虑下面的例子:
现在,试着用箭头函数做同样的事情:
💡 说明:
箭头函数是常规函数的轻量级版本,注重于短小和词域 this
。同时箭头函数不提供 arguments
对象的绑定。你可以使用 剩余参数(rest parameters)
来得到同样的结果:
- MDN 上的箭头函数
棘手的返回
return
语句是很棘手的. 看下面的代码:
💡 说明:
return
和返回的表达式必须在同一行:
这是因为一个叫自动分号插入的概念,它会在大部分换行处插入分号。第一个例子里,return
语句和对象字面量中间被插入了一个分号。所以函数返回 undefined
,其后的对象字面量永远不会被求值。
对象的链式赋值
从右到左,{n: 2}
被赋值给 foo
,而此赋值的结果 {n: 2}
被赋值给 foo.x
,因此 bar
是 {n: 1, x: {n: 2}}
,毕竟 bar
是 foo
的一个引用。但为什么 foo.x
是 undefined
而 bar.x
不是呢?
💡 说明:
foo
和 bar
引用同一个对象 {n: 1}
,而左值在赋值前解析。foo = {n: 2}
是创建一个新对象,所以 foo
被更新为引用那个新的对象。因为 foo.x = ...
中的 foo
作为左值在赋值前就被解析并依然引用旧的 foo = {n: 1}
对象并为其添加了 x
值。在链式赋值之后,bar
依然引用旧的 foo
对象,但 foo
更新为没有 x
属性的 {n: 2}
对象。
它等价于:
使用数组访问对象属性
那关于伪多维数组创建对象呢?
💡 说明:
[]
操作符会使用 toString
将传递的表达式转换为字符串。将单元素数组转换为字符串,相当于将这个元素转换为字符串:
Number.toFixed() 显示不同的数字
Number.toFixed()
在不同的浏览器中会表现得有点奇怪。看看这个例子:
💡 说明:
尽管你的第一直觉可能是 IE11 是正确的而 Firefox/Chrome 错了,事实是 Firefox/Chrome 更直接地遵循数字运算的标准(IEEE-754 Floating Point),而 IE11 经常违反它们(可能)去努力得出更清晰的结果。
你可以通过一些快速的测试来了解为什么它们发生:
浮点数在计算机内部不是以一系列十进制数字的形式存储的,而是通过一个可以产生一点点通常会被 toString 或者其他调用取整的不准确性的更复杂的方法,但它实际上在内部会被表示。
在这里,那个结尾的 “5” 实际上是一个极其小的略小于 5 的分数。将其以任何常理的长度取整它都会被看作一个 5,但它在内部通常不是 5。
然而 IE11 会直接在这个数字后面补 0,甚至在 toFixed(20) 的时候也是这样,因为它看起来强制取整了值来减少硬件限制带来的问题。
详见 ECMA-262 中 NOTE 2
的 toFixed
的定义。
min 大于 max
我发现一个神奇的例子:
💡 说明:
这是一个简单的例子。我们一步一步来:
为什么是这样呢?其实 Math.max()
并不会返回最大的正数,即 Number.MAX_VALUE
。
Math.max
接受两个参数,将它们转换到数字,比较之后返回最大的那个。若没有传入参数,结果将是 -∞。若参数中存在 NaN
,则返回 NaN
。
反过来,当 Math.min
没有传入参数,会返回 ∞。
- 15.8.2.11 Math.max
- 15.8.2.11 Math.min
- 为什么 Math.max() 小于 Math.min()?##
Math.max()
小于Math.min()
💡 说明:
- Charlie Harvey 的Why is Math.max() less than Math.min()?
比较 null 和 0
下面的表达式似乎有点矛盾:
既然 null >= 0
返回 true
,为什么 null
既不等于也不大于 0
?(对于小于比较也可以得出相似的结果。)
💡 说明:
这三个表达式的求值方式各不相同,因此产生了非预期的结果。
首先,对于 null == 0
这个抽象相等比较操作,通常当该运算符不能正确地比较两边的值,则它会将两边的值都转换为数字,再对数字进行比较。那么,您可能会期望以下行为:
然而,仔细阅读规范就会发现,数字转换实际上并没有发生在 null
或 undefined
的一侧。也就是说,如果在等号的一侧有 null
,则当另一侧的表达式为 null
或 undefined
就返回 true
;反之则返回 false
。
接下来,对于 null > 0
这个比较关系。与抽象相等运算符的算法不同,它 会 先将 null
转换为一个数字。因此,我们得到这样的行为:
最后一个,对于 null >= 0
的比较关系。你可能认为这个表达式应该等同于 null > 0 || null == 0
的结果;如果真是这样,那么基于上述的讨论,这里的结果也应当是 false
才对。然而,>=
操作符的工作方式实际上是 <
操作符的取反。在我们上述的讨论中,关于大于运算符的论述也适用于小于运算符,也就是说这个表达式的值是这样出来的:
相同变量重复声明
JS 允许重复声明变量:
严格模式也可以运行:
💡 解释:
所有的定义都被合并成一条定义。
Array.prototype.sort() 的默认行为
假设你需要对数组排序。
💡 说明:
默认的排序算法基于将给定元素转换为字符串,然后比较它们的 UTF-16 序列中的值。
提示
传入一个 compareFn
比较函数,对非字符串的其他值排序。
resolve() 不会返回 Promise 实例
从 thePromise
接收到的 value
值确实是 theObject
。
那么,如果向 resolve
传入另外一个 Promise
会怎样?
💡 说明:
此函数将类 promise 对象的多层嵌套平铺到单层嵌套。(例如上述的 promise 函数 resolve 了另一个会 resolve 出其他对象的 promise 函数)
– MDN 上的 Promise.resolve()
官方规范是 ECMAScript 25.6.1.3.2 Promise 的 Resolve 函数,但是这一章节对人类非常不友好。
{}{} 是 undefined
你可以在终端测试一下。类似这样的结构会返回最后定义的对象中的值。
💡 说明:
解析到 {}
会返回 undefined
,而解析 {foo: 'bar'}{}
时,表达式 {foo: 'bar'}
返回 'bar'
。
{}
有两重含义:表示对象,或表示代码块。例如,在 () => {}
中的 {}
表示代码块。所以我们必须加上括号:() => ({})
才能让它正确地返回一个对象。
因此,我们现在将 {foo: 'bar'}
当作代码块使用,则可以在终端中这样写:
啊哈,一样的结果!所以 {foo: 'bar'}{}
中的花括号就是表示代码块。
arguments 绑定
考虑以下函数:
💡 说明
arguments
是一个类数组对象,包含了所有传入当前函数的参数。当没有传入参数时,该对象中就不存在 x
属性,也就无法覆盖。
- arguments 对象 on MDN
来自地狱的 alert
如题,从地狱而来的代码:
💡 说明
这一串代码是基于多个采用了八进制转义序列的字符串构造的。
任何码值小于 256 的字符(又称扩展 ASCII 码表域)都可以用 \
加上其八进制代码的转义方式写出来。上面这个简单的例子就是将 alert
编码到八进制转义序列。
没有尽头的计时
如果我们对 setTimeout
赋予无限大会如何?
结果是,它会立即运行,并没有等待无限长的时间。
💡 说明:
通常运行时内部会将延时存储为一个 32 位的有符号整数,而上述代码会导致运行时在解析延时参数时发生整数溢出,从而使函数立即执行而不等待。
例如,在 Node.js 中我们可以看到这样的警告信息:
- WindowOrWorkerGlobalScope.setTimeout() on MDN
- Node.js 文档中关于计时器的章节
- W3C 上的 [计时器]](https://www.w3.org/TR/2011/WD-html5-20110525/timers.html)
setTimeout 对象
如果我们给 setTimeout
的回调函数参数传非函数值会发生什么?
没问题。
这个也没问题。
抛出了一个 SyntaxError(语法错误)。
这种错误很容易发生,尤其是当你有个函数返回一个对象,但是你忘了将其传进函数,直接就在这里调用了!不过,如果 content-policy
设置为 self
会怎么样呢?
终端会拒绝执行!
💡 说明:
WindowOrWorkerGlobalScope.setTimeout()
的第一个参数可以是代码(code
),代码会被传递到 eval
函数,这是不好的。eval
会把所有输入强制转换为字符串,然后进行求值,那么对象会变成 '[object Object]'
;嗯,你也看到了,这里确实有一个非法标识符 'Unexpected identifier'
。
- eval() on MDN (don’t use this)
- WindowOrWorkerGlobalScope.setTimeout() on MDN
- 内容安全策略
- W3C 上的计时器
点点运算符
现在尝试把一个数字转换到字符串:
如果我们再加上一个点呢?
那为什么第一个例子错了呢?
💡 说明:
这是文法的限制。
.
运算符存在歧义,它既可以当属性访问符,也可以是小数点,这取决于它在代码中的位置。
规范中定义了 .
运算符仅在特定的位置使用时会被当作小数点,这个定义写在 ECMAScript 的数字字面量语法一节中。
所以,当你想要在数字后加属性访问器的点号时,应当加上括号,或再加上一个点,以使该表达式合法。
- JavaScript 中 toString 的用法 on StackOverflow
- 为什么 10…toString() 可行,而 10.toString() 却不行?
再 new 一次
这仅仅是一个用于娱乐的例子。
💡 说明:
JavaScript 与其他面向对象语言不同,它的构造函数仅是一个比较特殊的函数。虽然 class 语法糖让你可以创建一个字面上的类,但实例化后它就变成了函数,因此它可以再次实例化。
虽然我没有测试过,但我觉得最后的那个表达式应该是这样分析的:
再补充一下,运行 new Function('return "bar";')
必然会创建一个内容为 return "bar";
的函数对象。而Foo
类的构造函数中的 super()
调用的是 Function
的构造函数,所以自然而然我们可以在它上面添加更多的操作。
你应该用上分号
下面这个应该是标准的 JavaScript……吧?不,它炸了!
woc……?
💡 说明:
嗯,你没猜错,这又是自动分号插入的功劳。
上面这个例子实际上会被转换为:
看到了吧,str
这个字符串被赋值到属性 array
上。
- Ryan Cavanaugh 发布的关于这个例子的原创推特
- TC39 会议中关于它的讨论
用空格分割(split)字符串
你试过用空格分割字符串吗?
💡 说明:
这是预期行为。它会在输入的字符串中遍历,一旦发现分隔符,就在此处分割。但若你传入的是空字符串,它找不到分隔符,因此返回该字符串。
规范引用如下:
它会从左向右搜索字符串,并根据
separator
(分隔符)决定子字符串的分割位置;分割位置的字符仅用于分割,不会包含在返回的数组中。
- 22.1.3.21 String.prototype.split
- Ryan Cavanaugh 发布的关于这个例子的原创推特
- Nabil Tharwat 发布的包含解释的推特
对字符串 stringify
这会导致一个缺陷,我曾经修了好几天:
💡 说明:
先看看 JSON.stringify
的返回值:
原来是被“字串化”了,所以这也难怪:
对数字和 true 的非严格相等比较
💡 说明:
根据规范:
比较 x == y 时,当 x 和 y 都有值,会返回 true 或 false。比较过程如下所述:
- 若
Type(x)
是数字且Type(y)
是字符串,则会返回x == ! ToNumber(y)
的结果。
所以比较过程是这样的:
其他资源
- wtfjs.com — 一些非常特别的不规范与不一致的集合,以及对于 web 编程语言来说非常痛苦的时光。
- Wat — CodeMash 2012 中 Gary Bernhardt 的演讲
- What the… JavaScript? — Kyle Simpsons 在 Forward 2 的演讲,描述了“疯狂的 JavaScript”。他希望帮助你写出更干净、更优雅、更易读的代码,鼓励人们为开源社区做出贡献。
- Zeros in JavaScript — 针对 JavaScript 中的
==
、===
、+
和*
的真值表。