What the f*ck JavaScript?

Souleigh ✨ 等级 359 0 0

What the f*ck JavaScript?

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

What the f*ck JavaScript?

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

相关推荐

JS排序算法
引子 有句话怎么说来着: 雷锋推倒雷峰塔,Java implements JavaScript. 当年,想凭借抱Java大腿火一把而不惜把自己名字给改了的JavaScript(原名LiveScript),如今早已光芒万丈。node JS的出现更是让JavaScript可以前后端通吃。虽然Java依然制霸企业级软件开发领域(C/C 的大神们不要打
What the f*ck JavaScript?
What the fck JavaScript? 一个有趣和棘手的 JavaScript 示例列表。 JavaScript 是一种很好的语言。
JavaScript中的类型
JavaScript中的类型 一、关于类型 什么叫做类型?简单地说,类型就是把内存中的一个二进制序列赋予某种意义。比如,二进制序列0100 0000 0111 0000 0001 0101 0100 1011 1100 0110 1010 0111 1110 1111 1001 1110如果看作是64位无符号整数类型就是4
JavaScript基础加ES6语法
JavaScript 一、什么是JavaScript 当下最流行的脚本语言,在世界上的所有浏览器中都有js的身影,是一门脚本语言,可以用于我们与web站点和web应用程序的交互,还可以用于后台服务器的编写,例如node.js 二、语法特点 基于对象和事件驱动的松散型,解释型语言 单线程异步 三、JavaScript作用 页面的交
2020 Javascript 明星项目
{{{width200}}} 译者:@Angelo Zuo 原文(https://risingstars.js.org/2020/zh) 欢迎来到
对 JavaScript 中事件循环的理解​
一、是什么 JavaScript 在设计之初便是单线程,即指程序运行时,只有一个线程存在,同一时间只能做一件事 为什么要这么设计,跟JavaScript的应用场景有关 JavaScript 初期作为一门浏览器脚本语言,通常用于操作 DOM ,如果是多线程,一个线程进行了删除 DOM ,另一个添加 DOM,此时浏览器该如何处理? 为了解决单
巨大提升!更快的 async 函数和 promises
(https://imghelloworld.osscnbeijing.aliyuncs.com/669a1c8f7203559afa4621628303674c.png) 翻译自:Faster async functions and promises(https://v8.dev/blog/fastasync) JavaScript
JavaScript sourceMap 笔记
js source map 建议打开一个真实的项目的sourceMap对照食用由于前端项目在网络中访问导致为了减少体积进行一系列优化操作,最后导致生产环境出问题无法定位到项目代码中的指定位置,使得调试变成一件很难得事。由此产生了Source Map。 它是个什么东西简单说,sourceMap就是一个文件,里面储存着位置信息。仔细点说,这个
js-Answers一
JavaScript的组成 JavaScript 由以下三部分组成: 1. ECMAScript(核心):JavaScript 语言基础 2. DOM(文档对象模型):规定了访问HTML和XML的接口 3. BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法 JS的基本数据类型和引用数据类型
14个优秀 JS 前端框架、库、工具及其使用时机
  这篇文章主要描述现今流行的一些 Javascript web 前端框架,库以及它们的适用场景。   新的 Javascript 库层出不穷,从而Web 社区愈发活跃、多样、在多方面快速发展。详细去描述每一种主流的 Javascript 框架和库近乎
JavaScript 是什么?
前言 引用《JavaScript 高级程序设计第四版》中说的话 ——“从简单的输入验证脚本到强大的编程语言,JavaScript 的崛起没有任何人预测到。它很简单,学会用只要几分钟;它又很复杂,掌握它要很多年。要真正学好用好 JavaScript,理解其本质、历史及局限性是非常重要的”。 面试官:JavaScript 是什么? 我:
理解 Javascript 中的 Async / Await
在本文中,我们将探讨async/await,对于每个Javascript开发人员来说,是异步编程的首选工具。如果您不熟悉javascript,请不要担心,本文将帮助您async/await从头开始理解。 介绍async/await 是javascript中的一种模式,可使您的代码以同步方式执行,但又不影响javascript的异步行为。 定义异步功能要定义一
JavaScript 和 Node.js 中事件循环
1.JavaScript中事件循环可以参考《JavaScript忍者秘籍第二版》第十三章,讲解的很好。JavaScript中事件循环,主要就在理解宏任务和微任务这两种异步任务。宏任务(macrotask): setTimeOut 、 setInterval 、 setImmediate 、 I/O 、 各种callback、 UI渲染 、messageCh
了解什么是 TypeScript
内容纲要 了解什么是 TypeScript TypeScript 基本语法 TypeScript 介绍 TypeScript 是什么TypeScript 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并
盘点3个可以操作JavaScript的Python库
前言我们都知道Python可以很轻松的实现某些功能,而且还可以编写网页,比如Remi,Pysimplegui,但是操作JavaScript这种浏览器的脚本语言,还是第一次听说,小编也是第一次听说,于是就跟大家脑补这一知识。 一、PyExecJS是一个可以执行JavaScript脚本的Python模块,可以与网页上的JavaScript进行交互,这样就能更加