JS 奇技淫巧(一)

parseInt 遇上 map

["1", "2", "3"].map(parseInt) // [ 1, NaN, NaN ]
js

parseInt 函数需要两个参数 parseInt(value, radix) , map 的回调函数存在三个参数 callback(currenvValue, index, array) 。MDN 文档中指明 parentInt 第二个参数是一个 2 到 36 之间的整数值,用于指定转换中采用的基数。如果省略该参数或者其值为 0,数字将以 10 为基础来解析。如果该参数小于 2 或者大于 36,则 parentInt 返回 NaN。此外,转换失败也会返回 NaN

题目分析如下:

  • parseInt("1", 0) 的结果当作十进制来解析,返回 1;
  • parseInt("2", 1) 的第二个参数非法,返回 NaN
  • parseInt("3", 2) 在进制中,“3” 是非法字符,转换失败,返回 NaN

parseInt 小贼

console.log(parseInt("3", 8)) // 3
console.log(parseInt("3", 2)) // NaN
console.log(parseInt("3", 0)) // 3
js

这个在第一个问题中解释的很清楚了。

神奇的 null

typeof null, null instanceof Object // [ 'object', false ]
js

typeof null 的结果是 object ,这是 ECMAScript 的 bug ,结果应该是 ”null"。但是这个 bug 由来已久,在 JavaScript 中存在了近二十年,也许永远不会被修复,因为它牵扯到太多的 web 系统,修复它可能会产生更多的 bug,令很多系统无法工作。

instanceof 运算符用来测试一个对象在其原型链构造函数上是否具有 prototype 属性,null 值并不以 Object 为原型创建出来,所以 null instance Object 返回 false。

愤怒的 reduce

[3, 2, 1].reduce(Math.pow), [].reduce(Math.pow) // Reduce of empty array with no initial value
js

如果数组为空且没有提供 initialValue ,会抛出 TypeError 。如果数组仅有一个元素(无论位置如何)且没有提供 initialValue ,或者有提供 initialValue 但是数组为空,那么唯一值将被返回并且 callback 不会被执行。

[[3].reduce(() => {}), [].reduce(() => {}, 3)]
js

该死的优先级

const val = 'heora'
console.log('Value is' + (val === 'heora') ? 'Something' : 'Nothing') // Something
js

结果输入 Something,因为 + 的优先级比条件运算符的 condition ? val1 : val2 的优先级高。

要想得到预期结果,我们可以使用模板字符串。

console.log(`Value is ${ (val === 'heora') ? 'Something' : 'Nothing' }`) // Value is Something
js

变量提升

var name = 'World!'

;(function () {
  if (typeof name === 'undefined') {
    var name = 'Jack'
    console.log('Goobye ' + name)
  } else {
    console.log('Hello' + name)
  }
})();
js

答案是 Goobye Jack 。在 JavaScript 中,functions 和 variables 会被提升。变量提升是 JavaScript 将声明移至作用域 scope(全局域或当前函数作用域)顶部的行为。这意味着你可以在声明一个函数或变量之前引用它,或者可以说,一个变量或函数可以在它被引用之后声明。

上面的代码和下面这段代码是等价的。

var name = 'World!'

;(function () {
  var name
  
  if (typeof name === 'undefined') {
    name = 'Jack'
    console.log('Goobye ' + name)
  } else {
    console.log('Hello' + name)
  }
})();
js

死循环陷阱

var END = Math.pow(2, 53)
var START = END - 100
var count = 0

for (var i = START; i <= END; i++) {
  console.log(count)
  count++
}
console.log(count)
js

在 JavaScript 中,2^53 是最大的值。2^53 + 1 == 2^53,所以这个循环无法终止。

Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
js

过滤器魔法

var arr = [0, 1, 2]
arr[10] = 10
arr.filter((x) => x === undefined) // []
js

filter 为数组中的每一个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 或等价于 true 的值的元素创建一个新数组。**callback 只会在已经赋值的元素上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。**那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中。

IEEE 754 标准

var one = 0.1
var two = 0.2
var six = 0.6
var eight = 0.8

[two - one == one, eight - six == two] // [true, false]
js

JavaScript 采用双精度浮点数格式,即 IEEE 754 标准。在该格式下,有些数字无法表示出来,比如:0.1 + 0.2 = 0.30000000000000004 ,这不是 JavaScript 的问题,所有采用该标准的语言都有这个问题,比如:Java、Python 等。

字符串陷阱

function showCase (value) {
  switch (value) {
    case 'A':
      console.log('Case A')
      break
    case 'B':
      console.log('Case B')
      break
    case 'C':
      console.log('Case C')
      break
    case 'D':
      console.log('Case D')
      break
    default:
      console.log('Do Not konw!')
  }
}
showCase(new String('A')) // Do Not konw!
js

在 switch 内部使用严格相等 === 进行判断,并且 new String("A") 返回的是一个对象,而 String("A") 则是直接返回字符串 “A”。

function showCase (value) {
  switch (value) {
    case 'A':
      console.log('Case A')
      break
    case 'B':
      console.log('Case B')
      break
    case undefined:
      console.log('undefined')
      break
    default:
      console.log('Do Not konw!')
  }
}
showCase(String('A')) // Case A
js

并非都是奇偶

function isOdd(num) {
  return num % 2 == 1
}
function isEven(num) {
  return num % 2 == 0
}
function isSane(num) {
  return isEven(num) || isOdd(num)
}

var values = [7, 4, '13', -9, Infinity]
console.log(values.map(isSane)) // [ true, true, true, false, false ]
js

-9 % 2 = -1 以及 Infinity % 2 = NaN,求余运算符会保留符号,所以只有 isEven 的判断是可靠的。

数组原型是数组

Array.isArray(Array.prototype) // true
js

其实 Array.prototype 也是一个数组,具体可以参考 MDN 文档

一言难尽的强制转换

var a = [0]
if ([0]) {
  console.log(a == true) // false
} else {
  console.log('what?')
}
js

[0] 需要被强制转成 Boolean 的时候会被认为是 true,所以进入第一个 if 语句。对于 a == true ,在 == 相等中,如果有一个操作符是布尔类型,会先把它转成数字,所以比较就变成了 [0] == 1。同时如果其他类型与数字比较,会尝试把这个类型转换成数字在进行宽松比较,而对象(数组也是对象)会先调用 toString() 方法,此时 [0] 会变成 ”0“ ,然后再将字符串 ”0“ 转成数字 0,而 0 == 1 的结果显然是 false。

所以当使用 a == false 进行比较,结果将是 true。

var a = [0]
if ([0]) {
  console.log(a == true) // false
  console.log(a == false) // true
} else {
  console.log('what?')
}
js

撒旦之子 ”==“

[] == [] // false
js

如果比较的两个对象指向的是一个对象,就返回 true,否则就返回 false。显然,这是两个不同的数组对象。

加号 vs 减号

'5' + 3 // 53
'5' - 3 // 2
js

"5" + 3 = "53" 很好理解,+ 运算符只要有一个是字符串,就会变成字符串拼接操作。而 - 运算符要求两个操作数都是数字,如果不是,则强制转换成数字,所以就变成 5 - 3 = 2

打死那个疯子

1 + - + + + - + 1 // 2
js

这个代码只能出现在示例代码中,如果你发现哪个疯子写在生产代码中,打死他就行了。

你只要知道 +1 = 1-1 = -1 ,注意符号之间的空格。两个减号抵消,所以最终结果就是 1 + 1 = 2。或者你可以在符号之间插入 0 来理解,即 1 + 0 - 0 + 0 + 0 + 0 - 0 + 1 ,这样就一目了然了。

淘气的 map

var arr = Array(3)
arr[0] = 2
arr.map((elem) => '1') // [ '1', <2 empty items> ]
js

map 方法会给原数组的每个元素都按顺序调用一次 callback 函数。callback 函数每次执行后的返回值组合成一个新数组。callback 函数只会在有值的索引上被调用。那些从没被赋过值或者使用 delete 删除的索引则不会被调用。

统统算我的

function sideEffecting(arr) {
  arr[0] = arr[2]
}
function bar(a, b, c) {
  c = 10
  sideEffecting(arguments)
  return a + b + c
}
console.log(bar(1, 1, 1)) // 21 
js

在 JavaScript 中,参数变量和 arguments 是双向绑定的。改变参数变量,arguments 中的值也会改变。改变 arguments 中的值,参数变量也会改变。

损失精度 IEEE 754

var a = 111111111111111110000
var b = 1111
console.log(a + b) // 111111111111111110000
js

这是 IEEE 754 规范的黑锅,不是 JavaScript 的问题。这么大的数占用过多位数,会丢失精度。

反转世界

var x = [].reverse
x() // Cannot convert undefined or null to object
js

最小的正值

Number.MIN_VALUE > 0 // true
js

MAX_VALUE 属性是 JavaScript 里最接近 0 的正值,而不是最小的负值。

MIN_VALUE的值约为 5e-324。小于 MIN_VALUE (“underflow values”) 的值将会转换为 0。

因为 MIN_VALUE 是 Number 的一个静态属性,因此应该直接使用:Number.MIN_VALUE,而不是作为一个创建的 Number实例的属性。

谨记优先级

[1 < 2 < 3, 3 < 2 < 1] // [true, true]
js

<> 的优先级都是从左到右,所以 1 < 2 < 3 会先比较 1 < 2 ,这会得到 true,但是 < 要求比较两边都是数字,所以会发生隐式强制类型转换,将 true 转换为 1,所以最后就变成比较 1 < 3 ,结果显示为 true。同理可以分析后者。

坑爹中的战斗机

2 == [[[2]]] // true
js

根据 ES5 规范,如果比较的两个值中有一个是数字类型,就会尝试将另外一个值强制转换成数字,在进行比较。而数组强制转换成数字的过程会先调用它的 toString 方法转成字符串,然后再转成数字。所以 [2] 会被转成 “2”,然后递归调用。最终 [[[2]]] 会被转换成数字 2。

小数点魔术

// 以下代码分别打印

console.log(3.toString()) // error
console.log(3..toString()) // 3
console.log(3...toString()) //  error
js

点运算符会被优先识别为数字常量的一部分,然后才是对象属性访问符。所以 3.toString() 实际上被 JS 引擎解析成 (3).toString(),显然会出现语法错误。但是如果你自己写 (3).toString() ,人为加上括号,这就是合法的。

自动提升全局变量

;(function () {
  var x = y = 1
})();
console.log(y) // 1
console.log(x) // x is not defined
js

很经典的例子,在函数中如果没有用 var 声明变量 y,y 就会被自动创建在全局变量 window 下面,所以在函数外面也可以访问到。而 x 由于被 var 声明过,所以在函数外部是无法访问的。