JavaScript语言特性
JavaScript的C风格的语法,包括大括号和复杂的for语句,让它看起来像是一个普通的过程式语言。这其实是一个误导,因为JavaScript和函数式语言如Lisp和Scheme有更多的共同之处。它用数组代替了列表,用对象代替了属性列表。函数是第一公民,而且有闭包。你不需要平衡那些括号就可以用lambda算子。
但是,JavaScript早期实现错误百出,这对该语言带来了很恶劣的影响。更糟糕的是,这些实现还被嵌入到错误百出的浏览器中。因此,在JavaScript中踩坑是非常常见的现象,本文将通过一系列示例来分析这些常见陷阱。
测试环境
下面所有的JavaScript测试基于ECMA 262标准在浏览器环境中的行为表现
常见JavaScript陷阱
1. 数组方法中的回调函数
示例一:map与parseInt的配合
输出结果:
实现原理:
Array.prototype.map(callback(currentValue, index, array)[,thisArg])方法中,回调函数会自动接收三个参数。而parseInt(string, radix)接受两个参数。当这两个函数配合时,实际执行过程如下:
示例二:replace与函数参数
输出结果:
实现原理:
String.prototype.replace(regexp|substr, newSubStr|function)方法的第二个参数为函数时,该函数会接收以下参数:
| 参数名 | 含义 |
|---|---|
| match | 匹配的子串 |
| p1,p2... | 正则表达式分组捕获的结果 |
| offset | 匹配到的子字符串在原字符串中的偏移量 |
| string | 被匹配的原字符串 |
2. JavaScript中的null陷阱
输出结果:
实现原理:
⚠️ JavaScript中typeof null返回'object'是一个历史遗留bug。在JavaScript最初实现中,值由一个类型标签和实际数据值表示,对象的类型标签是0,而null是空指针(通常是0x00),因此null的类型标签也变成了0,导致typeof null错误返回'object'。
这个问题曾计划在ECMAScript 6中修复(返回'null'),但提议被否决了。
3. 数组方法的边界情况
输出结果:
实现原理:
Array.prototype.reduce()方法在没有提供初始值且数组为空时会抛出TypeError。这是因为reduce需要至少一个元素作为初始累加器值,如果数组为空且没有提供initialValue,就会出错。
4. 运算符优先级问题
输出结果:
实现原理:
这个问题涉及到运算符优先级。加法运算符+优先级高于三元运算符?:,所以表达式会被解析为:
5. 变量提升机制
示例一:
输出结果:
实现原理: JavaScript中的变量提升(hoisting)会将变量声明(不包括赋值)提升到函数作用域顶部。因此,上面代码等价于:
示例二:
输出结果:
实现原理: 函数参数可以视为在函数体顶部的变量声明。上面的代码等价于:
6. 数值表示限制
输出结果: 无限循环,不会输出结果。
实现原理:
⚠️ JavaScript中的Number类型是64位浮点数,能精确表示的最大整数是2^53,即9007199254740992。超过这个值后,JavaScript无法精确表示连续整数。
当i = Math.pow(2, 53)时,i + 1 === i会返回true,导致循环条件一直为true,形成死循环。
7. 稀疏数组处理
输出结果:
实现原理:
JavaScript数组可以是稀疏的,即包含"空位"。Array.prototype.filter()等迭代方法会跳过数组中的"空位",但会处理值为undefined的元素。
在示例中,ary[10] = undefined创建了一个显式值为undefined的元素,而索引3到9是"空位"。filter方法跳过了空位,但处理了索引10的undefined值。
8. 浮点数计算精度问题
输出结果:
实现原理: ⚠️ JavaScript使用IEEE 754双精度浮点数表示数字,某些小数无法精确表示。浮点数计算可能会产生误差。
在二进制中,0.1和0.2都是无限循环小数,所以:
0.2 - 0.1的结果接近但不一定等于0.10.8 - 0.6的结果接近但不一定等于0.2
出乎意料的是第一个比较返回true,而第二个返回false,这与IEEE 754标准下的舍入规则有关。
处理金融计算时,应该使用专门的库或将数值转换为整数后再计算,以避免精度问题。
9. Switch语句的比较机制
输出结果:
实现原理:
switch语句使用严格相等(===)进行比较。
new String('A')创建了一个String对象,结果为String {'A'},它不严格等于字符串'A'String('A')是一个函数调用,返回原始字符串'A',严格等于'A'
这解释了为什么第一个调用匹配了default,而第二个调用匹配了case 'A'。
10. 取模运算符的特性
输出结果:
实现原理: JavaScript中模运算的结果符号与被除数相同:
-9 % 2 = -1,不等于1,所以isOdd(-9)返回falseInfinity % 2 = NaN,所以isOdd(Infinity)和isEven(Infinity)都返回false
对于字符串'13',在数学运算中会自动转换为数字13。
11. 原型链的特性
输出结果:
实现原理:
在JavaScript中,Array.prototype实际上是一个数组对象。这是JavaScript内置对象的一个特殊设计,等价于Array.prototype = []。因此,Array.isArray(Array.prototype)返回true。
12. 比较运算的隐式转换
输出结果:
实现原理:
- 非空数组
[0]转为布尔值是true,但在==比较时,数组会转为字符串'0',而true转为1,'0' == 1为false - 两个数组是不同的对象引用,
==比较引用地址,所以结果是false [[[2]]]在比较时会先转为字符串'2',然后与数字2比较,'2' == 2为true
13. arguments对象的特性
输出结果:
实现原理:
在非严格模式下,函数参数和arguments对象是相互关联的。当修改arguments[0]时,对应的参数a也会改变:
⚠️ 注意:在严格模式('use strict')下,arguments对象不会与参数相互影响。
14. Number的toString方法调用
输出结果:
实现原理:
3.toString()语法错误,因为JavaScript解析器会将第一个点视为小数点3..toString()正确,第一个点被解释为小数点,第二个点调用toString方法3...toString()错误,JavaScript不允许连续多个点
最佳实践: 为避免这类混淆,可以使用以下写法:
15. 原型链访问方式
输出结果:
实现原理:
- 普通对象没有
prototype属性,只有函数对象才有。所以a.prototype === undefined Object.getPrototypeOf(a)返回对象a的原型,也就是Object.prototype- 函数
f的prototype属性是为它创建的实例对象设置原型用的 Object.getPrototypeOf(f)返回函数f本身的原型,即Function.prototype
16. 数组的尾部逗号
输出结果:
实现原理:
JavaScript数组字面量中的尾部逗号会创建稀疏数组,[,,,]创建了一个有3个"空位"的数组。join()方法会将空位视为空字符串。
17. Function.length特性
输出结果:
实现原理:
Function.length返回1,表示Function构造函数期望的参数数量为1new Function().length返回0,表示通过new Function()创建的默认函数不接受任何参数
18. Math.min和Math.max的边界值
输出结果:
实现原理:
Math.min()在没有参数时返回InfinityMath.max()在没有参数时返回-Infinity
因此,Infinity < -Infinity是false。