这是一篇读书笔记,对《javascript语言精粹》这本书的内容进行了摘抄整理,有兴趣的朋友可以看看原书,虽然是十年前的书,但是大部分内容都不过时,而且对js的发展影响很大。
在学习JS的过程中,我们除了了解到它的优点之外,也应该了解它的缺点,而且这一步应该是同步进行,这不冲突。知道了缺点,才能更有针对性的从一开始就避免养成不好的代码习惯,以及可以预测到下一步的发展趋势。所以是很有必要的。
最近重新读了《Javascript语言精粹》这本书,虽然叫精粹,但是书中缺单独总结了两章,分别较“糟粕”和“毒瘤”,而这两章在我看来,是这本书最重要的部分,因为一方面,这两章详细地描述了js(ES5)的缺点,为开发者踩了坑,另一方面,书里的缺点在之后的ES6中,也有一部分被完善,可见确实是言之确凿。
所以在这里,将书中【毒瘤】章节的内容记录下来,配上代码,与大家分享,希望能引以为戒,不会轻易犯错。
毒瘤
1.全局变量
全局变量可以被称为javascript里最为糟糕的特性,他的意思就某个变量在所有作用域中都可见。全局变量在微型程序中可能是非常有帮助,但随着程序越来越大,它们很快变得难以理解。可以被随意修改的变量使得程序变得非常复杂,降低了程序的可靠性。
全局变量也使得在独立程序中使用子程序变得更加困难,增加运行与调试的成本。
许多编程语言都有全局变量,例如,Java的public static
成员属性就是全局变量。而javascript的问题不仅在于它允许使用全局变量,而在于它依赖全局变量。javascript没有连接器(linker),所有的编译单元都载入一个公共全局对象中(window)。
所以,尽可能地避免下面这三种变量声明:
var foo = value
// 或者
window.foo = value
// 或者
foo = value
第三种方法最初只是为了方便初学者,使得变量在使用前不需要声明,但是却成为了一个常见的错误。
2.作用域
javascript的语法来源于C,但是在其他的C风格的语言中,一个代码块(花括号中的一组语句),会创造一个作用域,代码块里声明的变量在外部是无法获取的。javascript使用了这样的语法,却没有提供块级作用域:代码块里的变量是外部可见的。
所以区别于其他语言:变量哪里用哪里声明,javascript生命变量的最好的位置是函数的头部。
3. 自动插入分号
对于这一点,随着javascript的发展,现在已经可以在开发过程中不写分号,有的规范甚至要求开发者不写分号。因为现在的打包机制已经可以帮助开发者自动补全缺失的分号。我们下面讨论在当时的js环境中。
虽然我们可能没发现,但是javascript有一个自动修复机制,就是尝试补全分号,来修复有缺损的代码。但是js自身的补全机制有很多严重的问题,而且可能造成更严重的问题。如果插入了错误的分号,可能导致程序无法正确运行,如:
return
{
code: 200
}
这样的代码本质上是没问题的,但通过js的补全机制,不会给出任何警告,并很有可能就会在return
后补全分号,变为:
return;
{
code: 200
}
进而导致程序无法运行,所以,正确规范地书写代码块的换行是十分重要的。
4.保留字
我曾经差了很多网上的文章,关于obj.key
和obj['key']
的存在意义是什么,首先查了哪个效率更高,网上众说纷纭,但是最后得出的结论是,几乎一样。其实是一开始的方向就错了,真正应该在意的点是“关键字”。
每当我们其变量名的时候,我们都知道,避开关键字,比如不应该有let class = true
这样的代码出现,其实保留字是一样的。但是js的保留字是非常多的,而这其中的大部分,其实并没有在语言中出现
// 按字母排序
abstract
boolean break byte
case catch char class const continue
debugger default delete do double
else enum export extends
false final finally float for function
goto
if implements import in instanceof int interface
long
native new null
package private protected public
return
short static super switch synchronized
this throw throws transient true try typeof
var void volatile
while with
所以obj.key
和obj['key']
的区别就是,后者相较于前者,允许我们使用保留字来问键命名并获取对应的值,比如:
let method // ok
let class // 非法
object = {box: value} // ok
object = {class: value} // 非法
object = {'case': value} // ok
object.box = value // ok
object.case = value // 非法
object['case'] = value // ok
5.Unicode
js设计之初,预计Unicode只有65536个字符,但现在变成了一百多万个(至2009年)。
js的字符是16位的那足以覆盖原有的65536个字符,剩下的百万字符中的每一个都可以用一对字符来表示。Unicode把一对字符视为一个单一的字符,但是js却把一对字符视为两个字符,这导致在很多地方产生了冲突。
6.typeof
typeof
运算符返回一个用于识别其运算数类型的字符串,所以:
typeof 98.6 // 'number'
遗憾的是
typeof null // 'object',而非'null'
所以一般检测是否是null
,我们都会采用
同时,还有一个更大的问题就是检测对象的值,typeof
不能检测出null
与对象,但你可以像下面这么来判断,因为null
对应false
,而对象对应true
:
if(my_value && typeof my_value === 'object'){
// my_value是一个对象或者数组
}
当我们用typeof
判断正则的时候,也会根据浏览器等的影响,返回object
或者function
等不同的结果。
7.parseInt
这是一个把浮点数转换为整数的函数,它在遇到非数字时会停止解析,所以
parseInt('20') // 20
parseInt('20 days') // 20
parseInt('20 days 60') // 20
如果函数提示我们出现了额外的文本就好了,然而并没有。
如果该字符串的第一个数字是0,那么该字符串会基于八进制而不是十进制来解析,在八进制中8和9都不是数字,所以, parseInt('08')
和parseInt('09')
都会返回0
(注:经试验,此情况在不同浏览器实现不同,ie8及以前仍为0
,其余均为8
)
如果你想在ie8及以前的浏览器也实现返回8
的效果,建议你规范代码写法,写作parseInt('08', 10)
,强制十进制转换。
8.+
这个不需要多说,1 + '1'
输出为'11'
这样的情况我们已经见过太多次了,所以情尽可能的规范你的取值。
9.浮点数
二进制的浮点数不能正确地处理为小数,所以0.1 + 0.2不等于0.3,这是js中经常被报的bug,并且他遵循的是二进制浮点数运算标准(IEEE 754)(限于能力,并没有读过…)而有意导致的结果。这个标准在很多应用中实适用的,但它违背了一些基本的数学知识,幸运的是,浮点数中整数运算时精确的,所以小数表示出来的错误,可以通过指定精度来避免。
10.NaN
NaN
是我们上面说的IEEE 754中定义特殊数量值,它表示的不是一个数字,尽管下面的表达式返回的是true
typeof NaN === 'number' // true
该值可能会在试图把非数字形式的字符串转换为数字时产生,比如:
+ '0' // 0
+ 'today' // NaN
问题是,你可以对NaN
进行检测,但是检测的结果却让人惊讶,typeof
不能区分数字和NaN
,而且NaN
和自己比较的值也是混乱的
typeof 16 // 'number'
typeof NaN // 'number'
NaN === NaN // false
NaN !== NaN // true
所以js才提供了一个isNaN
函数用来专门解决和个问题(虽然还是非常不容易理解)
isNaN(NaN) // true
isNaN(0) // false
isNaN('today') // true
isNaN('0') // false
判断一个值可否用作数字的最佳方法是使用isFinite
函数,因为它会筛除掉NaN
和Infinity
,但是isFinite
方法会尝试把你的运算值转化为数字,如果事实上不是一个数字,又会出现各种问题,所以这里提供了一个更好的实现
let isNumber = value => {
return typeof value === 'number' && inFinite(value)
}
11.伪数组
javascript没有真正的数组,js中的数组其实本质上是object的一种实现,比如下图
我们可以说Array()
仅仅是一种特殊类型的Object()
,也就是说,Array()
实例基本上是拥有一些额外功能的Object()
实例。
这一点也可以在控制台中查看,Array只是Object的拓展
虽然javascript的数组确实非常容易使用,但是性能却非常糟糕。
typeof
运算符不能区分数组和对象,要判断一个值是否维数组,你还需要检查它的constructor
属性:
if(my_value && typeof my_value === 'object' && my_value.constructor === Array){
// my_value是一个数组
}
上面的检测对于在不同帧或窗口创建的数组将会给出false
(window
对象不一样了),所以下面的方法更好
if(Object.prototypr.toString.apply(my_value) === '[object Array]'){
// my_value是一个数组
}
arguments
数组不是一个数组,他只是一个有着length
成员属性的对象。如下面例子所示 arguments不是普通的array
let a = function () {
let b = Object.prototype.toString.apply(arguments)
console.log(b)
}
a() // 输出[object Arguments]
let aa = function () {
let c = []
let b = Object.prototype.toString.apply(c)
console.log(b)
}
aa() // 输出[object Array]
现在我们也可以使用
instanceof
和isArray
来进行数组的判断,但是对于低版本浏览器有时还是无能为力
12.假值
js拥有大量的假值
值 | 类型 |
---|---|
0 | Number |
NaN | Number |
‘’ | String |
false | Boolean |
null | Object |
undefined | Undefined |
它们都是假值,但是它们是不可以互相替换的,例如想确定一个对象是否缺少一个属性。这是错误的
value = myObject[name]
if(value == null){
alert(name + ' not found.')
}
undefined
是缺少的属性的值,但是这里我们用null判断,所以是失效的,它使用了会强制转换类型的==
而不是更可靠的===
,有时候两个错误会被抵消,有时候则不会。
而且注意,undefined
和NaN
不是常量,而是全局变量,我们是可以改变值的,但是,千万不要这么做。(注:经试验,IE8及以前的浏览器会出现此问题)
13.hasOwnProperty
我们可以使用hasOwnProperty
作为一个过滤器去避开for in
的隐患。遗憾的是hasOwnProperty
一个方法,而不是一个运算符,所以有着被替代的风险。
14.对象
js的对象永远不可能是真的空对象,因为它可以从原型链中取得成员属性,有时候会有问题。
js的默认对象不是空对象,当原型链可能对我们产生影响时,使用Object.create()
创建纯净的对象。
同时这也是我们所说的避免使用new来创建对象,而建议使用字面量或者Object.create()
的原因所在。
总结
这是一篇读书笔记,对书中一部分内容进行粗暴的摘抄整理以及添加了部分的个人总结,希望可以提醒自己,避开陷阱,去完成质量更高的代码。