你不知道的 JS(中)
# 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)
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
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) 假值
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
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
2
3
4
5
6
7
8
9
# 10. 字符串和数字之间的隐式强制类型转换
[] + {} // '[object Object]',相当于:'' + '[object Object]'
{} + [] // 0,第一个 {..} 会被认为是区块语句而不是对象字面量,相当于:+[]
2
3
# 11. 相等比较
- 布尔值会先转换为数字再比较。
7 == true // false
7 == false // false
2
- null 与 undefined '==' 比较相等,与其他假值都不相等。
null == undefined // true
null == '' || null == 0 || null == false // false
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,与自身不等
2
3
4
- 极端情况
[] == ![] // true,转换过程:[] -> '' -> 0,![] -> false -> 0
'' == [null] // true
0 == '\n' || 0 == ' ' // true
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,根据规则为 {} < {} 比较结果的反转
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
2
3
4
5
- 可以使用万恶的 eval(..) (又读作“evil”)来获得结果值。
var a, b
a = eval('if (true) { b = 4 + 38; }')
a // 42
2
3
# 13. 表达式的副作用
++a++ 会产生 ReferenceError 错误,因为运算符需要将产生的副作用赋值给一个变量。以 ++a++ 为例,它首先执行 a++ (根据运算符优先级),返回 42,然后执行 ++42,这时会产生 ReferenceError 错误,因为 ++ 无法直接在 42 这样的值上产生副作用。
a = 42 中的 = 运算符看起来没有副作用,实际上它的结果值是 42,它的副作用是将 42 赋值给 a。
var a
a = 42 // 42
a // 42
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
21break 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
18JSON 被普遍认为是 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
9else 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
2
3
4
5
6
7
8
9
10
11
- finally 中的 return 会覆盖 try 和 catch 中 return 的返回值。
# 16. 混合环境 JavaScript
- 由于浏览器演进的历史遗留问题,在创建带有 id 属性的 DOM 元素时也会创建同名的全局变量。
<div id="foo"></div>
if (typeof foo == 'undefined') {
foo = 42 // 永远也不会运行
}
console.log(foo) // HTML元素
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++
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
2
3
4
5
6
7
8
9
10