JavaScript 是一门很棒的语言。它的语法简单,生态系统也很庞大,最重要的是,它拥有最伟大的社区力量。我们知道,JavaScript 是一个非常有趣的语言,但同时也充满了各种奇怪的行为。让我们一起来看一下吧~
example
数组等于一个数组取反:
[] == ![]; // -> true
解释
抽象相等运算符会将其两端的表达式转换为数字值进行比较。尽管这个例子中,左右两端均被转换为 0
,但原因各不相同。[]的值比较总是true,因此右值的数组取反后总是为 false
,然后在抽象相等比较中被类型转换为 0
。左值是另一种情形,空数组没有被转换为布尔值的话,尽管在逻辑上是true,但在抽象相等比较中,会被类型转换为数字 0
。
表达式的运算步骤如下:
+[] == +![]; // true;
0 == +false; // true;
0 == 0; // true;
true
不等于 ![]
,也不等于 []
;
数组不等于 true
,但数组取反也不等于 true
;数组等于 false
,数组取反也等于 false
:
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
解释
true == []; // -> false
true == ![]; // -> false
// 根据规范
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> true
false == ![]; // -> true
// 根据规范
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> true
![]; // -> false
false == false; // -> true
true 是 false
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
解释
// true 是真值,并且隐式转换为数字1,而字符串 'true' 会被转换为 NaN。
true == "true"; // -> false
false == "false"; // -> false
// 'false' 不是空字符串,所以它的值是 true
!!"false"; // -> true
!!"true"; // -> true
NaN
!== NaN
NaN === NaN; // -> false
解释
规范严格定义了这种行为背后的逻辑:
- 如果
Type(x)
不同于Type(y)
,返回 false。 - 如果
Type(x)
数值, 然后* 如果x
是 NaN,返回 false。* 如果y
是 NaN,返回 false。
根据 IEEE 对 NaN 的定义:
有四种可能的相互排斥的关系:小于、等于、大于和无序。
当比较操作中至少一个操作是 NaN 时,便是无序的关系。换句话说,NaN 对任何事物包括其本身比较都应当是无序关系。
Object.is()
和 ===
Object.is()
用于判断两个值是否相同,和 ===
操作符像作用类似,但它也有一些不同。
Object.is(NaN, NaN); // -> true
NaN === NaN; // -> false
Object.is(-0, 0); // -> false
-0 === 0; // -> true
Object.is(NaN, 0 / 0); // -> true
NaN === 0 / 0; // -> false
解释
在 JavaScript “语言”中,NaN
和 NaN
的值是相同的,但却不是严格相等。NaN === NaN
返回 false 是因为历史包袱,记住这个特例就行了。基于同样的原因,-0
和 0
是严格相等的,但它们的值却不同。
一个隐式类型转换的天花板例子
(![] + [])[+[]] +(![] + [])[+!+[]] +([![]] + [][[]])[+!+[] + [+[]]] +(![] + [])[!+[] + !+[]]; // -> 'fail'
解释 我们先分解成片段拆解来看,以下表达式出现:
+![]// -> 0
+!![] // -> 1
!![]// -> true
![] // -> false
[][[]]// -> undefined
+!![] / +![]// -> Infinity
[] + {} // -> "[object Object]"
+{} // -> NaN
![] + []; // -> 'false'
![]; // -> false
所以我们尝试将 []
和 false
加起来。但是因为一些内部函数调用(binary + Operator
- >ToPrimitive
- >[[DefaultValue]
]),我们最终将右边的操作数转换为一个字符串:
![] + [].toString(); // 'false'
将字符串作为数组,我们可以通过[0]
来访问它的第一个字符:
"false"[0]; // -> 'f'
剩下的部分以此类推,不过此处的 i
字符是比较巧的。fail
中的 i
来自于生成的字符串 falseundefined
,通过指定下标 ['10']
取得的。
null
是假值,但又不等于 false
尽管 null
是假值,但它不等于 false
。
!!null; // -> false
null == false; // -> false
但是,别的被当作假值的却等于 false
,如 0
或 ''
。
0 == false; // -> true
"" == false; // -> true
document.all
是一个 object,但又同时是 undefined。
尽管 document.all 是一个类数组对象(array-like object),并且通过它可以访问页面中的 DOM 节点,但在通过 typeof
的检测结果是 undefined
。
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'
同时,document.all
不等于 undefined
。
document.all === undefined; // -> false
typeof document.all; // -> 'undefined'
但是同时,document.all
不等于 undefined
。
document.all === undefined; // -> false
document.all == null; // -> true
不过。
document.all == null; // -> true
解释
document.all
作为访问页面 DOM 节点的一种方式,在早期版本的 IE 浏览器中较为流行。尽管这一 API 从未成为标准,但被广泛使用在早期的 JS 代码中。当标准演变出新的 API(例如
document.getElementById
)时,这个 API 调用就被废弃了。因为这个 API 的使用范围较为广泛,标准委员会决定保留这个 API,但有意地引入一个违反 JavaScript 标准的规范。这个有意的对违反标准的规范明确地允许该 API 与
undefined
使用[严格相等比较]得出false
,而使用[抽象相等比较]得出true
。
数组相加
如果你尝试将两个数组相加:
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
解释
数组之间会发生串联。步骤如下:
[1, 2, 3] +[4, 5, 6][// 调用 toString()(1, 2, 3)].toString() +[4, 5, 6].toString();
// 串联
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");
数组中的尾逗号
假设你想要创建了一个包含 4 个空元素的数组。如下所示,最终只能得到一个包含三个元素的数组,原因在于尾逗号:
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'
解释
尾逗号 (trailing commas,有时也称为“最后逗号”(final commas)) 在向 JavaScript 代码中添加新元素、参数或属性时非常有用。
如果您想添加一个新属性,若前一行已经有尾逗号,你无需修改前一行,只要添加一个新行并加上尾逗号即可。
这使得版本控制历史较为干净,编辑代码也很简单。
数组的相等性是深水猛兽
数组之间进行相等比较,是 JS 中的深水猛兽,看看这些例子:
[] == '' // -> 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
undefined
和 Number
无参数调用 Number
构造函数会返回 0
。我们知道,当函数没有接受到指定位置的实际参数时,该处的形式参数的值会是 undefined
。因此,你可能觉得当我们传入 undefined
时应当同样返回 0
。然而实际上传入 undefined
返回的是 NaN
。
Number(); // -> 0
Number(undefined); // -> NaN
解释
根据规范:
- 若无参数调用该函数,
n
将为+0
。 - 否则,
n
将为?Number(value)
。 - 如果值为
undefined
,Number(undefined)
应该返回NaN
。
parseInt
它的奇怪之处
parseInt("a*s"); // -> NaN
parseInt("a*s", 16); // -> 10
解释 这是因为 parseInt
会持续解析直到它解析到一个不识别的字符,'a*s'
中的 a
是 16 进制下的 10
。
parseInt
解析 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 时,第 21 个字母
“u”
进入数字的行列,此时整个字符串都可以解析了。而当基数增加到 37 以上,已经超出了数字和字母所能表达的数字范围,因此一律返回
NaN
。
解析八进制:
parseInt("06"); // 6
parseInt("08"); // 8 如果支持 ECMAScript 5
parseInt("08"); // 0 如果不支持 ECMAScript 5
解释
当输入的字符串以“0”开始时,根据实现的不同,会被解释为八进制或十进制。
ECMAScript 5 明确表示应当使用十进制,但有部分浏览器仍不支持。
因此推荐在调用
parseInt
函数时总是传入表示基数的第二个参数。
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
返回1
。
1/1999999
被转换为5.00000250000125e-7
,所以parseInt
返回5
。
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
方法。根据规范,该方法的返回值为:
如果
参数
是 true,返回 1。如果参数
是 false,则返回 +0。
因此我们可以将布尔值相加并得到正确的结果。
神奇的数字增长
999999999999999; // -> 999999999999999
9999999999999999; // -> 10000000000000000
10000000000000000; // -> 10000000000000000
10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002
解释
这是由 IEEE 754-2008 二进制浮点运算标准引起的。极大的数字会被四舍五入到最近的偶数。可以参考阅读:
- 6.1.6 数字类型
- 维基百科上的 IEEE 754
精度计算
来自 JavaScript 的知名笑话。0.1
和 0.2
相加是存在精度错误的
0.1 + 0.2; // -> 0.30000000000000004
0.1 + 0.2 === 0.3; // -> false
解释
程序中的常量
0.2
和0.3
是最接近真实值的近似值。最接近
0.2
的double
大于有理数0.2
,但最接近0.3
的double
小于有理数0.3
。
0.1
和0.2
的和大于有理数0.3
,因此在程序中进行常量比较会得到假。
这不仅仅是 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
对象和扩展其他对象并无不同之处。但是,扩展不符合规范的函数行为是不推荐的。
三个数字的比较
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-> 加法
Boolean + Number-> 加法
Boolean + Boolean -> 加法
Number+ String-> 串联字符串
String+ Boolean -> 串联字符串
String+ String-> 串联字符串
在相加之前,[]
和 {}
隐式调用 ToPrimitive
和 ToString
方法。
- 不过需要注意此处的
{} + []
,这是一个例外。 - 你可以发现它的求值结果与
[] + {}
不同,这是因为当我们不加括号时,它被当作是一个空的代码块和一个一元加法运算符,这个运算符会把其后的[]
转换为数字。具体如下:
{// 代码块
}
+[]; // -> 0
当我们加上括号,情况就不一样了:
({} + []); // -> [object Object]
正则表达式的加法
你知道可以做这样的运算吗?
// 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'
用反引号调用函数
我们来声明一个返回所有参数的函数:
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) {// 操作字符串和键值
}
到底 call 了谁
console.log.call.call.call.call.call.apply(a => a, [1, 2]); // -> 2;
解释
不用看它call了几遍,看你最后一个call就好。
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享