JavaScript 是一个伟大的语言。它有简单的语法,完善的生态系统,更重要的,有一个庞大的社区。
同时我们都知道,JavaScript 是一个有很多有趣的“潜规则”的语言。其中有一些经常在日常工作中给我们添麻烦,而有些可以给我们带来帮助,让我们大笑起来。
这篇文章的思想源于Brian Leroux,受到他在2012年dotJS上的演讲“WTFJS”的高度启发。
注:文中的说明中引用解释都可以在后面的链接找到 https://www.ecma-international.org/ecma-262/
只是为了好玩。 —“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
!
) (!!'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
学院派 JavaScript 中的一个笑话:
"foo" + + "bar" // -> 'fooNaN'
该表达式可以转换为 'foo'+(+'bar')
,+‘bar’
将 “bar” 转换为NaN
。
+
) (https://www.ecma-international.org/ecma-262/#sec-addition-operator-plus)NaN
不等于 NaN
NaN === NaN // -> false
规范中定义了这种行为背后的逻辑:
Type(x)
不等于Type(y)
,则返回false。1\. If x is **NaN**, return **false**.
2\. If y is **NaN**, return **false**.
3\. … … …
-- 7.2.14Strict Equality Comparison (https://www.ecma-international.org/ecma-262/#sec-strict-equality-comparison)
遵循IEEE的NaN定义:
四个相互排斥的关系是可能的:小于,等于,大于和无序。当至少一个操作是 NaN 时,最后一种情况出现。每个 NaN 相对于所有东西来说都是无序的,包括自己。 “IEEE754 中 NaN值返回false的所有比较的理由是什么?”--StackOverflow (https://stackoverflow.com/questions/1565164/1573715#1573715)
可能你不会相信,但...
(![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]
// -> 'fail'
将这条语句做几次分割,我们来分析一下结果:
(![]+[]) // -> 'false'
![] // -> false
我们尝试将[]
置为false
。但是通过一些内部函数调用,最终转换为一个字符串.
(![]+[].toString()) // 'false'
想想一个字符串作为数组,我们可以通过[0]
访问它的第一个字符:
'false'[0] // -> 'f'
剩下的部分显而易见的,但是 i
很特别。在 'falseundefined'
里取到了索引为10.
[]
为真,但不是true
数组为真值,但是它不等于true
。
!![] // -> true
[] == true // -> false
以下是ECMA-262规范中相应部分的链接:
!
) (https://www.ecma-international.org/ecma-262/#sec-logical-not-operator)false
尽管null
是假值,但它不等于false
。
!!null // -> false
null == false // -> false
同时,其他的假值,如0
或‘’
等于false
。
0 == false // -> true
'' == false // -> true
与前面一样,这是一个相应的链接:
Number.MIN_VALUE
是最小的数字,但还是大于零:
Number.MIN_VALUE > 0 // -> true
Number.MIN_VALUE
是5e-324
,即可以在浮点精度内表示的最小正数,即尽可能接近于零。它定义了最好的分辨率浮标给你。 现在,最小的正数是Number.NEGATIVE_INFINITY
,尽管这在严格意义上并不是真正的数字。 “为什么在JavaScript中为0小于Number.MIN_VALUE”?--StackOverflow (https://stackoverflow.com/questions/26614728/why-is-0-less-than-number-min-value-in-javascript)
如果两个数组相加会怎样?
[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(无参数)不定义为其参数的值。(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
根据标准:
n
为+0
。ToNumber(value)
undefined
,则ToNumber(undefined)
应返回NaN
。这里有一个相应的资料:
argument
) (https://www.ecma-international.org/ecma-262/#sec-tonumber)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开始,它将被解析为八进制数。
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
。
这就是为什么我们可以与布尔值相加,视为常规数字并获得正确的结果。
相应文档:
+
Operator+
)argument
)你可能不信,<!--
(在HTML中的注释)在 JavaScript 中是有效的
震惊了?HTML 类似的注释,旨在让没法解析<script>
标签浏览器优雅降级。例如现在不再流行的 Netscape 1.x 的这类浏览器。所以实际上,将 HTML 注释放在你的脚本标签中也没有任何意义了。
然而由于 Node.js 基于 V8 引擎,Node.js运行时也支持类似 HTML 的注释。而且,它们是规范的一部分:
NaN
是数字NaN
的类型是 number
:
typeof NaN // -> 'number'
typeof
和instanceof
运算符的工作原理:
typeof
OperatorO
,C
)
[]
和null
是对象
typeof [] // -> 'object' typeof null // -> 'object'
// however null instanceof Object // falsetypeof
在规范中的定义:
typeof
Operator 根据规范,typeof
运算符返回一个字符串(typeof Operator Results.)对于null
来说,ordinary,standard exotic and non-standard exotic objects which do not implement[[Call]]it returns string"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
的精度问题众所周知的笑话。0.1 + 0.2
有个非常牛X的精确度:
0.1 + 0.2 // -> 0.30000000000000004
(0.1 + 0.2) === 0.3 // -> false
“浮点数字迷题破解?”--StackOverflow
其实程序中的常数
0.2
和0.3
也将近似为真值。It happens that the closestdoubleto0.2is larger than the rational number0.2but that the closestdoubleto0.3is smaller than the rational number0.30.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
的属性列表:
为什么这样呢?那么问题在于表达式的第一部分。以下是它的工作原理:
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
剩下的例子呢?[]
和{}
在做相加运算之前,偷偷调用了ToPrimitive
和ToString
方法,了解详细规范参考:
+
)input
[,PreferredType
])argument
)
正则的相加运算你知道你可以这样做相加运算吗?
// Patch a toString method
RegExp.prototype.toString = function() {
return this.source
}
/7/ - /5/ // -> 2
String
的实例
'str' // -> 'str' typeof 'str' // -> 'string' 'str' instanceof String // -> falseString
的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
构造函数的更多信息:
我们来声明一个将所有参数返回到控制台中的函数:
function f(...args) {
return args
}
毫无疑问,你可以这样调用这个函数:
f(1, 2, 3) // -> [ 1, 2, 3 ]
但你知道反引号可以调用任何函数吗?
ftrue 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
方法。阅读更多:
thisArg
, ...args
)thisArg
, argArray
)
constructor
属性
const c = 'constructor' c[c]c')() // > 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 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
以上示例中:
x
,它的值是undefined
x
的值打包到对象属性x
中。x
的值,并希望赋值给y
。如果未定义该值,那么将用1
作为默认值。y
的值。下面是个关于数组解构的有趣例子思考这个:
[...[...'...']].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 中的标签:
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 的继承类。
考虑这个例子,
(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
// …
要了解为什么这样,请阅读这些部分:
考虑这个模糊的语法:
(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
来源:众成翻译
链接:http://www.zcfy.cc/article/what-the-f-ck-javascript-3844.html