What the f*ck JavaScript?

Souleigh ✨ 等级 121 0 0

What the f*ck JavaScript?

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

image

JavaScript 是一种很好的语言。它有一个简单的语法,庞大的生态系统,以及最重要,最伟大的社区。

同时,我们都知道,JavaScript 是一个非常有趣又充满戏法的语言。他们中的有些可以迅速将我们的日常工作变成地狱,有些可以让我们大声笑起来。

WTFJS 的原创思想属于 Brian Leroux。这个列表受到他的讲话的高度启发 “WTFJS” at dotJS 2012

npm 手稿

你可以通过 npm 来安装。只要运行:

$ npm install -g wtfjs

你应该能够在命令行中运行wtfjs,这将打开手册并在你选择的$PAGER中,否则你也可以选择在这里阅读。

💪🏻 动机

只是为了好玩

“只是为了好玩:一个意外革命的故事”, 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

💡 说明:

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

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

7.2.14 严格模式相等比较

遵循 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

尽管 nullfalse,但它不等于 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_VALUE5e-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 规范描述了这些行为。

undefinedNumber

如果我们不把任何参数传递到 Number 构造函数中,我们将得到 0undefined 是一个赋值形参,没有实际的参数,所以您可能期望 NaNundefined 作为参数的值。然而,当我们通过 undefined ,我们将得到 NaN

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

💡 说明:

根据规范:

  1. 如果没有参数传递给这个函数,让 n+0 ;
  2. 否则,让 n 调用 ToNumber(value)
  3. 如果值为 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 返回 11/1999999 被转换为 5.00000250000125e-7parseInt 返回 5

truefalse 数学运算

我们做一些数学计算:

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

嗯… 🤔

💡 说明:

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

Number(true); // -> 1

一元加运算符尝试将其值转换成数字。 它可以转换整数和浮点的字符串表示,以及非字符串值 truefalsenull 。 如果它不能解析特定的值,它将转化为 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

💡 说明:

typeofinstanceof 运算符的工作原理:

[]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 二进制浮点运算标准引起的。阅读更多:

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

扩展数字的方法

您可以添加自己的方法来包装对象,如 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  -> addition
Boolean + Number  -> addition
Boolean + Boolean -> addition
Number  + String  -> concatenation
String  + Boolean -> concatenation
String  + String  -> concatenation

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

正则表达式的加法

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

// 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方法。 阅读更多:

一个 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]': {}
//   }
// }

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

访问原型 __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 自身的生成器例子:

(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");

棘手的返回

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

💡 说明:

括号操作符将传递给字符串的表达式转换为字符串。将一个元素数组转换为字符串,就像将元素转换为字符串:

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

Null 和关系运算符

null > 0; // false
null == 0; // false

null >= 0; // true

💡 说明:

长话短说,如果 null 小于 0false,那么 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 2toFixed 的定义。

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

💡 说明:

比较 null0

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

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

null 怎么既不等于也不大于 0,如果null >= 0 实际上是 true?(这也适用于少于同样的方法。)

💡 说明:

执行这三个表达式的方式各不相同,并负责产生这种意外行为。 首先,抽象相等比较 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() on MDN

官方规范是 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 中“拉出疯狂”的尝试。他希望帮助您生成更干净、更优雅、更可读的代码,然后鼓励人们为开源社区做出贡献。
预览图
收藏
评论区