郑 farmer

WTF JavaScript ?

原文链接: github.com

什么鬼 JavaScript ?

一个关于 JavaScript 的花式玩法列表。

JavaScript 是一个伟大的语言。它有简单的语法,完善的生态系统,更重要的,有一个庞大的社区。

同时我们都知道,JavaScript 是一个有很多有趣的“潜规则”的语言。其中有一些经常在日常工作中给我们添麻烦,而有些可以给我们带来帮助,让我们大笑起来。

这篇文章的思想源于Brian Leroux,受到他在2012年dotJS上的演讲“WTFJS”的高度启发。

dotJS 2012 - Brian Leroux - WTFJS

目录

??动机

只是为了好玩。

“Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds

这篇文章主要收集了一些有趣(qí pā)的例子,并尽可能的解释它们如何工作的。因为这样可以让我们学习到很多之前不知道的东西。

如果你是个初学者,可以使用此文章来更深入了解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' is not empty string, so it's truthy value
!!'false' // -> true
!!'true'  // -> true

fooNaN

学院派 JavaScript 中的一个笑话:

"foo" + + "bar" // -> 'fooNaN'

?说明:

该表达式可以转换为 'foo'+(+'bar')+‘bar’将 “bar” 转换为NaN

NaN 不等于 NaN

NaN === NaN // -> false

?说明:

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

  1. 如果 Type(x)不等于Type(y),则返回false。
  1. 如果 Type(x)是Number,那么
1\.  If `x` is **NaN**, return **false**.

2\.  If `y` is **NaN**, return **false**.

3\.  … … …

-- 7.2.14Strict Equality Comparison

遵循IEEE的NaN定义:

四个相互排斥的关系是可能的:小于,等于,大于和无序。当至少一个操作是 NaN 时,最后一种情况出现。每个 NaN 相对于所有东西来说都是无序的,包括自己。

“IEEE754 中 NaN值返回false的所有比较的理由是什么?” --StackOverflow

It's a fail

可能你不会相信,但...

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

?说明:

将这条语句做几次分割,我们来分析一下结果:

(![]+[]) // -> 'false'
![]      // -> false

我们尝试将[]置为false。但是通过一些内部函数调用,最终转换为一个字符串.

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

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

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

剩下的部分显而易见的,但是 i 很特别。在 'falseundefined'里取到了索引为10.

[]为真,但不是true

数组为真值,但是它不等于true

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

?说明:

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

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

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

同时,其他的假值,如0‘’等于false

0 == false  // -> true
'' == false // -> true

?说明:

与前面一样,这是一个相应的链接:

Number.MIN_VALUE是最小的数字,但还是大于零:

Number.MIN_VALUE > 0 // -> true

?说明:

Number.MIN_VALUE5e-324,即可以在浮点精度内表示的最小正数,即尽可能接近于零。它定义了最好的分辨率浮标给你。

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

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

数组相加

如果两个数组相加会怎样?

[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'

undefinedNumber

如果我们没有将任何参数传递给 Number 的构造函数,我们将得到0undefined是一个分配给形式参数的值,它没有实际的参数,因此您可能希望Number(无参数)不定义为其参数的值。(undefinedis a value assigned to formal arguments which there are no actual arguments,so you might expect thatNumberwithout arguments takesundefinedas a value of its parameter)然而当我们传一个undefined的时候,我们将得到NaN

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

?说明:

根据标准:

  1. 如果没有参数传递给这个函数,n+0

  2. 否则,让 n 等于否则,n 为ToNumber(value)

  3. 所以在这里传入undefined,则ToNumber(undefined)应返回NaN

这里有一个相应的资料:

parseInt是个坏蛋

parseInt 有一些怪癖比如:

parseInt('f*ck');     // -> NaN
parseInt('f*ck', 16); // -> 15

?说明:发生这种情况是因为 parseInt 将逐字符的解析,直到它遇到解析不了的字符。'f * ck' 中的 f 是十六进制15。

解析无穷大到整数是...

//
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进制时,第二十一个字母“u”,解码整个字符串。在37时,不再有可以生成的有效数字集合,所以返回NaN。

[“parseInt(null,24) === 23…等等,什么?“(https://stackoverflow.com/questions/6459758/parseintnull-24-23-wait-what)在StackOverflow

不要忘记八进制:

parseInt('06'); // 6
parseInt('08'); // 0

?说明:这是因为 parseInt 第二个参数代表进制。如果没有提供,并且字符串以0开始,它将被解析为八进制数。

truefalse做计算操作

我们做一些计算操作:

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

嗯...?

?说明:

我们可以通过 Number 构造函数将这些值强制转换为数字。很明显,true将被转换成1

Number(true) // -> 1

+运算符尝试将其值转换成数字。它可以转换整数或者浮点数形式的字符串,以及非字符串值truefalsenull。如果不能解析,会转为NaN。这意味着我们可以强制true转为1

+true // -> 1

当你执行加法或乘法时,ToNumber方法被调用。根据规范,该方法返回:

如果argumenttrue,则返回1。如果argument为false,则返回+0

这就是为什么我们可以与布尔值相加,视为常规数字并获得正确的结果。

相应文档:

HTML 注释在 JavaScript 中有效

你可能不信,<!--(在HTML中的注释)在 JavaScript 中是有效的

震惊了?HTML 类似的注释,旨在让没法解析<script>标签浏览器优雅降级。例如现在不再流行的 Netscape 1.x 的这类浏览器。所以实际上,将 HTML 注释放在你的脚本标签中也没有任何意义了。

然而由于 Node.js 基于 V8 引擎,Node.js运行时也支持类似 HTML 的注释。而且,它们是规范的一部分:

NaN的类型是 number

typeof NaN            // -> 'number'

?说明:

typeofinstanceof运算符的工作原理:

?说明:

typeof在规范中的定义:

然而其实,你可以使用 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 的精度问题

众所周知的笑话。0.1 + 0.2有个非常牛X的精确度:

0.1 + 0.2 // -> 0.30000000000000004
(0.1 + 0.2) === 0.3 // -> false

?说明:

浮点数字迷题破解?”--StackOverflow

其实程序中的常数0.20.3也将近似为真值。It happens that the closestdoubleto0.2is larger than the rational number0.2but that the closestdoubleto0.3is smaller than the rational number0.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 // 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

?说明:

?说明:

Stringconstrunctor返回一个字符串 :

typeof String('str')   // -> 'string'
String('str')          // -> 'str'
String('str') == 'str' // -> true

我们来试一下new

new String('str') == 'str' // -> true
typeof new String('str')   // -> 'object'

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 ] ]

?说明:

如果你熟悉Tagged template literals那么可能你感觉这很正常,在上面的例子中,f函数是模板的标签。模板文字之前的标签允许您使用函数解析模板文字。标签函数的第一个参数是一个包含字符串的数组。其余的参数与表达式有关。比如:

function template(strings, ...keys) {
  // do something with strings and keys…
}

这是一个有魔力的类库,名为? styled-components,这在 React 社区很受欢迎。

规范:

@cramforce发现

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

?说明:

前方高能!看后可能会损伤大量脑细胞。尝试在你脑海中重现此代码:我们正在使用apply方法调用call方法。阅读更多:

?说明:

让我们逐步思考这个例子:

// Declare a new constant which is a string 'constructor'
const c = 'constructor'

// c is a string
c // -> 'constructor'

// Getting a constructor of string
c[c] // -> [Function: String]

// Getting a constructor of constructor
c[c][c] // -> [Function: Function]

// Call the Function constructor and pass
// the body of new function as an argument
c[c][c]('console.log("WTF?")') // -> [Function: anonymous]

// And then call this anonymous function
// The result is console-logging a string 'WTF'
c[c][c]('console.log("WTF?")')() // > WTF

Object.prototype.constructor返回一个Object用来创建实例函数的引用,在字符串中,它是String,数字则为Number等等。

用对象作为对象属性的key

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

?说明:

为什么这样?这里应用到了Computed property name。当在方括号中传递一个对象时,它会将对象强制转换为字符串,所以我们得到一个属性键'[object Object]'和值 {}

同样的方式,我们还可以像这样使用中括号:

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

// structure:
// {
//   '[object Object]': {
//     '[object Object]': {}
//   }
// }

阅读更多参考:

使用__proto__访问原型

大家都知道,原始数据类型是没有原型的。但是,如果我们尝试对它们获取proto,我们会得到这样的:

(1).__proto__.__proto__.__proto__ // -> null

?说明:

这是因为原始数据类型没有原型,它将使用ToObject方法包装在包装器对象中。分步来看:

(1).__proto__ // -> [Number: 0]
(1).__proto__.__proto__ // -> {}
(1).__proto__.__proto__.__proto__ // -> null

以下是有关__proto__的更多信息:

${{Object}}

下面的表达结果是什么?

`${{Object}}`

答案是:

// -> '[object Object]'

?说明:

我们使用Shorthand property notation表示法定义了一个带有属性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的值。

  5. Object initializer

Dots and spreading

下面是个关于数组解构的有趣例子思考这个:

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

?说明:

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

一个'...'字符串由三个.组成,因此结果数组的长度将为3。

逐步思考:

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

显然,我们可以像我们想要的那样解构和包装数组的元素:

[...'...']                 // -> [ '.', '.', '.' ]
[...[...'...']]            // -> [ '.', '.', '.' ]
[...[...[...'...']]]       // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]]  // -> [ '.', '.', '.' ]
// and so on …

标签

可能很多人不知道 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 的继承类。

yields 它自己的 generator

考虑这个例子,

(function* f() { yield f })().next()
// -> { value: [GeneratorFunction: f], done: false }

如你所见,返回的值是一个值等于f的对象。在这种情况下,我们可以这样做:

(function* f() { yield f })().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }

// and again
(function* f() { yield f })().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }

// and again
(function* f() { yield f })().next().value().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }

// and so on
// …

?说明:

要了解为什么这样,请阅读这些部分:

class 的 class

考虑这个模糊的语法:

(typeof (new (class { class () {} }))) // -> 'object'

看来我们一个类中声明一个类。按理来说应该会报错,但是,我们得到一个“object”字符串。

?说明:

由于 ECMAScript 5 的时代,允许用关键字作为属性名称。所以想一想这个简单的对象例子:

const foo = {
  class: function() {}
};

用 ES6 则简化成如下方法定义。此外,类还可能是匿名的。所以如果我们删除 function,我们将得到:

class {
  class() {}
}

默认情况,类的返回总是一个简单的对象。它的 typeof 应该返回 '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

?说明: