你不知道的 JS(中)

coderljw 2024-10-13 大约 10 分钟

# 1. 类型

  • 很多开发人员将 undefined 和 undeclared 混为一谈,但在 JavaScript 中它们是两码事。undefined 是值的一种,undeclared 则表示变量还没有被声明过。

  • 遗憾的是,JavaScript 却将它们混为一谈,在我们试图访问 “undeclared” 变量时这样报错: ReferenceError: a is not defined,并且 typeof 对 undefined 和 undeclared 变量都返回 “undefined”。

  • 然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的办法。

# 2. 数字有效语法

(7).toFixed(2)
7.0.toFixed(2)
7..toFixed(2)
7 .toFixed(2)
1
2
3
4

# 3. 零值

var a = 0 / -3

a.toString() // '0'
a + '' // '0'
String(a) // '0'

JSON.stringify(-0) // '0'
JSON.parse('-0') // -0

+'-0' // -0
Number('-0') // -0
1
2
3
4
5
6
7
8
9
10
11

# 4. 特殊等式

  • 能使用 == 和 === 时就尽量不要使用 Object.is(..),因为前者效率更高、更为通用。Object.is(..)主要用来处理那些特殊的相等比较(-0 与 NaN)。

# 5. Array

  • Array 与 new Array 效果一样,不带时它会被自动补上。

  • 永远不要创建和使用空单元数组。

    空单元数组(无可迭代元素)

    Array(7)
    
    var a = []
    a.length = 7
    
    1
    2
    3
    4

    快速创建指定长度数组

    Array.apply(null, { length: 7 })
    
    Array(7).fill(7)
    
    1
    2
    3

# 6. 假值

  • 我们经常通过将 document.all 强制类型转换为布尔值(比如在 if 语句中)来判断浏览器是否是老版本的 IE。IE 自诞生之日起就始终遵循浏览器标准,较其他浏览器更为有力地推动了 Web 的发展。

  • 以下为假值。

    • undefined
    • null
    • +0、-0、0n、-0n 和 NaN
    • ''
    • 浏览器中的 document.all 等假值对象

# 7. 奇特的 ~ 运算符

  • 字位反转是个很晦涩的主题,JavaScript 开发人员一般很少需要关心到字位级别。

  • 对 ~ 还可以有另外一种诠释,源自早期的计算机科学和离散数学:~ 返回 2 的补码。这样一来问题就清楚多了!

  • ~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题。

var a = 'Hello World'

~a.indexOf('lo') // -(3 + 1) 真值
~a.indexOf('ol') // -(-1 + 1) 假值
1
2
3
4

# 8. 字位截除

  • 它只适用于 32 位数字,更重要的是它对负数的处理与 Math.floor(..)不同。

  • ~~x 能将值截除为一个 32 位整数,x | 0 也可以。~~x 优先级更高,x | 0 更简洁。

Math.floor(43.96) // 43
Math.floor(-43.96) // -44

~~43.96 // 43
~~-43.96 // -43

43.96 | 0 // 43
-43.96 | 0 // -43
1
2
3
4
5
6
7
8

# 9. 显示解析数字字符串

  • parseInt(1/0, 19) 实际上是 parseInt("Infinity", 19)。 第一个字符是 "I",以 19 为基数时值为 18。第二个字符 "n" 不是一个有效的数字字符,解析到此为止,和 "42px" 中的 "p" 一样。

  • 最后的结果是 18,而非 Infinity 或者报错。所以理解其中的工作原理对于我们学习 JavaScript 是非常重要的。

  • 此外还有一些看起来奇怪但实际上解释得通的例子。

parseInt(1 / 0, 19) // 18

parseInt(0.000008) // 0 ('0'来自于'0.000008')
parseInt(0.0000008) // 8 ('8'来自于'8e-7')
parseInt(false, 16) // 250 ('fa'来自于'false')
parseInt(parseInt, 16) // 15 ('f'来自于'function..')

parseInt('0x10') // 16
parseInt('103', 2) // 2
1
2
3
4
5
6
7
8
9

# 10. 字符串和数字之间的隐式强制类型转换

[] + {} // '[object Object]',相当于:'' + '[object Object]'

{} + [] // 0,第一个 {..} 会被认为是区块语句而不是对象字面量,相当于:+[]
1
2
3

# 11. 相等比较

  • 布尔值会先转换为数字再比较。
7 == true // false
7 == false // false
1
2
  • null 与 undefined '==' 比较相等,与其他假值都不相等。
null == undefined // true
null == '' || null == 0 || null == false // false
1
2
  • 因为没有对应的封装对象,所以 null 和 undefined 不能够被封装(boxed),Object(null) 和 Object(undefined) 均返回一个常规对象。

  • NaN 能够被封装为数字封装对象,但拆封之后 NaN == NaN 返回 false,因为 NaN 不等于 NaN。

'abc' == Object('abc') // true,拆封调用toString方法
null == Object(null) // false,不能够被封装
undefined == Object(undefined) // false,不能够被封装
NaN == Object(NaN) // false,与自身不等
1
2
3
4
  • 极端情况
[] == ![] // true,转换过程:[] -> '' -> 0,![] -> false -> 0

'' == [null] // true

0 == '\n' || 0 == '  ' // true
1
2
3
4
5
  • ES5 规范 11.8.5 节定义了 “抽象关系比较”(abstract relational comparison),分为两个部分:比较双方都是字符串(后半部分)和其他情况(前半部分)。

  • 该算法仅针对 a < b,a = '' > b 会被处理为 b < a。

[42] < ['43'] // true,转为数字再比较

['42'] < ['043'] // false,都为字符串,比较字母顺序

{} < {} // false,字符串 '[object Object]' 比较
{} == {} // false,内存地址比较
{} > {} // false,字符串 '[object Object]' 比较

{} <= {} // true,根据规则为 {} > {} 比较结果的反转
{} >= {} // true,根据规则为 {} < {} 比较结果的反转
1
2
3
4
5
6
7
8
9
10

# 12. 语句的结果值

  • 从技术角度来解释要更复杂一些。ES5 规范 12.2 节中的变量声明(VariableDeclaration)算法实际上有一个返回值(是一个包含所声明变量名称的字符串,很奇特吧?),但是这个值被变量语句(VariableStatement)算法屏蔽掉了(for..in 循环除外),最后返回结果为空(undefined)。

  • 如果你用开发控制台(或者 JavaScript REPL——read/evaluate/print/loop 工具)调试过代码,应该会看到很多语句的返回值显示为 undefined,只是你可能从未探究过其中的原因,其实控制台中显示的就是语句的结果值。

var b
if (true) {
  b = 4 + 38
}
// 控制台打印42
1
2
3
4
5
  • 可以使用万恶的 eval(..) (又读作“evil”)来获得结果值。
var a, b
a = eval('if (true) { b = 4 + 38; }')
a // 42
1
2
3

# 13. 表达式的副作用

  • ++a++ 会产生 ReferenceError 错误,因为运算符需要将产生的副作用赋值给一个变量。以 ++a++ 为例,它首先执行 a++ (根据运算符优先级),返回 42,然后执行 ++42,这时会产生 ReferenceError 错误,因为 ++ 无法直接在 42 这样的值上产生副作用。

  • a = 42 中的 = 运算符看起来没有副作用,实际上它的结果值是 42,它的副作用是将 42 赋值给 a。

var a
a = 42 // 42
a // 42
1
2
3
  • 链式赋值常常被误用,例如 var a = b = 42,看似和前面的例子差不多,实则不然。如果变量 b 没有在作用域中象 var b 这样声明过,则 var a = b =42 不会对变量 b 进行声明。在严格模式中这样会产生错误,或者会无意中创建一个全局变量。

# 14. 上下文规则

  • 标签语句(非 goto)。

    contine foo 并不是指 “跳转到标签 foo 所在位置继续执行”,而是 “执行 foo 循环的下一轮循环”。

    // 标签为foo的循环
    foo: for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        // 如果j和i相等,继续外层循环
        if (j == i) {
          // 跳转到foo的下一个循环
          continue foo
        }
        // 跳过奇数结果
        if ((j * i) % 2 == 1) {
          // 继续内层循环(没有标签的)
          continue
        }
        console.log(i, j)
      }
    }
    // 1 0
    // 2 0
    // 2 1
    // 3 0
    // 3 2
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    break foo 不是指 “跳转到标签 foo 所在位置继续执行”,而是 “跳出标签 foo 所在的循环 / 代码块,继续执行后面的代码”。

    // 标签为foo的循环
    foo: for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 4; j++) {
        if (i * j >= 3) {
          console.log('stopping!', i, j)
          break foo
        }
        console.log(i, j)
      }
    }
    // 0 0
    // 0 1
    // 0 2
    // 0 3
    // 1 0
    // 1 1
    // 1 2
    // 停止! 1 3
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • JSON 被普遍认为是 JavaScript 语言的一个真子集, {"a":42} 这样的 JSON 字符串会被当作合法的 JavaScript 代码(请注意 JSON 属性名必须使用双引号!)。其实不是!如果在控制台中输入 {"a":42} 会报错。因为标签不允许使用双引号,所以 "a" 并不是一个合法的标签,因此后面不能带 : 。

    • JSON 的确是 JavaScript 语法的一个子集,但是 JSON 本身并不是合法的 JavaScript 语法。
    • 这里存在一个十分常见的误区,即如果通过 <script src=..> 标签加载 JavaScript 文件,其中只包含 JSON 数据(比如某个 API 返回的结果),那它就会被当作合法的 JavaScript 代码来解析,只不过其内容无法被程序代码访问到。JSON-P(将 JSON 数据封装为函数调用,比如 foo({"a":42}) )通过将 JSON 数据传递给函数来实现对其的访问。
    • {"a":42} 作为 JSON 值没有任何问题,但是在作为代码执行时会产生错误,因为它会被当作一个带有非法标签的语句块来执行。foo({"a":42}) 就没有这个问题,因为 {"a":42} 在这里是一个传递给 foo(..) 的对象常量。所以准确地说,JSON-P 能将 JSON 转换为合法的 JavaScript 语法。
  • else if 和可选代码块。

    很多人误以为 JavaScript 中有 else if,因为我们可以这样来写代码。

    if (a) {
      // ..
    } else if (b) {
      // ..
    } else {
      // ..
    }
    
    1
    2
    3
    4
    5
    6
    7
    • 事实上 JavaScript 没有 else if,但 if 和 else 只包含单条语句的时候可以省略代码块的 { }。
    • 我们经常用到的 else if 实际上是这样的。
    if (a) {
      // ..
    } else {
      if (b) {
        // ..
      } else {
        // ..
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    else if 极为常见,能省掉一层代码缩进,所以很受青睐。但这只是我们自己发明的用法,切勿想当然地认为这些都属于 JavaScript 语法的范畴。

# 15. try..finally

  • 这里 return 42 先执行,并将 foo() 函数的返回值设置为 42。然后 try 执行完毕,接着执行 finally。最后 foo() 函数执行完毕,console.log(..) 显示返回值。
function foo() {
  try {
    return 42
  } finally {
    console.log('Hello')
  }
  console.log('never runs')
}
console.log(foo())
// Hello
// 42
1
2
3
4
5
6
7
8
9
10
11
  • finally 中的 return 会覆盖 try 和 catch 中 return 的返回值。

# 16. 混合环境 JavaScript

  • 由于浏览器演进的历史遗留问题,在创建带有 id 属性的 DOM 元素时也会创建同名的全局变量。
<div id="foo"></div>
1
if (typeof foo == 'undefined') {
  foo = 42 // 永远也不会运行
}
console.log(foo) // HTML元素
1
2
3
4

# 17. 异步控制台

  • 并没有什么规范或一组需求指定 console.* 方法族如何工作 —— 它们并不是 JavaScript 正式的一部分,而是由宿主环境添加到 JavaScript 中的。

  • 因此,不同的浏览器和 JavaScript 环境可以按照自己的意愿来实现,有时候这会引起混淆。尤其要提出的是,在某些条件下,某些浏览器的 console.log(..) 并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是 JavaScript)中,I/O 是非常低速的阻塞部分。所以,(从页面 /UI 的角度来说)浏览器在后台异步处理控制台 I/O 能够提高性能,这时用户甚至可能根本意识不到其发生。

var a = {
  index: 1,
}
// 然后
console.log(a) // ?? =>  { index: 2 }
// 再然后
a.index++
1
2
3
4
5
6
7

# 18. 并行线程与并发

  • 并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。

  • 两个或多个 “进程” 同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作 “进程” 级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。

# 19. 不是所有的引擎都类似

  • 过去把多个字符串值放在一个数组中,然后在数组上调用 join("") 来连接这些值比直接用 + 连接这些值要快。这一点的历史原因是微妙的,涉及字符串值在内存中如何存储和管理这样的内部实现细节。

# 20. 尾调用优化(你不知道的 JS(下)

  • 简单地说,尾调用就是一个出现在另一个函数 “结尾” 处的函数调用。这个调用结束后就没有其余事情要做了(除了可能要返回结果值)。
function foo(x) {
  return x
}
function bar(y) {
  return foo(y + 1) // 尾调用
}
function baz() {
  return 1 + bar(40) // 非尾调用
}
baz() // 42
1
2
3
4
5
6
7
8
9
10
以父之名
周杰伦.mp3