你知道JavaScript是一种多么奇葩的编程语言吗?


微信号:gh_ffb279ea1674 奇舞周刊

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

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

这篇文章的思想源于Brian Leroux,受到他在2012年dotJS上的演讲“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

?说明:

  • 12.5.9 Logical NOT Operator (!)
  • 7.2.13 Abstract Equality Comparison

true 是 false

!!’false’ == !!’true’ // -> true

!!’false’ === !!’true’ // -> true

?说明:

按照下面这几步思考:

true == ‘true’ // -> true

false == ‘false’ // -> false

// ‘false’ 不是空字符串,因此它的值为真值

!!’false’ // -> true

!!’true’ // -> true

  • 7.2.13 Abstract Equality Comparison

fooNaN

JavaScript 的一个老笑话:

“foo” + + “bar” // -> ‘fooNaN’

?说明:

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

  • 12.8.3 The Addition Operator (+)

NaN 不等于 NaN

NaN === NaN // -> false

?说明:

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

  • 如果 Type(x)不等于Type(y),则返回false。
  • 如果 Type(x)是Number,那么

遵循IEEE的NaN定义:

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

下面这个输出fail

可能你不会相信,但…

(![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]

// -> ‘fail’

?说明:

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

(![]+[]) // -> ‘false’

![] // -> false

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

(![]+[].toString()) // ‘false’

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

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

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

[]为真,但不是true

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

!![] // -> true

[] == true // -> false

?说明:

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

  • 12.5.9 Logical NOT Operator (!)
  • 7.2.13 Abstract Equality Comparison

null是假的(falsy),但不等于false

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

!!null // -> false

null == false // -> false

但是,其他的假值,如0或”等于false。

0 == false // -> true

” == false // -> true

?说明:

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

  • 7.2.13 Abstract Equality Comparison

最小的值大于零

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

Number.MIN_VALUE > 0 // -> true

?说明:

Number.MIN_VALUE是5e-324,即可以在浮点精度内表示的最小正数,即尽可能接近于零。它定义了浮点数给能给你的最小值。

数组相加

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

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

undefined 和 Number

如果我们没有将任何参数传递给 Number 的构造函数,我们将得到0。

undefined是一个分配给形式参数的值,它没有实际的参数,因此您可能希望Number(无参数)不定义为其参数的值。

然而当我们传一个undefined的时候,我们将得到NaN。

Number() // -> 0

Number(undefined) // -> NaN

?说明:

根据标准:

  1. 如果没有参数传递给这个函数,n为+0。
  2. 否则,让 n 等于否则,n 为ToNumber(value)
  3. 所以在这里传入undefined,则ToNumber(undefined)应返回NaN。

这里有相应的资料:

  • 20.1.1 The Number Constructor
  • 7.1.3 ToNumber(argument)

parseInt是个坏小子

parseInt 因为一些怪癖而出名,比如:

parseInt(‘f*ck’); // -> NaN

parseInt(‘f*ck’, 16); // -> 15

?说明:发生这种情况是因为 parseInt 将逐字符的解析,直到它遇到解析不了的字符。’f * ck’ 中的 f 是十六进制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进制时,第二十一个字母”u”,解码整个字符串。在37时,不再有可以生成的有效数字集合,所以返回NaN。

不要忘记八进制:

parseInt(’06’); // 6

parseInt(’08’); // 8 如果支持ECMAScript 5

parseInt(’08’); // 0 如果不支持ECMAScript 5

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

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方法被调用。根据规范,该方法返回:

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

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

相应文档:

  • 12.5.6 Unary + Operator
  • 12.8.3 The Addition Operator (+)
  • 7.1.3 ToNumber(argument)

HTML 注释在 JavaScript 中有效

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

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

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

  • B.1.3 HTML-like Comments

NaN 是数字

NaN的类型是 number:

typeof NaN // -> ‘number’

?说明:

typeof和instanceof运算符的工作原理:

  • 12.5.5 The typeof Operator
  • 12.10.4 Runtime Semantics: InstanceofOperator(O,C)

[]和null是对象

typeof [] // -> ‘object’

typeof null // -> ‘object’

// however

null instanceof Object // false

?说明:

typeof在规范中的定义:

  • 12.5.5 The typeof Operator 根据规范,typeof运算符返回一个字符串(typeof Operator Results.)对于null,一般的对象,标准的外来对象,以及非标准的外来对象,没有实现[[Call]],将会返回字符串“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 二进制浮点运算标准引起的。在这个标准之上,它会舍入到最接近的偶数。阅读更多:

  • 6.1.6 The Number Type
  • 维基百科上的IEEE 754

0.1 + 0.2 的精度问题

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

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的网站。它发生在使用浮点数的每种语言中,而不仅仅是JavaScript。

数字补丁

你可以添加自己的方法来包装对象,如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的属性列表:

  • 20.1 Number Objects

三个数字比较

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

详细了解规范中的关系运算符:

  • 12.10 Relational Operators

有趣的数学运算

通常 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)

正则的相加运算

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

// 实现toString方法

RegExp.prototype.toString = function() {

return this.source

}

/7/ – /5/ // -> 2

?说明:

  • 21.2.5.10 get RegExp.prototype.source

字符串不是String的实例

‘str’ // -> ‘str’

typeof ‘str’ // -> ‘string’

‘str’ instanceof String // -> false

?说明:

String 的construnctor返回一个字符串 :

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构造函数的更多信息:

  • 21.1.1 The String Constructor

用反引号调用函数

我们来声明一个将所有参数返回到控制台中的函数:

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 社区很受欢迎。

规范:

  • 12.3.7 Tagged Templates

Call call call

由@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?

?说明:

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

// 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等等。

  • Object.prototype.constructor
  • 19.1.3.1 Object.prototype.constructor

用对象作为对象属性的key

{ [{}]: {} } // -> { ‘[object Object]’: {} }

?说明:

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

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

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

// structure:

// {

// ‘[object Object]’: {

// ‘[object Object]’: {}

// }

// }

阅读更多参考:

  • Object initializer
  • 12.2.6 Object Initializer

使用__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]’。

  • 12.2.9 Template Literals
  • Object initializer

使用默认值进行解构

思考这个例子:

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

点和展开操作

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

[…[…’…’]].length // -> 3

?说明:

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

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

逐步思考:

[…’…’] // -> [ ‘.’, ‘.’, ‘.’ ]

[…[…’…’]] // -> [ ‘.’, ‘.’, ‘.’ ]

[…[…’…’]].length // -> 3

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

[…’…’] // -> [ ‘.’, ‘.’, ‘.’ ]

[…[…’…’]] // -> [ ‘.’, ‘.’, ‘.’ ]

[…[…[…’…’]]] // -> [ ‘.’, ‘.’, ‘.’ ]

[…[…[…[…’…’]]]] // -> [ ‘.’, ‘.’, ‘.’ ]

// and so on …

标签

可能很多人不知道 JavaScript 中的标签。他们很有趣:

foo: {

console.log(‘first’);

break foo;

console.log(‘second’);

}

// > first

// -> undefined

?说明:

带标签的语句与break或continue语句一起使用。你可以使用标签来标识循环,然后使用break或continue语句来控制程序中断或者继续执行。

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

详细了解 JavaScript 中的标签:

  • 13.13 Labelled Statements
  • Labeled statements

嵌套标签

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

?说明:

和之前一样,参考下面的链接:

  • 12.16 Comma Operator (,)
  • 13.13 Labelled Statements
  • Labeled statements

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

// …

?说明:

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

  • 25 Control Abstraction Objects
  • 25.3 Generator Objects

class 的 class

考虑这个让人摸不着头脑的语法:

(typeof (new (class { class () {} }))) // -> ‘object’

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

?说明:

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

const foo = {

class: function() {}

};

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

class {

class() {}

}

默认情况,类的返回总是一个简单的对象。它的 typeof 应该返回 ‘object’。

在这里了解更多: • 14.3 Method Definitions • 14.5 Class Definitions

非强转对象

有一个很常用的方法,用来避免强制类型转换。比如:

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

余下全文(1/3)
分享这篇文章:

请关注我们:

共有 2 条讨论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注