What the f*ck JavaScript?
一个有趣和棘手的 JavaScript 示例列表。
JavaScript 是一种很好的语言。它有一个简单的语法,庞大的生态系统,以及最重要,最伟大的社区。
同时,我们都知道,JavaScript 是一个非常有趣又充满戏法的语言。他们中的有些可以迅速将我们的日常工作变成地狱,有些可以让我们大声笑起来。
WTFJS 的原创思想属于 Brian Leroux。这个列表受到他的讲话的高度启发 “WTFJS” at dotJS 2012:
npm 手稿
你可以通过 npm
来安装。只要运行:
$ npm install -g wtfjs
你应该能够在命令行中运行wtfjs
,这将打开手册并在你选择的$PAGER
中,否则你也可以选择在这里阅读。
- 💪🏻 动机
- ✍🏻 符号
- 👀 例子
-
数组中的逗号
- 💡 说明:
- 数组相等是一个怪物
-
undefined
和Number
parseInt
是一个坏蛋-
true
和false
数学运算 - HTML 注释在 JavaScript 中有效
-
NaN
不是一个数值 -
[]
和null
是对象 - 神奇的数字增长
-
0.1 + 0.2
精度计算 - 扩展数字的方法
- 三个数字的比较
- 有趣的数学
- 正则表达式的加法
-
字符串不是
String
的实例 - 用反引号调用函数
- 调用 调用 调用
-
一个
constructor
属性 - 将对象做为另一个对象的 key
-
访问原型
__proto__
-
`${{Object}}`
- 使用默认值解构
- 点和扩展运算符
- 标签
- 嵌套标签
-
阴险的
try..catch
- 这是多重继承吗?
- 一个类的类
- 非强制对象
- 棘手的箭头功能
- 箭头函数不能作为构造器
-
arguments
和箭头函数 - 棘手的返回
- 对象的链式赋值
- 使用数组访问对象属性
- Null 和关系运算符
-
Number.toFixed()
显示不同的数字 -
Math.max()
小于Math.min()
-
比较
null
和0
- 相同变量重复声明
- Array.prototype.sort() 的默认行为
- resolve() 不会返回 Promise 实例
- 其他资源
- 🎓 License
💪🏻 动机
只是为了好玩
— “只是为了好玩:一个意外革命的故事”, Linus Torvalds
这个列表的主要目的是收集一些疯狂的例子,并解释它们如何工作,如果可能的话。只是因为学习以前不了解的东西很有趣。
如果您是初学者,您可以使用此笔记来深入了解 JavaScript。我希望这个笔记会激励你花更多的时间阅读规范。
如果您是专业开发人员,您可以将这些示例视为您公司新手访问问题和测验的重要资源。同时,这些例子在准备面试时会很方便。
无论如何,读读看。也许你会为自己找到新的东西。
✍🏻 符号
// ->
用于显示表达式的结果。例如:
1 + 1; // -> 2
// >
意思是 console.log
或其他输出的结果。例如:
console.log("hello, world!"); // > hello, world!
//
只是一个解释的评论。例如:
// Assigning a function to foo constant const foo = function() {};
👀 例子
[]
等于 ![]
数组等于一个数组取反:
[] == ![]; // -> true
💡 说明:
true 是 false
!!"false" == !!"true"; // -> true !!"false" === !!"true"; // -> true
💡 说明:
考虑一下这一步:
true == "true"; // -> true false == "false"; // -> false // 'false' 不是空字符串,所以它的值是 true !!"false"; // -> true !!"true"; // -> true
baNaNa
"b" + "a" + +"a" + "a";
用 JavaScript 写的老派笑话:
"foo" + +"bar"; // -> 'fooNaN'
💡 说明:
这个表达式可以转化成 'foo' + (+'bar')
,但无法将'bar'
强制转化成数值。
NaN
不是一个 NaN
NaN === NaN; // -> false
💡 说明:
规范严格定义了这种行为背后的逻辑:
- 如果
Type(x)
不同于Type(y)
, return false.- 如果
Type(x)
数值, 然后
- 如果
x
是 NaN, return false.- 如果
y
是 NaN, return false.- … … …
遵循 IEEE 的“NaN”的定义:
有四种可能的相互排斥的关系:小于,等于,大于和无序。 当至少一个操作数是 NaN 时,便是最后一种情况。每个 NaN 都要比较无穷无尽的一切,包括自己。
— “对于 IEEE754 NaN 值的所有比较返回 false 的理由是什么?” at StackOverflow
它是 fail
你不会相信,但...
(![] + [])[+[]] + (![] + [])[+!+[]] + ([![]] + [][[]])[+!+[] + [+[]]] + (![] + [])[!+[] + !+[]]; // -> 'fail'
💡 说明:
将大量的符号分解成片段,我们注意到,以下表达式经常出现:
![] + []; // -> 'false' ![]; // -> false
所以我们尝试将[]
和false
加起来。 但是通过一些内部函数调用(binary + Operator
- >ToPrimitive
- >[[DefaultValue]
]),我们最终将右边的操作数转换为一个字符串:
![] + [].toString(); // 'false'
将字符串作为数组,我们可以通过[0]
来访问它的第一个字符:
"false"[0]; // -> 'f'
现在,其余的是明显的,可以自己弄清楚!
[]
是 true
, 但它不等于 true
数组是一个true
,但是它不等于true
。
!![] // -> true [] == true // -> false
💡 说明:
以下是 ECMA-262 规范中相应部分的链接:
null
是 false, 但又不等于 false
尽管 null
是 false
,但它不等于 false
。
!!null; // -> false null == false; // -> false
同时,其他的一些等于 false 的值,如 0
或 ''
等于 false
。
0 == false; // -> true "" == false; // -> true
💡 说明:
跟前面的例子相同。这是一个相应的链接:
document.all
是一个 object,但又同时是 undefined
⚠️ 这是浏览器 API 的一部分,对于 Node.js 环境无效 ⚠️
尽管 document.all 是一个 array-like object 并且通过它可以访问页面中的 DOM 节点,但在通过 typeof
的检测结果是 undefined
。
document.all instanceof Object; // -> true typeof document.all; // -> 'undefined'
同时,document.all
不等于 undefined
。
document.all === undefined; // -> false document.all === null; // -> false
但是同时:
document.all == null; // -> true
💡 说明:
document.all
曾经是访问页面 DOM 节点的一种方式,特别是在早期版本的 IE 浏览器中。它从未成为标准,但被广泛使用在早期的 JS 代码中。当标准演变出新的 API 时(例如document.getElementById
)这个 API 调用就被废弃了,标准委员会必须决定如何处理它。因为它被广泛使用嗯他们决定保留这个 API 但引入一个有意的对 JavaScript 的标准的违反。 其与undefined
使用严格相等比较得出false
而使用抽象相等比较 得出true
是因为这个有意的对标准的违反明确地允许了这一点。— “Obsolete features - document.all” at WhatWG - HTML spec — “Chapter 4 - ToBoolean - Falsy values” at YDKJS - Types & Grammar
最小值大于零
Number.MIN_VALUE
是最小的数字,大于零:
Number.MIN_VALUE > 0; // -> true
💡 说明:
Number.MIN_VALUE
是5e-324
,即可以在浮点精度内表示的最小正数,即可以达到零。 它定义了浮点数的最高精度。
现在,整体最小的值是
Number.NEGATIVE_INFINITY
,尽管这在严格意义上并不是真正的数字。— “为什么在 JavaScript 中
0
小于Number.MIN_VALUE
?” at StackOverflow
函数不是函数
⚠️ V8 v5.5 或更低版本中出现的 Bug(Node.js <= 7) ⚠️
你们所有人都知道的关于讨厌的 undefined 不是 function ,但是这个呢?
// Declare a class which extends null class Foo extends null {} // -> [Function: Foo] new Foo() instanceof null; // > TypeError: function is not a function // > at … … …
💡 说明:
这不是规范的一部分。这只是一个错误,现在它已被修复,所以将来不会有这个问题。
数组相加
如果您尝试两个数组相加呢?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
💡 说明:
会发生合并。一步一步地,它是这样的:
[1, 2, 3] + [4, 5, 6][ // joining (1, 2, 3) ].join() + [4, 5, 6].join(); // concatenation "1,2,3" + "4,5,6"; // -> ("1,2,34,5,6");
数组中的逗号
您已经创建了一个包含 4 个空元素的数组。尽管如此,你还是会得到一个有三个元素的,因为后面的逗号:
let a = [, , ,]; a.length; // -> 3 a.toString(); // -> ',,'
💡 说明:
尾逗号 (有时也称为“最后逗号”) 在向 JavaScript 代码中添加新元素、参数或属性时有用。如果您想添加一个新属性,您可以简单地添加一个新行,而不用修改以前的最后一行,如果该行已经使用了后面的逗号。这使得版本控制比较清洁和编辑代码可能不太麻烦。
— Trailing commas at MDN
数组相等是一个怪物
数组进行相等比较是一个怪物,看下面的例子:
[] == '' // -> 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 Abstract Equality Comparison 规范描述了这些行为。
undefined
和 Number
如果我们不把任何参数传递到 Number
构造函数中,我们将得到 0
。undefined
是一个赋值形参,没有实际的参数,所以您可能期望 NaN
将 undefined
作为参数的值。然而,当我们通过 undefined
,我们将得到 NaN
。
Number(); // -> 0 Number(undefined); // -> NaN
💡 说明:
根据规范:
- 如果没有参数传递给这个函数,让
n
为+0
; - 否则,让
n
调用ToNumber(value)
- 如果值为
undefined
,那么ToNumber(undefined)
应该返回NaN
.
这是相应的部分:
parseInt
是一个坏蛋
parseInt
它以的怪异而出名。
parseInt("f*ck"); // -> NaN parseInt("f*ck", 16); // -> 15
**💡 说明:
** 这是因为 parseInt
会持续通过解析直到它解析到一个不识别的字符,'f*ck'
中的 f
是 16 进制下的 15
。
解析 Infinity
到整数也很有意思…
// parseInt("Infinity", 10); // -> NaN // ... parseInt("Infinity", 18); // -> NaN... parseInt("Infinity", 19); // -> 18 // ... parseInt("Infinity", 23); // -> 18... parseInt("Infinity", 24); // -> 151176378 // ... parseInt("Infinity", 29); // -> 385849803 parseInt("Infinity", 30); // -> 13693557269 // ... parseInt("Infinity", 34); // -> 28872273981 parseInt("Infinity", 35); // -> 1201203301724 parseInt("Infinity", 36); // -> 1461559270678... parseInt("Infinity", 37); // -> NaN
也要小心解析 null
:
parseInt(null, 24); // -> 23
💡 说明:
它将
null
转换成字符串'null'
,并尝试转换它。 对于基数 0 到 23,没有可以转换的数字,因此返回 NaN。 在 24,“n”
,第 14 个字母被添加到数字系统。 在 31,“u”
,添加第 21 个字母,可以解码整个字符串。 在 37 处,不再有可以生成的有效数字集,并返回NaN
。— “parseInt(null, 24) === 23… wait, what?” at StackOverflow
不要忘记八进制:
parseInt("06"); // 6 parseInt("08"); // 8 如果支持 ECMAScript 5 parseInt("08"); // 0 如果不支持 ECMAScript 5
💡 说明:
这是因为 parseInt
能够接受两个参数,如果没有提供第二个参数,并且第一个参数以 0
开始,它将把第一个参数当做八进制数解析。
parseInt
总是把输入转为字符串:
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2 Number({ toString: () => 2, valueOf: () => 1 }); // -> 1
解析浮点数的时候要注意
parseInt(0.000001); // -> 0 parseInt(0.0000001); // -> 1 parseInt(1 / 1999999); // -> 5
💡 说明: ParseInt
接受字符串参数并返回一个指定基数下的证书。ParseInt
也去除第一个字符串中非数字字符(字符集由基数决定)后的内容。0.000001
被转换为 "0.000001"
而 parseInt
返回 0
。当 0.0000001
被转换为字符串时它被处理为 "1e-7"
因此 parseInt
返回 1
。1/1999999
被转换为 5.00000250000125e-7
而 parseInt
返回 5
。
true
和 false
数学运算
我们做一些数学计算:
true + true( // -> 2 true + true ) * (true + true) - true; // -> 3
嗯… 🤔
💡 说明:
我们可以用 Number
构造函数强制转化成数值。 很明显,true
将被强制转换为 1
:
Number(true); // -> 1
一元加运算符尝试将其值转换成数字。 它可以转换整数和浮点的字符串表示,以及非字符串值 true
,false
和 null
。 如果它不能解析特定的值,它将转化为 NaN
。 这意味着我们可以更容易地强制将 true
换成 1
+true; // -> 1
当你执行加法或乘法时,ToNumber
方法调用。 根据规范,该方法返回:
如果
参数
is true , 返回 1 。 如果参数
是 false 返回 +0。
这就是为什么我们可以进行进行布尔值相加并得到正确的结果
相应部分:
HTML 注释在 JavaScript 中有效
你会留下深刻的印象,<!--
(这是 HTML 注释)是一个有效的 JavaScript 注释。
// 有效注释 <!-- 也是有效的注释
💡 说明:
感动吗? 类似 HTML 的注释旨在允许不理解标签的浏览器优雅地降级。这些浏览器,例如 Netscape 1.x 已经不再流行。因此,在脚本标记中添加 HTML 注释是没有意义的。
由于 Node.js 基于 V8 引擎,Node.js 运行时也支持类似 HTML 的注释。 而且,它们是规范的一部分:
NaN
不是一个数值
尽管 NaN
类型是 'number'
,但是 NaN
不是数字的实例:
typeof NaN; // -> 'number' NaN instanceof Number; // -> false
💡 说明:
typeof
和 instanceof
运算符的工作原理:
[]
和 null
是对象
typeof []; // -> 'object' typeof null; // -> 'object' // 然而 null instanceof Object; // false
💡 说明:
typeof
运算符的行为在本节的规范中定义:
根据规范,typeof
操作符返回一个字符串 Table 35: typeof
Operator Results。对于没有 [[Call]]
实现的 null
、普通对象、标准特异对象和非标准特异对象,它返回字符串 "object“
。
但是,您可以使用 toString
方法检查对象的类型。
Object.prototype.toString.call([]); // -> '[object Array]' Object.prototype.toString.call(new Date()); // -> '[object Date]' Object.prototype.toString.call(null); // -> '[object Null]'
神奇的数字增长
999999999999999; // -> 999999999999999 9999999999999999; // -> 10000000000000000
💡 说明:
这是由 IEEE 754-2008 二进制浮点运算标准引起的。阅读更多:
- 6.1.6 The Number Type
- IEEE 754 on Wikipedia
0.1 + 0.2
精度计算
来自 JavaScript 的知名笑话。0.1
和 0.2
相加是存在精度错误的
0.1 + 0.2( // -> 0.30000000000000004 0.1 + 0.2 ) === 0.3; // -> false
💡 说明:
”浮点计算坏了?” 问题的答案在 StackOverflow:
程序中的常量
0.2
和0.3
也将近似为真实值。最接近0.2
的double
大于有理数0.2
,但最接近0.3
的double
小于有理数0.3
。0.1
和0.2
的总和大于有理数0.3
,因此不符合您的代码中的常数判断。
这个问题是众所周知的,甚至有一个网站叫 0.30000000000000004.com。
扩展数字的方法
您可以添加自己的方法来包装对象,如 Number
或 String
。
Number.prototype.isOne = function() { return Number(this) === 1; }; (1.0).isOne(); // -> true (1).isOne(); // -> true (2.0) .isOne()( // -> false 7 ) .isOne(); // -> false
💡 说明:
显然,您可以像 JavaScript 中的任何其他对象一样扩展 Number
对象。但是,不建议扩展不属于规范的行为定义。以下是 Number
属性的列表:
三个数字的比较
1 < 2 < 3; // -> true 3 > 2 > 1; // -> false
💡 说明:
为什么会这样呢?其实问题在于表达式的第一部分。以下是它的工作原理:
1 < 2 < 3; // 1 < 2 -> true true < 3; // true -> 1 1 < 3; // -> true 3 > 2 > 1; // 3 > 2 -> true true > 1; // true -> 1 1 > 1; // -> false
我们可以用 _大于或等于运算符(>=
)_:
3 > 2 >= 1; // true
详细了解规范中的关系运算符:
有趣的数学
通常 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
💡 说明:
前四个例子发生了什么?这是一个小表,以了解 JavaScript 中的添加:
Number + Number -> addition Boolean + Number -> addition Boolean + Boolean -> addition Number + String -> concatenation String + Boolean -> concatenation String + String -> concatenation
剩下的例子呢?在相加之前,[]
和 {}
隐式调用 ToPrimitive
和 ToString
方法。详细了解规范中的求值过程:
- 12.8.3 The Addition Operator (
+
) - 7.1.1 ToPrimitive(
input
[,PreferredType
]) - 7.1.12 ToString(
argument
)
正则表达式的加法
你知道可以做这样的运算吗?
// Patch a toString method RegExp.prototype.toString = function() { return this.source; } / 7 / -/5/; // -> 2
💡 说明:
字符串不是 String
的实例
"str"; // -> 'str' typeof "str"; // -> 'string' "str" instanceof String; // -> false
💡 说明:
String
构造函数返回一个字符串:
typeof String("str"); // -> 'string' String("str"); // -> 'str' String("str") == "str"; // -> true
我们来试试一个 new
:
new String("str") == "str"; // -> true typeof new String("str"); // -> 'object'
对象?那是什么?
new String("str"); // -> [String: 'str']
有关规范中的 String 构造函数的更多信息:
用反引号调用函数
我们来声明一个返回所有参数的函数:
function f(...args) { return args; }
毫无疑问,你知道你可以这样调用这个函数:
f(1, 2, 3); // -> [ 1, 2, 3 ]
但是你知道你可以使用反引号来调用任何函数吗?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`; // -> [ [ 'true is ', ', false is ', ', array is ', '' ], // -> true, // -> false, // -> [ 1, 2, 3 ] ]
💡 说明:
那么,如果你熟悉 标签模板字面量 ,这根本就不是魔术。在上面的例子中,f
函数是模板字面量的标签。模板文字之前的标签允许您使用函数解析模板文字。标签函数的第一个参数包含字符串值的数组。其余的参数与表达式有关。例:
function template(strings, ...keys) { // 用字符串和键做一些事情 }
这是 React 社区很流行的库💅 styled-components的背后的秘密。
规范的链接:
调用 调用 调用
发现于 @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
💡 说明:
注意,可能会打破你的头脑! 尝试在您的头脑中重现此代码:我们使用apply
方法应用call
方法。 阅读更多:
- 19.2.3.3 Function.prototype.call(
thisArg
, ...args
) - *19.2.3.1 * Function.prototype.apply(
thisArg
,argArray
)
一个 constructor
属性
const c = "constructor"; c[c][c]('console.log("WTF?")')(); // > WTF?
💡 说明:
让我们逐步考虑一下这个例子:
// 声明一个新的常字符串 'constructor' const c = "constructor"; // c 是一个字符串 c; // -> 'constructor' // 获取字符串的构造函数 c[c]; // -> [Function: String] // 获取构造函数的构造函数 c[c][c]; // -> [Function: Function] // 调用函数构造函数并将新函数的主体作为参数传递 c[c][c]('console.log("WTF?")'); // -> [Function: anonymous] // 然后调用这个匿名函数得到的结果是一个字符串 'WTF' c[c][c]('console.log("WTF?")')(); // > WTF
一个 Object.prototype.constructor
返回一个引用对象的构造函数创建的实例对象。在字符串的情况下,它是 String
,在数字的情况下它是 Number
等等。
将对象做为另一个对象的 key
{ [{}]: {} } // -> { '[object Object]': {} }
💡 说明:
为什么这样工作?这里我们使用 已计算的属性名称 。当这些方括号之间传递一个对象时,它会将对象强制转换成一个字符串,所以我们得到一个属性键 [object Object]
以及值是 {}
。
我们可以把括号地狱搞成这样:
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {} // 结构: // { // '[object Object]': { // '[object Object]': {} // } // }
这里阅读更多关于对象字面量:
- Object initializer at MDN
访问原型 __proto__
正如我们所知道的,原始数据(premitives)没有原型。但是,如果我们尝试为原始数据获取一个 __proto__
的值,我们会得到这样的一个结果:
(1).__proto__.__proto__.__proto__; // -> null
💡 说明:
这是因为原始数据的没有原型,它将使用 ToObject
方法包装在包装器对象中。所以,一步一步:
(1) .__proto__( // -> [Number: 0] 1 ) .__proto__.__proto__( // -> {} 1 ).__proto__.__proto__.__proto__; // -> null
以下是关于 __proto__
的更多信息:
`${{Object}}`
下面的表达式结果如何?
`${{ Object }}`;
答案是:
// -> '[object Object]'
💡 说明:
我们通过 简写属性表示 使用一个 Object
属性定义了一个对象:
{ Object: Object; }
然后我们将该对象传递给模板文字,因此 toString
方法调用该对象。这就是为什么我们得到字符串 '[object Object]'
。
使用默认值解构
考虑这个例子:
let x, { x: y = 1 } = { x }; y;
上面的例子是面试中的一个很好的任务。y
有什么值? 答案是:
// -> 1
💡 说明:
let x, { x: y = 1 } = { x }; y; // ↑ ↑ ↑ ↑ // 1 3 2 4
以上示例:
- 我们声明
x
没有赋值,所以它是 'undefined`。 - 然后我们将
x
的值打包到对象属性x
中。 - 然后我们使用解构来提取
x
的值,并且要将这个值赋给y
。 如果未定义该值,那么我们将使用1
作为默认值。 - 返回
y
的值。
- Object initializer at MDN
点和扩展运算符
数组的扩展可以组成有趣的例子。考虑这个:
[...[..."..."]].length; // -> 3
💡 说明:
为什么是 3?当我们使用扩展运算符时,@@iterator
方法会被调用,而返回的迭代器用于获取要迭代的值。字符串的默认迭代器按字符展开字符串。展开之后,我们把这些字符打包成一个数组。然后再展开这个数组并再打包回数组。
一个 '...'
字符串包含 .
,所以结果数组的长度将 3
。
现在,一步一步的看:
[...'...'] // -> [ '.', '.', '.' ] [...[...'...']] // -> [ '.', '.', '.' ] [...[...'...']].length // -> 3
显然,我们可以展开和包装数组的元素任意多次,只要你想:
[...'...'] // -> [ '.', '.', '.' ] [...[...'...']] // -> [ '.', '.', '.' ] [...[...[...'...']]] // -> [ '.', '.', '.' ] [...[...[...[...'...']]]] // -> [ '.', '.', '.' ] // 以此类推 …
标签
很多程序员不知道 JavaScript 中的标签。它们很有去
foo: { console.log("first"); break foo; console.log("second"); } // > first // -> undefined
💡 说明:
带标签的语句与 break
或 continue
语句一起使用。您可以使用标签来标识循环,然后使用 break
或 continue
语句来指示程序是否应该中断循环或继续执行它。
在上面的例子中,我们识别一个标签 foo
。然后 console.log('first');
执行,然后中断执行。
详细了解 JavaScript 中的标签:
- 13.13 标签语句
- 标签语句 at MDN
嵌套标签
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
💡 说明:
像以前的例子一样,请遵循以下链接:
- 12.16 逗号运算符(
,
) - 13.13 标签语句
- 标签语句 at MDN
阴险的 try..catch
这个表达式将返回什么?2
还是 3
?
(() => { try { return 2; } finally { return 3; } })();
答案是 3
。惊讶吗?
💡 说明:
这是多重继承吗?
看下面的例子:
new class F extends (String, Array) {}(); // -> F []
这是多重继承吗?不。
💡 说明:
有趣的部分是 extends
子句的值((String,Array)
)。分组运算符总是返回其最后一个参数,所以 (String,Array)
实际上只是 Array
。 这意味着我们刚刚创建了一个扩展 Array
的类。
##
考虑一下这个 yield 自身的生成器例子:
(function* f() { yield f; })().next(); // -> { value: [GeneratorFunction: f], done: false }
如您所见,返回的值是一个值等于 f
的对象。那样的话,我们可以做这样的事情:
(function* f() { yield f; })() .next() .value() .next()( // -> { value: [GeneratorFunction: f], done: false } // 再一次 function* f() { yield f; } )() .next() .value() .next() .value() .next()( // -> { value: [GeneratorFunction: f], done: false } // 再一次 function* f() { yield f; } )() .next() .value() .next() .value() .next() .value() .next(); // -> { value: [GeneratorFunction: f], done: false } // 以此类推 // …
💡 说明:
要理解为什么这样工作,请阅读规范的这些部分:
一个类的类
考虑这个混淆语法:
typeof new class { class() {} }(); // -> 'object'
似乎我们在类内部声明了一个类。应该是个错误,然而,我们得到一个 'object'
字符串。
💡 说明:
ECMAScript 5 时代以来,关键字允许访问属性。所以请考虑一下这个简单的对象示例:
const foo = { class: function() {} };
还有 ES6 标准速记方法定义。此外,类可能是匿名的。因此,如果我们放弃 : function
部分,我们将得到:
class { class() {} }
默认类的结果总是一个简单的对象。其类型应返回 'object'
。
在这里阅读更多
非强制对象
有着名的符号,有一种方法可以摆脱类型的强制。看一看:
function nonCoercible(val) { if (val == null) { throw TypeError("nonCoercible should not be called with null or undefined"); } const res = Object(val); res[Symbol.toPrimitive] = () => { throw TypeError("Trying to coerce non-coercible object"); }; return res; }
现在我们可以这样使用:
// objects const foo = nonCoercible({ foo: "foo" }); foo * 10; // -> TypeError: Trying to coerce non-coercible object foo + "evil"; // -> TypeError: Trying to coerce non-coercible object // strings const bar = nonCoercible("bar"); bar + "1"; // -> TypeError: Trying to coerce non-coercible object bar.toString() + 1; // -> bar1 bar === "bar"; // -> false bar.toString() === "bar"; // -> true bar == "bar"; // -> TypeError: Trying to coerce non-coercible object // numbers const baz = nonCoercible(1); baz == 1; // -> TypeError: Trying to coerce non-coercible object baz === 1; // -> false baz.valueOf() === 1; // -> true
💡 说明:
棘手的箭头功能
考虑下面的例子:
let f = () => 10; f(); // -> 10
好吧,但是这是怎么说的呢?
let f = () => {}; f(); // -> undefined
💡 说明:
你可能期待 {}
而不是 undefined
。这是因为花括号是箭头函数语法的一部分,所以 f
会返回未定义的。然而要从箭头函数直接返回 {}
对象也是可能的,要通过用括号把返回值括起来。
箭头函数不能作为构造器
考虑下面的例子:
let f = function() { this.a = 1; }; new f(); // -> { 'a': 1 }
现在,试着用箭头函数做同样的事情:
let f = () => { this.a = 1; }; new f(); // -> TypeError: f is not a constructor
💡 说明:
箭头函数不能作为构造器并且会在被 new 时抛出错误。因为它有一个词域的 this
,而且也没有 prototype
属性,所以这样做没什么意义。
arguments
和箭头函数
考虑下面的例子:
let f = function() { return arguments; }; f("a"); // -> { '0': 'a' }
现在,试着用箭头函数做同样的事情:
let f = () => arguments; f("a"); // -> Uncaught ReferenceError: arguments is not defined
💡 说明:
箭头函数是注重短小和词域下的 this
的常规函数的轻量级版本。同时箭头函数不提供 arguments
对象的绑定。作为一个有效的替代选择使用 rest parameters
来得到同样的结果:
let f = (...args) => args; f("a");
- Arrow functions at MDN.
棘手的返回
return
语句是很棘手的. 看下面的代码:
(function() { return { b: 10; } })(); // -> undefined
💡 说明:
return
和返回的表达式必须在同一行:
(function() { return { b: 10 }; })(); // -> { b: 10 }
这是因为一个叫自动插入分号的概念,它会在大部分换行处插入分号。第一个例子里,有一个分号被插入到 return
语句和对象字面量中间。所以函数返回 undefined
而对象字面量不会被求值。
对象的链式赋值
var foo = { n: 1 }; var bar = foo; foo.x = foo = { n: 2 }; foo.x; // -> undefined foo; // -> {n: 2} bar; // -> {n: 1, x: {n: 2}}
从右到左,{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}
对象。
它等价于:
var foo = { n: 1 }; var bar = foo; foo = { n: 2 }; // -> {n: 2} bar.x = foo; // -> {n: 1, x: {n: 2}} // bar.x 指向新的 foo 对象的地址 // 这不等价于:bar.x = {n: 2}
使用数组访问对象属性
var obj = { property: 1 }; var array = ["property"]; obj[array]; // -> 1
那关于伪多维数组创建对象呢?
var map = {}; var x = 1; var y = 2; var z = 3; map[[x, y, z]] = true; map[[x + 10, y, z]] = true; map["1,2,3"]; // -> true map["11,2,3"]; // -> true
💡 说明:
括号操作符将传递给字符串的表达式转换为字符串。将一个元素数组转换为字符串,就像将元素转换为字符串:
["property"].toString(); // -> 'property'`
Null 和关系运算符
null > 0; // false null == 0; // false null >= 0; // true
💡 说明:
长话短说,如果 null
小于 0
是 false
,那么 null >= 0
则是 true
。
请阅读这里的详细解释。
Number.toFixed()
显示不同的数字
Number.toFixed()
在不同的浏览器中会表现得有点奇怪。看看这个例子:
(0.7875).toFixed(3); // Firefox: -> 0.787 // Chrome: -> 0.787 // IE11: -> 0.788 (0.7876).toFixed(3); // Firefox: -> 0.788 // Chrome: -> 0.788 // IE11: -> 0.788
💡 说明:
尽管你的第一直觉可能是 IE11 是正确的而 Firefox/Chrome 错了,事实是 Firefox/Chrome 更直接地遵循数字运算的标准(IEEE-754 Floating Point),而 IE11 经常违反它们(可能)去努力得出更清晰的结果。
你可以通过一些快速的测试来了解为什么它们发生:
// 确认 5 向下取证的奇怪结果 (0.7875).toFixed(3); // -> 0.787 // 当你展开到 64 位(双精度)浮点数准确度限制时看起来就是一个 5 (0.7875).toFixed(14); // -> 0.78750000000000 // 但如果你超越这个限制呢? (0.7875).toFixed(20); // -> 0.78749999999999997780
浮点数在计算机内部不是以一系列十进制数字的形式存储的,而是通过一个可以产生一点点通常会被 toString 或者其他调用取整的不准确性的更复杂的方法,但它实际上在内部会被表示。
在这里,那个结尾的 "5" 实际上是一个极其小的略小于 5 的分数。将其以任何常理的长度取整它都会被看作一个 5,但它在内部通常不是 5。
IE11,尽管如此,描述这个数字时只是加上一些 0,甚至在 toFixed(20) 的时候也是这样,因为它看起来强制取整了值来减少硬件限制带来的问题。
详见 ECMA-262 中 NOTE 2
的 toFixed
的定义。
Math.max()
小于 Math.min()
Math.min(1, 4, 7, 2); // -> 1 Math.max(1, 4, 7, 2); // -> 7 Math.min(); // -> Infinity Math.max(); // -> -Infinity Math.min() > Math.max(); // -> true
💡 说明:
- Why is Math.max() less than Math.min()? by Charlie Harvey
比较 null
和 0
下面的表达式似乎有点矛盾:
null == 0; // -> false null > 0; // -> false null >= 0; // -> true
null
怎么既不等于也不大于 0
,如果null >= 0
实际上是 true
?(这也适用于少于同样的方法。)
💡 说明:
执行这三个表达式的方式各不相同,并负责产生这种意外行为。
首先,抽象相等比较 null == 0
。通常情况下,如果这个运算符不能正确地比较两边的值,则它将两个数字转换为数字,并对数字进行比较。然后,您可能会期望以下行为:
// 事实并非如此 (null == 0 + null) == +0; 0 == 0; true;
然而,根据对规范的仔细阅读,数字转换实际上并没有发生在 null
或 undefined
的一侧。因此,如果在等号的一侧有 null
,则另一侧的表达式必须为 null
或 undefined
,以返回 true
。既然不是这样,就会返回 false
。
接下来,关系比较 null > 0
。这里的算法不同于抽象的相等运算符,将 null
转换为一个数字。因此,我们得到这样的行为:
null > 0 +null = +0 0 > 0 false
最后,关系比较 null >= 0
。你可以认为这个表达式应该是 null > 0 || null == 0
的结果;如果是这样,那么以上的结果将意味着这也是false
。然而,>=
操作符实际上以一种非常不同的方式工作,这基本上与 <
操作符相反。因为我们的例子中,大于运算符的例子也适用于小于运算符,也就是说这个表达式的值是这样的:
null >= 0; !(null < 0); !(+null < +0); !(0 < 0); !false; true;
相同变量重复声明
JS 允许重复声明变量:
a; a; // 这也是有效的 a, a;
严格模式也可以运行:
var a, a, a; var a; var a;
💡 解释:
所有的定义都被合并成一条定义。
Array.prototype.sort() 的默认行为
想象你需要排序数组中的数字。
[ 10, 1, 3 ].sort() // -> [ 1, 10, 3 ]
💡 说明:
默认排序基于将给定元素转换为字符串,然后比较它们的 UTF-16 序列中的值。
提示
传入一个 compareFn
比较函数如果你想对字符串以外的内容排序。
[ 10, 1, 3 ].sort((a, b) => a - b) // -> [ 1, 3, 10 ]
resolve() 不会返回 Promise 实例
const theObject = { "a": 7, }; const thePromise = new Promise((resolve, reject) => { resolve(theObject); }); // -> Promise 实例对象 thePromise.then(value => { console.log(value === theObject); // -> true console.log(value); // -> { a: 7 } })
从thePromise
接收到的value
值完全就是theObject
。
那么,如果向resolve
传入另外一个Promise
会怎样?
const theObject = new Promise((resolve, reject) => { resolve(7); }); // -> Promise 实例对象 const thePromise = new Promise((resolve, reject) => { resolve(theObject); }); // -> Promise 实例对象 thePromise.then(value => { console.log(value === theObject); // -> false console.log(value); // -> 7 })
💡 说明:
此函数将类promise对象的多层嵌套展平。
官方规范是 ECMAScript 25.6.1.3.2 Promise Resolve Functions,由于是机械思维,所以难以读懂。
其他资源
- wtfjs.com — 这是一组非常特别的不规范,不一致的地方,以及那些对于网络语言来说非常痛苦的不直观的时刻。
- Wat — A lightning talk by Gary Bernhardt from CodeMash 2012
- What the... JavaScript? — 凯尔。辛普森一家谈到了前两次试图从 JavaScript 中“拉出疯狂”的尝试。他希望帮助您生成更干净、更优雅、更可读的代码,然后鼓励人们为开源社区做出贡献。