深入理解JavaScript系列(1)
本文是深入理解JavaScript系列的第一篇读文笔记,博客原文在这里。
内容简要
本文是汤姆大叔在《JavaScript Patterns》的基础上,可能参考了一些其他的文章,写成的一篇最佳实践Style文章。全文紧扣如何编写高质量JavaScript代码这一问题,通过罗列一系列points来阐述哪些是推荐做的,哪些是不推荐做的。
BACKBONE
书写可维护的代码
bug是应用程序的天敌,在现实世界中,没有应用程序是没有bug的。但是bug的修复成本往往是比较昂贵的。故,为了降低修复bug的成本,书写维护性高的代码是及其有意义的。
那么,可维护的代码应该包括哪些基本要素呢?
- 可读的
我们不推荐在代码中使用过多的hack style的技巧,以及过多的复杂判断或者其他逻辑等。因为这会让后续的维护者花费更多的时间去理解代码。 - 一致的
代码应该在空间和行为上具有一致性。 - 可预测的
可预测的含义比较广,比如,应该把相似的逻辑放在一起,让维护者可以预测你代码的组织结构。 - 看上去像是同一个人写的
这一点比较好理解。同一份代码中,所有的参与者都应该遵循同一份code style。 - 已记录
这里的已记录我个人猜测的含义应该是在适当的地方应该有明确的注释,并且在复杂逻辑出应该注明逻辑实现思想。
最小全局变量
JavaScript语言通过函数来管理作用域。在函数内部声明的变量只能在这个函数的内部使用,外部不可用。另一方面,全局变量指在函数外部声明的变量,或者未声明就直接使用的变量(我们一般不推荐这种做法,因为可能会导致各种意想不到的问题。)。
就一般而言,我们指的全局变量都是指浏览器环境,往往就是指window
这个对象。
全局变量的问题
JavaScript中全局变量导致的问题最常见的就是命名冲突。比如你先引入了JQuery
,然后又自己定义了一个全局变量$
,明显的,JQuery
的$
变量就被你覆盖了。
一般而言,我们推荐尽量少的使用全局变量,可以通过命名空间或者自执行函数来减少命名空间的使用。
全局变量还有另外一个常见的问题,就是不通过var
关键字声明的变量,都将隐式的转变成全局变量。所以我们推荐所有的变量都使用var
关键字进行声明。
另一个创建隐式全局变量的反例就是使用任务链进行部分var声明。如下代码,
function foo() {
var a = b = 0;
// ...
}
这里,变量b
将会被隐式转换为全局变量。此现象发生的原因在于JavaScript使用从右到左的赋值。
首先,是赋值表达式b = 0
,此情况下b
是未声明的。这个表达式的返回值是0
,然后这个0
就分配给了通过var
定义的局部变量a
。换句话说,相当于你输入了,
var a = (b = 0);
所以,当需要进行一次性进行多个变量赋值时,我们一般推荐如下的做法,
function foo() {
var a, b;
// ...
}
忘记var的副作用
隐式的全局变量和明确定义的全局变量间有些小的差异,就是能否通过delete
操作符让变量未定义的能力。
- 通过
var
创建的全局变量是不能被删除的。 - 无
var
创建的隐式全局变量是能被删除的。(被删除后,变量的值变为undefined
)
这点说明了什么问题呢?
在技术上,隐式全局变量并不是真正的全局变量,但他们是全局变量的属性。而属性是可以通过delete
操作符删除的,而变量是不可以的。
现在ES5的strict
模式下,未声明的变量工作时抛出一个错误。
ps:这点老实说,之前我也不知道。:(
单var形式
在函数顶部使用单var语句是比较有用的一种形式,其好处在于,
- 提供了一个单一的地方去寻找功能所需要的所有局部变量
- 防止变量在定义之前使用
- 帮助你记住声明的全局变量
- 减少代码量
- 利于压缩工具的压缩
单var
形式长得就像下面的这个样子,
function foo() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// more code...
}
这种单var
形式的变量声明好处多多,但是有一个不好的地方就是不利于调试。因为调试时,单步就直接把var
语句执行完毕了,这样你可能就看不到类似sum = a + b
这种运算表达式的细节了。
预解析
在JavaScript中,你可以在函数的任意位置进行var
语句声明,并且他们就好象是在函数顶部声明一样发挥作用。这种行为称为hoisting
(悬置/置顶解析/预解析)。
当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它是在var
声明前使用的。
ps:我本人以前就踩过这种坑!
让我们来看一个例子,
// 反例
myname = "global"; // 全局变量
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
在这个例子中,你可能会以为第一个alert弹出的是global
,第二个弹出loacl
。这种期许是可以理解的,因为在第一个alert的时候,myname
未声明,此时函数肯定很自然而然地看全局变量myname
,但是,实际上并不是这么工作的。第一个alert会弹 出undefined
是因为myname
被当做了函数的局部变量(尽管是之后声明的),所有的变量声明都被悬置到函数的顶部了。
因此,为了避免这种混乱,最好是预先声明你想使用的全部变量。
ps:大叔的解释已经够好了,我就直接引用了,没必要画蛇添足了。
其实,上面的代码就等同于下面,
myname = "global"; // global variable
function func() {
var myname; // 等同于 -> var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"
}
func();
for循环
在for循环中,你可以循环取得数组或是数组类似对象的值,譬如arguments
和HTMLCollection
对象。通常的循环形式如下,
// 次佳的循环
for (var i = 0; i < myarray.length; i++) {
// 使用myarray[i]做点什么
}
这种形式的循环的不足在于每次循环的时候数组的长度都要去获取下。这回降低你的代码,尤其当myarray
不是数组,而是一个array-like
对象的时候。
一般我们会采取缓存数组的长度这种方式来进行循环遍历,
for (var i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做点什么
}
这样,在这个循环过程中,你只检索了一次长度值。
for-in循环
for-in
循环应该用在非数组对象的遍历上,使用for-in
进行循环也被称为枚举。
for-in
循环有两点需要提一下,
- 尽量不要使用
for-in
去遍历数组对象 - 使用
hasOwnProperty
方法可以过滤掉来自原型上的属性和方法
不扩展内置原型
在许久之前,有一个流行的JavaScript类库叫做Prototype,他就是扩展了原生对象的原型(我不明确现在是不是还是这样的:(,因为我自己也没用过这个类库)。
不过,现在业内基本都已经达成共识,不推荐(或者不允许)扩展原生对象的原型。因为这在多人合作或者大型项目造成诸多问题。
如果你嫌弃原生对象没有提供足够的方法,推荐你使用下面两款工具类库,
如果上面的工具库还不能满足你,你可以自己实现需要工具方法,但是请记住,不要挂载在原生对象的原型上!。
避免隐式类型转换
JavaScript是一门弱语言编程语言,他有一个强大的功能就是隐式类型自动转换。这个功能有时间太强大了,会在你不知道的情况下进行类型转换,然后引起一些问题和混乱。
比如下面的例子,
var a = '11',
b = 11,
c = a + b;
console.log(c);
这里c
的结果将会是1111
,他是一个字符串。可能这种情况并不是你的本意。
另一方面,特别是在进行条件判断的时候,我们更推荐使用===
和!==
,而不是==
和!=
。
避免使用eval和with
我相信,大部分写JavaScript的程序员甚至很少接触到这两个东西,甚至都没听说过(哈哈,有点夸张了)。先简单说下eval
和with
的作用。
eval()
的作用是,接受一个字符串,将此字符串当作JavaScript代码来处理with(){}
的作用是,指定{}
内代码的this
指向
eval
在特定的情况下可能会比较有用一点,比如进行一些内库开发的时候,但是with
的使用真的很少,基本上都是不推荐使用的。
这里我简单的说一下使用eval
可能会出现的几个问题,
- 存在安全隐患
因为eval
将接受到的字符串当作JavaScript代码来处理。如果接受到的字符串本身存在安全问题(比如来自网络请求得到的),那么执行这条字符串可能造成安全风险。 - 污染当前作用域
eval
执行时,其内部使用的作用域是全局作用域。如果你在内部定义了一个变量与全局变量名一致,那么就会覆盖之前的全局变量。
使用parseInt()进行数值转换
parseInt
是原生提供一个用于解析数字的工具方法。他接受一个字符串,将字符串中数字解析成数值型,如果遇到非数字型字符串,则解析过程停止。该函数还可以接受第二个参数,表示数值的基数。
这里就简单的说一下使用parseInt
应该注意的几个方面,
- 尽量传入第二个参数
通过的用法中都省略了第二个基数参数,其实这是不应该的。比如,如果我传入的字符串为099
这种,而且没有传入基数参数,那么这个099
将会被当作八进制数来处理,这明显与我的本意相违背。
编码规范相关
大叔这里还提了一下编码规范相关的内容。关于code style这个东西,我个人的看法是遵循一般性原则,细节适时调整即可。
比如说我个人的code style,我大致参考的是Google JavaScript Style Guide,当然也不是完全100%的照搬,我也会做一些适当的调整。比如,google js style中说空格使用2个空格,但是我个人习惯使用4个空格。所以说,code style这东西不必太较真,就个人来说有个大致的参考即可;就公司团队来说,务必有一套人人都得遵守的规范,这是高质量、高可维护代码的基石之一。
这里,我仅罗列一下大叔提到的code style points,并不作具体说明,
- 缩进(Indentation)
- 花括号(Curly Braces)
- 花括号的位置(Opening Brace Location)
- 空格(White Space)
- 命名规范(Naming Conventions)
- 以大写字段写构造函数(Capitalizing Constructors)
- 分割单词(Separating Words)
- 其他命名形式(Other Naming Patterns)
- 注释(Writing Comments)
这里,打个小广告,我个人code style的repo。
总结
如何编写高质量的JavaScript代码?
ps:以下都是个人观点
一句话可以概括,把握语言本质,较真语言细节,辅以实践经验。
说的简单通俗点,就是
- 把《JavaScript权威指南》读薄
- 把《JavaScript语言精粹》读厚
- 再加上一些实践的经验