Skip to content

Latest commit

 

History

History
2267 lines (1573 loc) · 71.8 KB

README-zh-cn.md

File metadata and controls

2267 lines (1573 loc) · 71.8 KB

What the f*ck JavaScript?

WTFPL 2.0 NPM version Patreon Buy Me A Coffee

一个有趣和棘手的 JavaScript 示例列表。

JavaScript 是一个不错的语言。它的语法简单,生态系统也很庞大,最重要的是,它拥有最伟大的社区力量。

我们知道,JavaScript 是一个非常有趣的语言,但同时也充满了各种奇怪的行为。这些奇怪的行为有时会搞砸我们的日常工作,有时则会让我们忍俊不禁。

WTFJS 的灵感源于 Brian Leroux。这个列表受到他 在 2012 年的 dotJS 上的演讲 “WTFJS” 的高度启发:

dotJS 2012 - Brian Leroux - WTFJS

适用于 NodeJS 的指南手册

你可以通过 npm 安装该项目的指南手册。只需运行:

$ npm install -g wtfjs

然后在命令行中运行 wtfjs,将会在命令行中打开手册并跳转至你选择的页数 $PAGER。这不是必需的步骤,你也可以继续在这里阅读。

源码在此处: https://github.com/denysdovhan/wtfjs

翻译

如今,wtfjs 已被翻译成多种语言:

点此添加新语言的翻译

注意: 翻译由该语言的译者维护,因此可能缺失部分例子,或存在过时的例子等。

Table of Contents

💪🏻 初衷

只是因为好玩

“只是为了好玩:一个意外革命的故事”, Linus Torvalds

这个列表的主要目的是收集一些疯狂的例子,并尽可能解释它们的原理。我很喜欢学习以前不了解的东西。

如果您是初学者,您可以根据此笔记深入了解 JavaScript。我希望它会激励你在阅读规范上投入更多时间和精力。

如果您是专业开发人员,您将从这些例子中看到人见人爱的 JavaScript 也充满了非预期的边界行为。

总之,古人云:三人行,必有我师焉。我相信这些例子总能让你学习到新的知识。

⚠️ Note: 如果这些例子帮助到你,请务必赞助收集了这些例子的作者.

✍🏻 符号

// -> 表示表达式的结果。例如:

1 + 1; // -> 2

// > 表示 console.log 等输出的结果。例如:

console.log("hello, world!"); // > hello, world!

// 则是用于解释的注释。例如:

// 将一个函数赋值给 foo 常量
const foo = function() {};

👀 例子

[] 等于 ![]

数组等于一个数组取反:

[] == ![]; // -> true

💡 说明:

抽象相等运算符会将其两端的表达式转换为数字值进行比较,尽管这个例子中,左右两端均被转换为 0,但原因各不相同。数组总是真值(truthy),因此右值的数组取反后总是为 false,然后在抽象相等比较中被被类型转换为 0。而左值则是另一种情形,空数组没有被转换为布尔值的话,尽管在逻辑上是真值(truthy),但在抽象相等比较中,会被类型转换为数字 0

该表达式的运算步骤如下:

+[] == +![];
0 == +false;
0 == 0;
true;

了解更多:[] 是真值,但并非 true.

true 不等于 ![],也不等于 []

数组不等于 true,但数组取反也不等于 true; 数组等于 false数组取反也等于 false

true == []; // -> false
true == ![]; // -> false

false == []; // -> true
false == ![]; // -> true

💡 说明:

true == []; // -> false
true == ![]; // -> false

// 根据规范

true == []; // -> false

toNumber(true); // -> 1
toNumber([]); // -> 0

1 == 0; // -> false

true == ![]; // -> false

![]; // -> false

true == false; // -> false
false == []; // -> true
false == ![]; // -> true

// 根据规范

false == []; // -> true

toNumber(false); // -> 0
toNumber([]); // -> 0

0 == 0; // -> true

false == ![]; // -> true

![]; // -> false

false == false; // -> true

true 是 false

!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true

💡 说明:

考虑以下步骤:

// true 是真值(truthy),并且隐式转换为数字1,而字符串 'true' 会被转换为 NaN。
true == "true"; // -> false
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

💡 说明:

规范严格定义了这种行为背后的逻辑:

  1. 如果 Type(x) 不同于 Type(y),返回 false
  2. 如果 Type(x) 数值, 然后
    1. 如果 xNaN,返回 false
    2. 如果 yNaN,返回 false
    3. ……

7.2.14 严格模式相等比较

根据 IEEE 对 NaN 的定义:

有四种可能的相互排斥的关系:小于、等于、大于和无序。当比较操作中至少一个操作数是 NaN 时,便是无序的关系。换句话说,NaN 对任何事物包括其本身比较都应当是无序关系。

— StackOverflow 上的 “为什么对于 IEEE754 NaN 值的所有比较返回 false?”

奇怪的 Object.is()===

Object.is() 用于判断两个值是否相同。和 === 操作符像作用类似,但它也有一些奇怪的行为:

Object.is(NaN, NaN); // -> true
NaN === NaN; // -> false

Object.is(-0, 0); // -> false
-0 === 0; // -> true

Object.is(NaN, 0 / 0); // -> true
NaN === 0 / 0; // -> false

💡 说明:

在 JavaScript “语言”中,NaNNaN 的值是相同的,但却不是严格相等。NaN === NaN 返回 false 是因为历史包袱,记住这个特例就行了。

基于同样的原因,-00 是严格相等的,但它们的值却不同。

关于 NaN === NaN 的更多细节,请参阅上一个例子。

它是 fail

你可能不会相信,但……

(![] + [])[+[]] +
  (![] + [])[+!+[]] +
  ([![]] + [][[]])[+!+[] + [+[]]] +
  (![] + [])[!+[] + !+[]];
// -> 'fail'

💡 说明:

将大量的符号分解成片段,我们注意到,以下表达式经常出现:

![] + []; // -> 'false'
![]; // -> false

所以我们尝试将 []false 加起来。但是因为一些内部函数调用(binary + Operator - >ToPrimitive - >[[DefaultValue] ]),我们最终将右边的操作数转换为一个字符串:

![] + [].toString(); // 'false'

将字符串作为数组,我们可以通过[0]来访问它的第一个字符:

"false"[0]; // -> 'f'

剩下的部分以此类推,不过此处的 i 字符是比较讨巧的。fail 中的 i 来自于生成的字符串 falseundefined,通过指定序号 ['10'] 取得的。

更多的例子:

+![]          // -> 0
+!![]         // -> 1
!![]          // -> true
![]           // -> false
[][[]]        // -> undefined
+!![] / +![]  // -> Infinity
[] + {}       // -> "[object Object]"
+{}           // -> NaN

[] 是真值,但不等于 true

数组是一个真值,但却不等于 true

!![]       // -> true
[] == true // -> false

💡 说明:

以下是 ECMA-262 规范中相应部分的链接:

null 是假值,但又不等于 false

尽管 null 是假值,但它不等于 false

!!null; // -> false
null == false; // -> false

但是,别的被当作假值的却等于 false,如 0''

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
typeof document.all; // -> 'undefined'

但是同时,document.all 不等于 undefined

document.all === undefined; // -> false
document.all == null; // -> true

不过:

document.all == null; // -> true

💡 说明:

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 > 0; // -> true

💡 说明:

Number.MIN_VALUE5e-324,即可以在浮点精度内表示的最小正数,也是在该精度内无限接近零的数字。它定义了浮点数的最高精度。

现在,整体最小的值是 Number.NEGATIVE_INFINITY,尽管这在严格意义上并不是真正的数字。

— StackOverflow 上的“为什么在 JavaScript 中 0 小于 Number.MIN_VALUE?”

函数不是函数

⚠️ V8 v5.5 或更低版本中出现的 Bug(Node.js <= 7) ⚠️

大家都知道 undefined 不是 function 对吧?但是你知道这个吗?

// 声明一个继承null的类
class Foo extends null {}
// -> [Function: Foo]

new Foo() instanceof null;
// > TypeError: function is not a function
// >     at … … …

💡 说明:

这不是规范的一部分。这只是一个缺陷,且已经修复了。所以将来不会有这个问题。

Super constructor null of Foo is not a constructor (Foo 的超类的构造函数 null 不是构造函数)

这是前述缺陷的后续行为,在现代环境中可以复现(在 Chrome 71 和 Node.js v11.8.0 测试成功)。

class Foo extends null {}
new Foo() instanceof null;
// > TypeError: Super constructor null of Foo is not a constructor

💡 说明:

这并不是缺陷,因为:

Object.getPrototypeOf(Foo.prototype); // -> null

若当前类没有构造函数,则在构造该类时会顺次调用其原型链上的构造函数,而本例中其父类没有构造函数。补充一下,null 也是一个 object

typeof null === "object";

因此,你可以继承 null(尽管在面向对象编程的世界里这是不允许的),但是却不能调用 null 的构造函数。若你把代码改成这样:

class Foo extends null {
  constructor() {
    console.log("something");
  }
}

将会报错:

ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
// 引用错误:在访问`this`或返回之前,你需要在子类中先调用super构造函数

但是当你加上 super 时:

class Foo extends null {
  constructor() {
    console.log(111);
    super();
  }
}

JS 抛出错误:

TypeError: Super constructor null of Foo is not a constructor
// 类型错误:Foo的超类的构造函数null不是构造函数

数组相加

如果你尝试将两个数组相加:

[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'

💡 说明:

数组之间会发生串联。步骤如下:

[1, 2, 3] +
  [4, 5, 6][
    // 调用 toString()
    (1, 2, 3)
  ].toString() +
  [4, 5, 6].toString();
// 串联
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");

数组中的尾逗号

假设你想要创建了一个包含 4 个空元素的数组。如下所示,最终只能得到一个包含三个元素的数组,原因在于尾逗号:

let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'

💡 说明:

尾逗号 (trailing commas,有时也称为“最后逗号”(final commas)) 在向 JavaScript 代码中添加新元素、参数或属性时非常有用。如果您想添加一个新属性,若前一行已经有尾逗号,你无需修改前一行,只要添加一个新行并加上尾逗号即可。这使得版本控制历史较为干净,编辑代码也很简单。

— MDN 上的 尾逗号

数组的相等性是深水猛兽

数组之间进行相等比较是 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 抽象相等比较 一节描述了这些行为。

undefinedNumber

无参数调用 Number 构造函数会返回 0。我们知道,当函数没有接受到指定位置的实际参数时,该处的形式参数的值会是 undefined。因此,你可能觉得当我们传入 undefined 时应当同样返回 0。然而实际上传入 undefined 返回的是 NaN

Number(); // -> 0
Number(undefined); // -> NaN

💡 说明:

根据规范:

  1. 若无参数调用该函数,n 将为 +0
  2. 否则,n 将为?ToNumber(value)
  3. 如果值为 undefinedToNumber(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 时,第 14 个字母“n”也可以作数字用。当基数为 31 时,第 21 个字母“u”进入数字的行列,此时整个字符串都可以解析了。而当基数增加到 37 以上,已经超出了数字和字母所能表达的数字范围,因此一律返回 NaN

— StackOverflow 上的 “parseInt(null, 24) === 23 什么鬼”

不要忘记八进制:

parseInt("06"); // 6
parseInt("08"); // 8 如果支持 ECMAScript 5
parseInt("08"); // 0 如果不支持 ECMAScript 5

💡 说明: 当输入的字符串以“0”开始时,根据实现的不同,会被解释为八进制或十进制。ECMAScript 5 明确表示应当使用十进制,但有部分浏览器仍不支持。因此推荐在调用 parseInt 函数时总是传入表示基数的第二个参数。

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 返回 11/1999999 被转换为 5.00000250000125e-7,所以 parseInt 返回 5

truefalse 的数学运算

做一下数学计算:

true + true; // -> 2
(true + true) * (true + true) - true; // -> 3

嗯……🤔

💡 说明:

我们可以用 Number 构造函数将值强制转化成数值。很明显,true 将被强制转换为 1

Number(true); // -> 1

一元加运算符会尝试将其值转换成数字。它可以转换字符串形式表达的整数和浮点数,以及非字符串值 truefalsenull。如果它不能解析特定的值,它将转化为 NaN。这意味着我们可以有更简便的方式将 true 转换成 1

+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 NaN; // -> 'number'

💡 说明:

typeofinstanceof 运算符的工作原理:

[]null 是对象

typeof []; // -> 'object'
typeof null; // -> 'object'

// 然而
null instanceof Object; // false

💡 说明:

typeof 运算符的行为在本节的规范中定义:

根据规范,typeof 操作符返回一个字符串,且必须符合 Table 37: typeof 操作符 返回值。对于没有实现 [[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

10000000000000000; // -> 10000000000000000
10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002

💡 说明:

这是由 IEEE 754-2008 二进制浮点运算标准引起的。极大的数字会被四舍五入到最近的偶数。阅读更多:

0.1 + 0.2 精度计算

来自 JavaScript 的知名笑话。0.10.2 相加是存在精度错误的

0.1 + 0.2; // -> 0.30000000000000004
0.1 + 0.2 === 0.3; // -> false

💡 说明:

来自于 StackOverflow 上的问题“浮点计算坏了?”的答案:

程序中的常量 0.20.3 是最接近真实值的近似值。最接近 0.2double 大于有理数 0.2,但最接近 0.3double 小于有理数 0.30.10.2 的和大于有理数 0.3,因此在程序中进行常量比较会得到假。

这个问题太过于出名,甚至有一个网站叫 0.30000000000000004.com。这不仅仅是 JavaScript 特有的问题,在其他采用浮点计算的语言中也广泛存在。

扩展数字的方法

你可以向包装对象添加自己的方法,比如 NumberString

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  -> 加法
Boolean + Number  -> 加法
Boolean + Boolean -> 加法
Number  + String  -> 串联字符串
String  + Boolean -> 串联字符串
String  + String  -> 串联字符串

那其他例子呢?在相加之前,[]{} 隐式调用 ToPrimitiveToString 方法。详细了解规范中的求值过程:

不过需要注意此处的 {} + [],这是一个例外。你可以发现它的求值结果与 [] + {} 不同,这是因为当我们不加括号时,它被当作是一个空的代码块和一个一元加法运算符,这个运算符会把其后的 [] 转换为数字。具体如下:

{
  // 代码块
}
+[]; // -> 0

当我们加上括号,情况就不一样了:

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

正则表达式的加法

你知道可以做这样的运算吗?

// 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背后的秘密

规范的链接:

到底 call 了谁

@cramforce 发现

console.log.call.call.call.call.call.apply(a => a, [1, 2]);

💡 说明:

注意,这可能会击碎你的三观!尝试在您的头脑中重现此代码:我们使用 apply 方法调用 call 方法。阅读更多:

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 返回一个创建示例对象的 Object 构造函数引用。当当前对象是字符串时,它是 String;当当前对象是数字时,它是 Number;以此类推。

将对象做为另一个对象的 key

{ [{}]: {} } // -> { '[object Object]': {} }

💡 说明:

为何可以正常运行?这里我们使用的是 计算属性。当你将对象用方括号括起来当作对象的属性名时,它会将对象强制转换成一个字符串,所以我们得到属性键是 [object Object],其值为 {}

体验一下简单的“括号地狱”:

({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}

// 结构:
// {
//   '[object Object]': {
//     '[object Object]': {}
//   }
// }

关于对象字面量,点击这里阅读更多:

访问原型 __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

以上示例:

  1. 我们声明了 x,但没有立刻赋值,所以它是 undefined
  2. 我们将 x 的值打包到对象属性 x 中。
  3. 我们使用解构来提取 x 的值,并且要将这个值赋给 y。如果未定义该值,那么我们将使用 1 作为默认值。
  4. 返回 y 的值。

点和扩展运算符

数组的扩展可以组成有趣的例子。考虑这个:

[...[..."..."]].length; // -> 3

💡 说明:

为什么是 3?当我们使用扩展运算符时,@@iterator 方法会被调用,而返回的迭代器用于获取要迭代的值。字符串的默认迭代器按字符展开字符串。展开之后,我们把这些字符打包成一个数组。然后再展开这个数组并再打包回数组。

一个 '...' 字符串包含 . ,所以结果数组的长度将 3

现在,一步一步的看:

[...'...']             // -> [ '.', '.', '.' ]
[...[...'...']]        // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3

显然,我们可以展开和包装数组的元素任意多次,只要你想:

[...'...']                 // -> [ '.', '.', '.' ]
[...[...'...']]            // -> [ '.', '.', '.' ]
[...[...[...'...']]]       // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]]  // -> [ '.', '.', '.' ]
// 以此类推 …

标签

很多程序员不知道 JavaScript 中也有标签,并且很有趣:

foo: {
  console.log("first");
  break foo;
  console.log("second");
}

// > first
// -> undefined

💡 说明:

带标签的语句与 breakcontinue 语句一起使用。您可以使用标签来标识循环,然后使用 breakcontinue 语句来指示程序是否应该中断循环或继续执行它。

在上面的例子中,我们识别一个标签 foo。然后 console.log('first'); 执行,然后中断执行。

详细了解 JavaScript 中的标签:

嵌套标签

a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5

💡 说明:

和上面的例子类似,请遵循以下链接:

阴险的 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 返回自身的生成器

考虑这个 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;
}

现在我们可以这样使用:

// 对象
const foo = nonCoercible({ foo: "foo" });

foo * 10; // -> TypeError: Trying to coerce non-coercible object
foo + "evil"; // -> TypeError: Trying to coerce non-coercible object

// 字符串
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

// 数字
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 会返回 undefined。不过要从箭头函数明确返回 {} 对象也是有可能的,这时你需要用括号把返回值括起来。

let f = () => ({});
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");

棘手的返回

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}},毕竟 barfoo 的一个引用。但为什么 foo.xundefinedbar.x 不是呢?

💡 说明:

foobar 引用同一个对象 {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

💡 说明:

[] 操作符会使用 toString 将传递的表达式转换为字符串。将单元素数组转换为字符串,相当于将这个元素转换为字符串:

["property"].toString(); // -> 'property'`

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 2toFixed 的定义。

min 大于 max

我发现一个神奇的例子:

Math.min() > Math.max(); // -> true
Math.min() < Math.max(); // -> false

💡 说明:

这是一个简单的例子。我们一步一步来:

Math.min(); // -> Infinity
Math.max(); // -> -Infinity
Infinity > -Infinity; // -> true

为什么是这样呢?其实 Math.max() 并不会返回最大的正数,即 Number.MAX_VALUE

Math.max 接受两个参数,将它们转换到数字,比较之后返回最大的那个。若没有传入参数,结果将是 -∞。若参数中存在 NaN,则返回 NaN

反过来,当 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

💡 说明:

比较 null0

下面的表达式似乎有点矛盾:

null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true

既然 null >= 0 返回 true,为什么 null 既不等于也不大于 0?(对于小于比较也可以得出相似的结果。)

💡 说明:

这三个表达式的求值方式各不相同,因此产生了非预期的结果。 首先,对于 null == 0 这个抽象相等比较操作,通常当该运算符不能正确地比较两边的值,则它会将两边的值都转换为数字,再对数字进行比较。那么,您可能会期望以下行为:

// 事实并非如此
(null == 0 + null) == +0;
0 == 0;
true;

然而,仔细阅读规范就会发现,数字转换实际上并没有发生在 nullundefined 的一侧。也就是说,如果在等号的一侧有 null,则当另一侧的表达式为 nullundefined就返回 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 对象的多层嵌套平铺到单层嵌套。(例如上述的 promise 函数 resolve 了另一个会 resolve 出其他对象的 promise 函数)

MDN 上的 Promise.resolve()

官方规范是 ECMAScript 25.6.1.3.2 Promise 的 Resolve 函数,但是这一章节对人类非常不友好。

{}{} 是 undefined

你可以在终端测试一下。类似这样的结构会返回最后定义的对象中的值。

{}{}; // -> undefined
{}{}{}; // -> undefined
{}{}{}{}; // -> undefined
{foo: 'bar'}{}; // -> 'bar'
{}{foo: 'bar'}; // -> 'bar'
{}{foo: 'bar'}{}; // -> 'bar'
{a: 'b'}{c:' d'}{}; // -> 'd'
{a: 'b', c: 'd'}{}; // > SyntaxError: Unexpected token ':'
({}{}); // > SyntaxError: Unexpected token '{'

💡 说明:

解析到 {} 会返回 undefined,而解析 {foo: 'bar'}{}时,表达式 {foo: 'bar'} 返回 'bar'

{} 有两重含义:表示对象,或表示代码块。例如,在 () => {} 中的 {} 表示代码块。所以我们必须加上括号:() => ({}) 才能让它正确地返回一个对象。

因此,我们现在将 {foo: 'bar'} 当作代码块使用,则可以在终端中这样写:

if (true) {
  foo: "bar";
} // -> 'bar'

啊哈,一样的结果!所以 {foo: 'bar'}{} 中的花括号就是表示代码块。

arguments 绑定

考虑以下函数:

function a(x) {
  arguments[0] = "hello";
  console.log(x);
}

a(); // > undefined
a(1); // > "hello"

💡 说明

arguments 是一个类数组对象,包含了所有传入当前函数的参数。当没有传入参数时,该对象中就不存在 x 属性,也就无法覆盖。

来自地狱的 alert

如题,从地狱而来的代码:

[666]["\155\141\160"]["\143\157\156\163\164\162\165\143\164\157\162"](
  "\141\154\145\162\164(666)"
)(666); // alert(666)

💡 说明

这一串代码是基于多个采用了八进制转义序列的字符串构造的。

任何码值小于 256 的字符(又称扩展 ASCII 码表域)都可以用 \ 加上其八进制代码的转义方式写出来。上面这个简单的例子就是将 alert 编码到八进制转义序列。

没有尽头的计时

如果我们对 setTimeout 赋予无限大会如何?

setTimeout(() => console.log("called"), Infinity); // -> <timeoutId>
// > 'called'

结果是,它会立即运行,并没有等待无限长的时间。

💡 说明:

通常运行时内部会将延时存储为一个 32 位的有符号整数,而上述代码会导致运行时在解析延时参数时发生整数溢出,从而使函数立即执行而不等待。

例如,在 Node.js 中我们可以看到这样的警告信息:

(node:1731) TimeoutOverflowWarning: Infinity does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
(Use `node --trace-warnings ...` to show where the warning was created)

setTimeout 对象

如果我们给 setTimeout 的回调函数参数传非函数值会发生什么?

setTimeout(123, 100); // -> <timeoutId>
// > 'called'

没问题。

setTimeout('{a: 1}', 100); // -> <timeoutId>
// > 'called'

这个也没问题。

setTimeout({a: 1}, 100); // -> <timeoutId>
// > 'Uncaught SyntaxError: Unexpected identifier               setTimeout (async) (anonymous) @ VM__:1'
// 未捕获的语法错误:非预期的标识符

抛出了一个 SyntaxError(语法错误)。

这种错误很容易发生,尤其是当你有个函数返回一个对象,但是你忘了将其传进函数,直接就在这里调用了!不过,如果 content-policy 设置为 self 会怎么样呢?

setTimeout(123, 100); // -> <timeoutId>
// > console.error("[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'report-sample' 'self' ")
// [仅报告] 拒绝将字符串当作JavaScript求值,因为内容安全策略(CSP,Content Security Policy)指令被设置为 "script-src 'report-sample' 'self'",在该指令模式下不允许 'unsafe-eval' 的脚本源。

终端会拒绝执行!

💡 说明:

WindowOrWorkerGlobalScope.setTimeout() 的第一个参数可以是代码(code),代码会被传递到 eval 函数,这是不好的。eval 会把所有输入强制转换为字符串,然后进行求值,那么对象会变成 '[object Object]';嗯,你也看到了,这里确实有一个非法标识符 'Unexpected identifier'

点点运算符

现在尝试把一个数字转换到字符串:

27.toString() // > Uncaught SyntaxError: Invalid or unexpected token
// 未捕获的语法错误:非法或非预期的词元(token)

如果我们再加上一个点呢?

27..toString(); // -> '27'

那为什么第一个例子错了呢?

💡 说明:

这是文法的限制。

. 运算符存在歧义,它既可以当属性访问符,也可以是小数点,这取决于它在代码中的位置。

规范中定义了 . 运算符仅在特定的位置使用时会被当作小数点,这个定义写在 ECMAScript 的数字字面量语法一节中。

所以,当你想要在数字后加属性访问器的点号时,应当加上括号,或再加上一个点,以使该表达式合法。

(27).toString(); // -> '27'
// or
27..toString(); // -> '27'

再 new 一次

这仅仅是一个用于娱乐的例子。

class Foo extends Function {
  constructor(val) {
    super();
    this.prototype.val = val;
  }
}

new new Foo(":D")().val; // -> ':D'

💡 说明:

JavaScript 与其他面向对象语言不同,它的构造函数仅是一个比较特殊的函数。虽然 class 语法糖让你可以创建一个字面上的类,但实例化后它就变成了函数,因此它可以再次实例化。

虽然我没有测试过,但我觉得最后的那个表达式应该是这样分析的:

new new Foo(":D")().val(new newFooInstance()).val;
veryNewFooInstance.val;
// -> ':D'

再补充一下,运行 new Function('return "bar";') 必然会创建一个内容为 return "bar"; 的函数对象。而Foo类的构造函数中的 super() 调用的是 Function 的构造函数,所以自然而然我们可以在它上面添加更多的操作。

class Foo extends Function {
  constructor(val) {
    super(`
      this.val = arguments[0];
    `);
    this.prototype.val = val;
  }
}

var foo = new new Foo(":D")("D:");
foo.val; // -> 'D:'
delete foo.val; // 移除这个实例的“val”属性,让它退回(defer back)到他的原型的“val”属性
foo.val; // -> ':D'

你应该用上分号

下面这个应该是标准的 JavaScript……吧?不,它炸了!

class SomeClass {
  ["array"] = []
  ["string"] = "str"
}

new SomeClass().array; // -> 'str'

woc……?

💡 说明:

嗯,你没猜错,这又是自动分号插入的功劳。

上面这个例子实际上会被转换为:

class SomeClass {
  ["array"] = ([]["string"] = "str");
}

看到了吧,str 这个字符串被赋值到属性 array 上。

用空格分割(split)字符串

你试过用空格分割字符串吗?

"".split(""); // -> []
// 但是……
"".split(" "); // -> [""]

💡 说明:

这是预期行为。它会在输入的字符串中遍历,一旦发现分隔符,就在此处分割。但若你传入的是空字符串,它找不到分隔符,因此返回该字符串。

规范引用如下:

它会从左向右搜索字符串,并根据 separator(分隔符)决定子字符串的分割位置;分割位置的字符仅用于分割,不会包含在返回的数组中。

对字符串 stringify

这会导致一个缺陷,我曾经修了好几天:

JSON.stringify("production") === "production"; // -> false

💡 说明:

先看看 JSON.stringify 的返回值:

JSON.stringify("production"); // -> '"production"'

原来是被“字串化”了,所以这也难怪:

'"production"' === "production"; // -> false

对数字和 true 的非严格相等比较

1 == true; // -> true
// 但是……
Boolean(1.1); // -> true
1.1 == true; // -> false

💡 说明:

根据规范:

比较 x == y 时,当 x 和 y 都有值,会返回 true 或 false。比较过程如下所述:

  1. Type(x) 是数字且 Type(y) 是字符串,则会返回 x == ! ToNumber(y) 的结果。

所以比较过程是这样的:

1 == true;
1 == Number(true);
1 == 1; // -> true
// 但是……
1.1 == true;
1.1 == Number(true);
1.1 == 1; // -> false

其他资源

  • wtfjs.com — 一些非常特别的不规范与不一致的集合,以及对于 web 编程语言来说非常痛苦的时光。
  • Wat — CodeMash 2012 中 Gary Bernhardt 的演讲
  • What the... JavaScript? — Kyle Simpsons 在 Forward 2 的演讲,描述了“疯狂的 JavaScript”。他希望帮助你写出更干净、更优雅、更易读的代码,鼓励人们为开源社区做出贡献。
  • Zeros in JavaScript — 针对 JavaScript 中的 =====+* 的真值表。

🤝 捐赠支持

你好!这个项目是我在空闲时间做的,作为我的主要工作的补充。我希望你在阅读这篇文章时保持愉快的心情。请考虑支持我 🙏。

每一次捐赠对我来说意义重大。你的捐赠是对我的工作的肯定:我的工作有价值。

🙏 感谢您的支持! 🙏

服务 链接 动作
Patreon Become a patron
BuyMeACoffee Buy me a cup of ☕️ or 🥤
Bitcoin 1EJsKs6rPsqa7QLoVLpe3wgcdL9Q8WmDxE
Ethereum 0x6aF39C917359897ae6969Ad682C14110afe1a0a1

⚠️ 提示: 我现居乌克兰,乌克兰的银行账户没办法绑定 PayPal 或 Stripe 之类的账户。所以我没法开启 Github Sponsors、OpenCollective 和其他依赖于这些服务的捐赠渠道。对不起,目前您只能通过这些方式支持我。

🎓 许可证

CC 4.0

© Denys Dovhan