likes
comments
collection
share

JavaScript的“神奇”之处

作者站长头像
站长
· 阅读数 24

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)
  • 如果值为 undefinedNumber(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

一元加运算符会尝试将其值转换成数字。 它可以转换字符串形式表达的整数和浮点数,以及非字符串值 truefalse 和 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 二进制浮点运算标准引起的。极大的数字会被四舍五入到最近的偶数。 可以参考阅读:

精度计算

来自 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就好。

转载自:https://juejin.cn/post/7172059960140890125
评论
请登录