forked from jayli/jayli.github.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchapter2.html
More file actions
1154 lines (828 loc) · 55.6 KB
/
chapter2.html
File metadata and controls
1154 lines (828 loc) · 55.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" media="screen" href="http://a.tbcdn.cn/app/dp/s/screen.css" />
<style>
#bd {margin-left:20px;margin-right:20px;}
</style>
<title>wd-gallery - markdown</title>
</head>
<body>
<div id="bd">
<p><a name="a1"></a></p>
<h1>第二章 高质量JavaScript基本要点</h1>
<p>本章将对一些实质内容展开讨论,这些内容包括最佳实践、模式和编写高质量JavaScript代码的习惯,比如避免全局变量、使用单var声明、循环中的length预缓存、遵守编码约定等等。本章还包括一些非必要的编程习惯,但更多的关注点将放在总体的代码创建过程上,包括撰写API文档、组织相互评审以及使用JSLint。这些习惯和最佳实践可以帮助你写出更好的、更易读的和可维护的代码,当几个月后或数年后再重读你的代码时,你就会深有体会了。</p>
<p><a name="a2"></a></p>
<h2>编写可维护的代码</h2>
<p>修复软件bug成本很高,而且随着时间的推移,它们造成的损失也越来越大,特别是在已经打包发布了的软件发现了bug的时候。当然最好是发现bug立刻解决掉,但前提是你对你的代码依然很熟悉,否则当你转身投入到另外一个项目的开发中后,根本不记得当初代码的模样了。过了一段时间后你再去阅读当初的代码你需要:</p>
<ul>
<li>时间来重新学习并理解问题</li>
<li>时间去理解问题相关的代码</li>
</ul>
<p>对大型项目或者公司来说还有一个不得不考虑的问题,就是解决这个bug的人和制造这个bug的人往往不是同一个人。因此减少理解代码所需的时间成本就显得非常重要,不管是隔了很长时间重读自己的代码还是阅读团队内其他人的代码。这对于公司的利益底线和工程师的幸福指数同样重要,因为每个人都宁愿去开发新的项目而不愿花很多时间和精力去维护旧代码。</p>
<p>另外一个软件开发中的普遍现象是,在读代码上花的时间要远远超过写代码的时间。常常当你专注于某个问题的时候,你会坐下来用一下午的时间产出大量的代码。当时的场景下代码是可以正常运行的,但当应用趋于成熟,会有很多因素促使你重读代码、改进代码或对代码做微调。比如:</p>
<ul>
<li>发现了bug</li>
<li>需要给应用添加新需求</li>
<li>需要将应用迁移到新的平台中运行(比如当市场中出现了新的浏览器时)</li>
<li>代码重构</li>
<li>由于架构更改或者更换另一种语言导致代码重写</li>
</ul>
<p>这些不确定因素带来的后果是,少数人花几小时写的代码需要很多人花几个星期去阅读它。因此,创建可维护的代码对于一个成功的应用来说至关重要。</p>
<p>可维护的代码意味着代码是:</p>
<ul>
<li>可读的</li>
<li>一致的</li>
<li>可预测的</li>
<li>看起来像是同一个人写的</li>
<li>有文档的</li>
</ul>
<p>本章接下来的部分会对这几点深入讲解。</p>
<p><a name="a3"></a></p>
<h2>减少全局对象</h2>
<p>JavaScript 使用函数来管理作用域,在一个函数内定义的变量称作“局部变量”,局部变量在函数外部是不可见的。另一方面,“全局变量”是不在任何函数体内部声明的变量,或者是直接使用而未明的变量。</p>
<p>每一个JavaScript运行环境都有一个“全局对象”,不在任何函数体内使用this就可以获得对这个全局对象的引用。你所创建的每一个全局变量都是这个全局对象的属性。为了方便起见,浏览器都会额外提供一个全局对象的属性window,(常常)用以指向全局对象本身。下面的示例代码中展示了如何在浏览器中创建或访问全局变量:</p>
<pre><code>myglobal = "hello"; // antipattern
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"
</code></pre>
<p><a name="a4"></a></p>
<h3>全局对象带来的困扰</h3>
<p>全局变量的问题是,它们在JavaScript代码执行期间或者整个web页面中始终是可见的。它们存在于同一个命名空间中,因此命名冲突的情况时有发生,毕竟在应用程序的不同模块中,经常会出于某种目的定义相同的全局变量。</p>
<p>同样,常常网页中所嵌入的代码并不是这个网页的开发者所写,比如:</p>
<ul>
<li>网页中使用了第三方的JavaScript库</li>
<li>网页中使用了广告代码</li>
<li>网页中使用了用以分析流量和点击率的第三方统计代码</li>
<li>网页中使用了很多组件,挂件和按钮等等</li>
</ul>
<p>假设某一段第三方提供的脚本定义了一个全局变量result。随后你在自己写的某个函数中也定义了一个全局变量result。这时,第二个变量就会覆盖第一个,这时就会导致第三方脚本停止工作。</p>
<p>因此,为了让你的脚本和这个页面中的其他脚本和谐相处,要尽可能少的使用全局变量,这一点非常重要。本书随后的章节中会讲到一些减少全局变量的技巧和策略,比如使用命名空间或者立即执行的匿名函数等,但减少全局变量最有效的方法是坚持使用var来声明变量。</p>
<p>由于JavaScript的特点,我们经常有意无意的创建全局变量,毕竟在JavaScript中创建全局变量实在太简单了。首先,你可以不声明而直接使用变量,再者,JavaScirpt中具有“隐式全局对象”的概念,也就是说任何不通过var声明(译注:在JavaScript1.7及以后的版本中,可以通过let来声明块级作用域的变量)的变量都会成为全局对象的一个属性(可以把它们当作全局变量)。看一下下面这段代码:</p>
<pre><code>function sum(x, y) {
// antipattern: implied global
result = x + y;
return result;
}
</code></pre>
<p>这段代码中,我们直接使用了result而没有事先声明它。这段代码是能够正常工作的,但在调用这个方法之后,会产生一个全局变量result,这会带来其他问题。</p>
<p>解决办法是,总是使用var来声明变量,下面代码就是改进了的sum()函数:</p>
<pre><code>function sum(x, y) {
var result = x + y;
return result;
}
</code></pre>
<p>这里我们要注意一种反模式,就是在var声明中通过链式赋值的方法创建全局变量。在下面这个代码片段中,a是局部变量,但b是全局变量,而作者的意图显然不是如此:</p>
<pre><code>// antipattern, do not use
function foo() {
var a = b = 0;
// ...
}
</code></pre>
<p>为什么会这样?因为这里的计算顺序是从右至左的。首先计算表达式b=0,这里的b是未声明的,这个表达式的值是0,然后通过var创建了局部变量a,并赋值为0。换言之,可以等价的将代码写成这样:</p>
<pre><code>var a = (b = 0);
</code></pre>
<p>如果变量b已经被声明,这种链式赋值的写法是ok的,不会意外的创建全局变量,比如:</p>
<pre><code>function foo() {
var a, b;
// ...
a = b = 0; // both local
}
</code></pre>
<blockquote>
<p>避免使用全局变量的另一个原因是出于可移植性考虑的,如果你希望将你的代码运行于不同的平台环境(宿主),使用全局变量则非常危险。很有可能你无意间创建的某个全局变量在当前的平台环境中是不存在的,你认为可以安全的使用,而在其他的环境中却是存在的。</p>
</blockquote>
<p><a name="a5"></a></p>
<h3>忘记var时的副作用</h3>
<p>隐式的全局变量和显式定义的全局变量之间有着细微的差别,差别在于通过delete来删除它们的时候表现不一致。</p>
<ul>
<li>通过var创建的全局变量(在任何函数体之外创建的变量)不能被删除。</li>
<li>没有用var创建的隐式全局变量(不考虑函数内的情况)可以被删除。</li>
</ul>
<p>也就是说,隐式全局变量并不算是真正的变量,但他们是全局对象的属性成员。属性是可以通过delete运算符删除的,而变量不可以被删除:</p>
<pre><code>// define three globals
var global_var = 1;
global_novar = 2; // antipattern
(function () {
global_fromfunc = 3; // antipattern
}());
// attempt to delete
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true
// test the deletion
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"
</code></pre>
<p>在ES5严格模式中,给未声明的变量赋值会报错(比如这段代码中提到的两个反模式)。</p>
<p><a name="a6"></a></p>
<h3>访问全局对象</h3>
<p>在浏览器中,我们可以随时随地通过window属性来访问全局对象(除非你定义了一个名叫window的局部变量)。但换一个运行环境这个方便的window可能就换成了别的名字(甚至根本就被禁止访问全局对象了)。如果不想通过这种写死window的方式来得到全局变量,有一个办法,你可以在任意层次嵌套的函数作用域内执行:</p>
<pre><code>var global = (function () {
return this;
}());
</code></pre>
<p>这种方式总是可以得到全局对象,因为在被当作函数执行的函数体内(而不是被当作构造函数执行的函数体内),this总是指向全局对象。但这种情况在ECMAScript5的严格模式中行不通,因此在严格模式中你不得不寻求其他的替代方案。比如,如果你在开发一个库,你会将你的代码包装在一个立即执行的匿名函数中(在第四章会讲到),然后从全局作用域中给这个匿名函数传入一个指向this的参数。</p>
<p><a name="a7"></a></p>
<h3>单 var 模式</h3>
<p>在函数的顶部使用一个单独的var语句是非常推荐的一种模式,它有如下一些好处:</p>
<ul>
<li>在同一个位置可以查找到函数所需的所有变量</li>
<li>避免当在变量声明之前使用这个变量时产生的逻辑错误(参照下一小节“声明提前:分散的 var 带来的问题”)</li>
<li>提醒你不要忘记声明变量,顺便减少潜在的全局变量</li>
<li>代码量更少(输入更少且更易做代码优化)</li>
</ul>
<p>单var模式看起来像这样:</p>
<pre><code>function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body...
}
</code></pre>
<p>你可以使用一个var语句来声明多个变量,变量之间用逗号分隔。也可以在这个语句中加入变量的初始化,这是一个非常好的实践。这种方式可以避免逻辑错误(所有未初始化的变量都被声明了,且值为undefined)并增加了代码的可读性。过段时间后再看这段代码,你会体会到声明不同类型变量的惯用名称,比如,你一眼就可看出某个变量是对象还是整数。</p>
<p>你可以在声明变量时多做一些额外的工作,比如在这个例子中就写了sum=a+b这种代码。另一个例子就是当代码中用到对DOM元素时,你可以把对DOM的引用赋值给一些变量,这一步就可以放在一个单独的声明语句中,比如下面这段代码:</p>
<pre><code>function updateElement() {
var el = document.getElementById("result"),
style = el.style;
// do something with el and style...
}
</code></pre>
<p><a name="a8"></a></p>
<h3>声明提前:分散的 var 带来的问题</h3>
<p>JavaScript 中是允许在函数的任意地方写任意多个var语句的,其实相当于在函数体顶部声明变量,这种现象被称为“变量提前”,当你在声明之前使用这个变量时,可能会造成逻辑错误。对于JavaScript来说,一旦在某个作用域(同一个函数内)里声明了一个变量,这个变量在整个作用域内都是存在的,包括在var声明语句之前。看一下这个例子:</p>
<pre><code>// antipattern
myname = "global"; // global variable
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
</code></pre>
<p>这个例子中,你可能期望第一个alert()弹出“global”,第二个alert()弹出“local”。这种结果看起来是合乎常理的,因为在第一个alert执行时,myname还没有声明,这时就应该“寻找”全局变量中的myname。但实际情况并不是这样,第一个alert弹出“undefined”,因为myname已经在函数内有声明了(尽管声明语句在后面)。所有的变量声明都提前到了函数的顶部。因此,为了避免类似带有“歧义”的程序逻辑,最好在使用之前一起声明它们。</p>
<p>上一个代码片段等价于下面这个代码片段:</p>
<pre><code>myname = "global"; // global variable
function func() {
var myname; // same as -> var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"
}
func();
</code></pre>
<blockquote>
<p>这里有必要对“变量提前”作进一步补充,实际上从JavaScript引擎的工作机制上看,这个过程稍微有点复杂。代码处理经过了两个阶段,第一阶段是创建变量、函数和参数,这一步是预编译的过程,它会扫描整段代码的上下文。第二阶段是代码的运行,这一阶段将创建函数表达式和一些非法的标识符(未声明的变量)。从实用性角度来讲,我们更愿意将这两个阶段归成一个概念“变量提前”,尽管这个概念并没有在ECMAScript标准中定义,但我们常常用它来解释预编译的行为过程。</p>
</blockquote>
<p><a name="a9"></a></p>
<h2>for 循环</h2>
<p>在for循环中,可以对数组或类似数组的对象(比如arguments和HTMLCollection对象)作遍历,最普通的for循环模式形如:</p>
<pre><code>// sub-optimal loop
for (var i = 0; i < myarray.length; i++) {
// do something with myarray[i]
}
</code></pre>
<p>这种模式的问题是,每次遍历都会访问数组的length属性。这降低了代码运行效率,特别是当myarray并不是一个数组而是一个HTMLCollection对象的时候。</p>
<p>HTMLCollection是由DOM方法返回的对象,比如:</p>
<ul>
<li>document.getElementsByName()</li>
<li>document.getElementsByClassName()</li>
<li>document.getElementsByTagName()</li>
</ul>
<p>还有很多其他的HTMLCollection,这些对象是在DOM标准之前就已经在用了,这些HTMLCollection主要包括:</p>
<p><strong>document.images</strong></p>
<p>页面中所有的IMG元素</p>
<p><strong>document.links</strong></p>
<p>页面中所有的A元素</p>
<p><strong>document.forms</strong></p>
<p>页面中所有的表单</p>
<p><strong>document.forms[0].elements</strong></p>
<p>页面中第一个表单的所有字段</p>
<p>这些对象的问题在于,它们均是指向文档(HTML页面)中的活动对象。也就是说每次通过它们访问集合的length时,总是会去查询DOM,而DOM操作则是很耗资源的。</p>
<p>更好的办法是为for循环缓存住要遍历的数组的长度,比如下面这段代码:</p>
<pre><code>for (var i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}
</code></pre>
<p>通过这种方法只需要访问DOM节点一次以获得length,在整个循环过程中就都可以使用它。</p>
<p>不管在什么浏览器中,在遍历HTMLCollection时缓存length都可以让程序执行的更快,可以提速两倍(Safari3)到一百九十倍(IE7)不等。更多细节可以参照Nicholas Zakas的《高性能JavaScript》,这本书也是由O'Reilly出版。</p>
<p>需要注意的是,当你在循环过程中需要修改这个元素集合(比如增加DOM元素)时,你更希望更新length而不是更新常量。</p>
<p>遵照单var模式,你可以将var提到循环的外部,比如:</p>
<pre><code>function looper() {
var i = 0,
max,
myarray = [];
// ...
for (i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}
}
</code></pre>
<p>这种模式带来的好处就是提高了代码的一致性,因为你越来越依赖这种单var模式。缺点就是在重构代码的时候不能直接复制粘贴一个循环体,比如,你正在将某个循环从一个函数拷贝至另外一个函数中,必须确保i和max也拷贝至新函数里,并且需要从旧函数中将这些没用的变量删除掉。</p>
<p>最后一个需要对循环做出调整的地方是将i++替换成为下面两者之一:</p>
<pre><code>i = i + 1
i += 1
</code></pre>
<p>JSLint提示你这样做,是因为++和--实际上降低了代码的可读性,如果你觉得无所谓,可以将JSLint的plusplus选项设为false(默认为true),本书所介绍的最后一个模式用到了: i += 1。</p>
<p>关于这种for模式还有两种变化的形式,做了少量改进,原因有二:</p>
<ul>
<li>减少一个变量(没有max)</li>
<li>减量循环至0,这种方式速度更快,因为和零比较要比和非零数字或数组长度比较要高效的多</li>
</ul>
<p>第一种变化形式是:</p>
<pre><code>var i, myarray = [];
for (i = myarray.length; i--;) {
// do something with myarray[i]
}
</code></pre>
<p>第二种变化形式用到了while循环:</p>
<pre><code>var myarray = [],
i = myarray.length;
while (i--) {
// do something with myarray[i]
}
</code></pre>
<p>这些小改进只体现在性能上,此外,JSLint不推荐使用i--。</p>
<p><a name="a10"></a></p>
<h2>for-in 循环</h2>
<p>for-in 循环用于对非数组对象作遍历。通过for-in进行循环也被称作“枚举”。</p>
<p>从技术角度讲,for-in循环同样可以用于数组(JavaScript中数组即是对象),但不推荐这样做。当使用自定义函数扩充了数组对象时,这时更容易产生逻辑错误。另外,for-in循环中属性的遍历顺序是不固定的,所以最好数组使用普通的for循环,对象使用for-in循环。</p>
<p>可以使用对象的hasOwnProperty()方法将从原型链中继承来的属性过滤掉,这一点非常重要。看一下这段代码:</p>
<pre><code>// the object
var man = {
hands: 2,
legs: 2,
heads: 1
};
// somewhere else in the code
// a method was added to all objects
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function () {};
}
</code></pre>
<p>在这段例子中,我们定义了一个名叫man的对象直接量。在代码中的某个地方(可以是man定义之前也可以是之后),给Object的原型中增加了一个方法clone()。原型链是实时的,这意味着所有的对象都可以访问到这个新方法。要想在枚举man的时候避免枚举出clone()方法,则需要调用hasOwnProperty()来对原型属性进行过滤。如果不做过滤,clone()也会被遍历到,而这不是我们所希望的:</p>
<pre><code>// 1.
// for-in loop
for (var i in man) {
if (man.hasOwnProperty(i)) { // filter
console.log(i, ":", man[i]);
}
}
/*
result in the console
hands : 2
legs : 2
heads : 1
*/
// 2.
// antipattern:
// for-in loop without checking hasOwnProperty()
for (var i in man) {
console.log(i, ":", man[i]);
}
/*
result in the console
hands : 2
legs : 2
heads : 1
clone: function()
*/
</code></pre>
<p>另外一种的写法是通过Object.prototype直接调用hasOwnProperty()方法,像这样:</p>
<pre><code>for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // filter
console.log(i, ":", man[i]);
}
}
</code></pre>
<p>这种做法的好处是,当man对象中重新定义了hasOwnProperty方法时,可以避免调用时的命名冲突(译注:明确指定调用的是Object.prototype上的方法而不是实例对象中的方法),这种做法同样可以避免冗长的属性查找过程(译注:这种查找过程多是在原型链上进行查找),一直查找到Object中的方法,你可以定义一个变量来“缓存”住它(译注:这里所指的是缓存住Object.prototype.hasOwnProperty):</p>
<pre><code>var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // filter
console.log(i, ":", man[i]);
}
}
</code></pre>
<blockquote>
<p>严格说来,省略hasOwnProperty()并不是一个错误。根据具体的任务以及你对代码的自信程度,你可以省略掉它以提高一些程序执行效率。但当你对当前要遍历的对象不确定的时候,添加hasOwnProperty()则更加保险些。</p>
</blockquote>
<p>这里提到一种格式上的变化写法(这种写法无法通过JSLint检查),这种写法在for循环所在的行加入了if判断条件,他的好处是能让循环语句读起来更完整和通顺(“如果元素包含属性X,则拿X做点什么”):</p>
<pre><code>// Warning: doesn't pass JSLint
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // filter
console.log(i, ":", man[i]);
}
</code></pre>
<p><a name="a11"></a></p>
<h2>(不)扩充内置原型</h2>
<p>我们可以扩充构造函数的prototype属性,这是一种非常强大的特性,用来为构造函数增加功能,但有时这个功能强大到超过我们的掌控。</p>
<p>给内置构造函数比如Object()、Array()、和Function()扩充原型看起来非常诱人,但这种做法严重降低了代码的可维护性,因为它让你的代码变得难以预测。对于那些基于你的代码做开发的开发者来说,他们更希望使用原生的JavaScript方法来保持工作的连续性,而不是使用你所添加的方法(译注:因为原生的方法更可靠,而你写的方法可能会有bug)。</p>
<p>另外,如果将属性添加至原型中,很可能导致在那些不使用hasOwnProperty()做检测的循环中将原型上的属性遍历出来,这会造成混乱。</p>
<p>因此,不扩充内置对象的原型是最好的,你也可以自己定义一个规则,仅当下列条件满足时做例外考虑:</p>
<ol>
<li>未来的ECMAScript版本的JavaScirpt会将你实现的方法添加为内置方法。比如,你可以实现ECMAScript5定义的一些方法,一直等到浏览器升级至支持ES5。这样,你只是提前定义了这些有用的方法。</li>
<li>如果你发现你自定义的方法已经不存在,要么已经在代码其他地方实现了,要么是浏览器的JavaScript引擎已经内置实现了。</li>
<li>你所做的扩充附带充分的文档说明,且和团队其他成员做了沟通。</li>
</ol>
<p>如果你遇到这三种情况之一,你可以给内置原型添加自定义方法,写法如下:</p>
<pre><code>if (typeof Object.protoype.myMethod !== "function") {
Object.protoype.myMethod = function () {
// implementation...
};
}
</code></pre>
<p><a name="a12"></a></p>
<h2>switch 模式</h2>
<p>你可以通过下面这种模式的写法来增强switch语句的可读性和健壮性:</p>
<pre><code>var inspect_me = 0,
result = '';
switch (inspect_me) {
case 0:
result = "zero";
break;
case 1:
result = "one";
break;
default:
result = "unknown";
}
</code></pre>
<p>这个简单的例子所遵循的风格约定如下:</p>
<ul>
<li>每个case和switch对齐(这里不考虑花括号相关的缩进规则)</li>
<li>每个case中的代码整齐缩进</li>
<li>每个case都以break作为结束</li>
<li>避免连续执行多个case语句块(当省略break时会发生),如果你坚持认为连续执行多case语句块是最好的方法,请务必补充文档说明,对于其他人来说,这种情况看起来是错误的。</li>
<li>以default结束整个switch,以确保即便是在找不到匹配项时也会有正常的结果,</li>
</ul>
<p><a name="a13"></a></p>
<h2>避免隐式类型转换</h2>
<p>在JavaScript的比较操作中会有一些隐式的数据类型转换。比如诸如false == 0或""==0之类的比较都返回true。</p>
<p>为了避免隐式类型转换造对程序造成干扰,推荐使用===和!===运算符,它们较除了比较值还会比较类型。</p>
<pre><code>var zero = 0;
if (zero === false) {
// not executing because zero is 0, not false
}
// antipattern
if (zero == false) {
// this block is executed...
}
</code></pre>
<p>另外一种观点认为当==够用的时候就不必多余的使用===。比如,当你知道typeof的返回值是一个字符串,就不必使用全等运算符。但JSLint却要求使用全等运算符,这当然会提高代码风格的一致性,并减少了阅读代码时的思考(“这里使用==是故意的还是无意的?”)。</p>
<p><a name="a14"></a></p>
<h3>避免使用eval()</h3>
<p>当你想使用eval()的时候,不要忘了那句话“eval()是魔鬼”。这个函数的参数是一个字符串,它可以执行任意字符串。如果事先知道要执行的代码是有问题的(在运行之前),则没有理由使用eval()。如果需要在运行时动态生成执行代码,往往都会有更佳的方式达到同样的目的,而非一定要使用eval()。例如,访问动态属性时可以使用方括号:</p>
<pre><code>// antipattern
var property = "name";
alert(eval("obj." + property));
// preferred
var property = "name";
alert(obj[property]);
</code></pre>
<p>eval()同样有安全隐患,因为你需要运行一些容易被干扰的代码(比如运行一段来自于网络的代码)。在处理Ajax请求所返回的JSON数据时会常遇到这种情况,使用eval()是一种反模式。这种情况下最好使用浏览器的内置方法来解析JSON数据,以确保代码的安全性和数据的合法性。如果浏览器不支持JSON.parse(),你可以使用JSON.org所提供的库。</p>
<p>记住,多数情况下,给setInterval()、setTimeout()和Function()构造函数传入字符串的情形和eval()类似,这种用法也是应当避免的,这一点非常重要,因为这些情形中JavaScript最终还是会执行传入的字符串参数:</p>
<pre><code>// antipatterns
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);
// preferred
setTimeout(myFunc, 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);
</code></pre>
<p>new Function()的用法和eval()非常类似,应当特别注意。这种构造函数的方式很强大,但往往被误用。如果你不得不使用eval(),你可以尝试用new Function()来代替。这有一个潜在的好处,在new Function()中运行的代码会在一个局部函数作用域内执行,因此源码中所有用var定义的变量不会自动变成全局变量。还有一种方法可以避免eval()中定义的变量转换为全局变量,即是将eval()包装在一个立即执行的匿名函数内(详细内容请参照第四章)。</p>
<p>看一下这个例子,这里只有un成为了全局变量,污染了全局命名空间:</p>
<pre><code>console.log(typeof un);// "undefined"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"
var jsstring = "var un = 1; console.log(un);";
eval(jsstring); // logs "1"
jsstring = "var deux = 2; console.log(deux);";
new Function(jsstring)(); // logs "2"
jsstring = "var trois = 3; console.log(trois);";
(function () {
eval(jsstring);
}()); // logs "3"
console.log(typeof un); // "number"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"
</code></pre>
<p>eval()和Function构造函数还有一个区别,就是eval()可以修改作用域链,而Function更像是一个沙箱。不管在什么地方执行Function,它只能看到全局作用域。因此它不会太严重的污染局部变量。在下面的示例代码中,eval()可以访问且修改其作用域之外的变量,而Function不能(注意,使用Function和new Function是完全一样的)。</p>
<pre><code>(function () {
var local = 1;
eval("local = 3; console.log(local)"); // logs 3
console.log(local); // logs 3
}());
(function () {
var local = 1;
Function("console.log(typeof local);")(); // logs undefined
}());
</code></pre>
<p><a name="a15"></a></p>
<h2>使用parseInt()进行数字转换</h2>
<p>可以使用parseInt()将字符串转换为数字。函数的第二个参数是转换基数(译注:“基数”指的是数字进制的方式),这个参数通常被省略。但当字符串以0为前缀时转换就会出错,例如,在表单中输入日期的一个字段。ECMAScript3中以0为前缀的字符串会被当作八进制数处理(基数为8)。但在ES5中不是这样。为了避免转换类型不一致而导致的意外结果,应当总是指定第二个参数:</p>
<pre><code>var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);
</code></pre>
<p>在这个例子中,如果省略掉parseInt的第二个参数,比如parseInt(year),返回值是0,因为“09”被认为是八进制数(等价于parseInt(year,8)),而且09是非法的八进制数。</p>
<p>字符串转换为数字还有两种方法:</p>
<pre><code>+"08" // result is 8
Number("08") // 8
</code></pre>
<p>这两种方法要比parseInt()更快一些,因为顾名思义parseInt()是一种“解析”而不是简单的“转换”。但当你期望将“08 hello”这类字符串转换为数字,则必须使用parseInt(),其他方法都会返回NaN。</p>
<p><a name="a16"></a></p>
<h2>编码风格</h2>
<p>确立并遵守编码规范非常重要,这会让你的代码风格一致、可预测、可读性更强。团队新成员通过学习编码规范可以很快进入开发状态、并写出团队其他成员易于理解的代码。</p>
<p>在开源社区和邮件组中关于编码风格的争论一直不断(比如关于代码缩进,用tab还是空格?)。因此,如果你打算在团队内推行某种编码规范时,要做好应对各种反对意见的心理准备,而且要吸取各种意见,这对确立并一贯遵守某种编码规范是非常重要,而不是斤斤计较的纠结于编码规范的细节。</p>
<p><a name="a17"></a></p>
<h3>缩进</h3>
<p>代码没有缩进几乎就不能读了,而不一致的缩进更加糟糕,因为它看上去像是遵循了规范,真正读起来却磕磕绊绊。因此规范的使用缩进非常重要。</p>
<p>有些开发者喜欢使用tab缩进,因为每个人都可以根据自己的喜好来调整tab缩进的空格数,有些人则喜欢使用空格缩进,通常是四个空格,这都无所谓,只要团队每个人都遵守同一个规范即可,本书中所有的示例代码都采用四个空格的缩进写法,这也是JSLint所推荐的。</p>
<p>那么到底什么应该缩进呢?规则很简单,花括号里的内容应当缩进,包括函数体、循环(do、while、for和for-in)体、if条件、switch语句和对象直接量里的属性。下面的代码展示了如何正确的使用缩进:</p>
<pre><code>function outer(a, b) {
var c = 1,
d = 2,
inner;
if (a > b) {
inner = function () {
return {
r: c - d
};
};
} else {
inner = function () {
return {
r: c + d
};
};
}
return inner;
}
</code></pre>
<p><a name="a18"></a></p>
<h3>花括号</h3>
<p>应当总是使用花括号,即使是在可省略花括号的时候也应当如此。从技术角度讲,如果if或for中只有一个语句,花括号是可以省略的,但最好还是不要省略。这让你的代码更加工整一致而且易于更新。</p>
<p>假设有这样一段代码,for循环中只有一条语句,你可以省略掉这里的花括号,而且不会有语法错误:</p>
<pre><code>// bad practice
for (var i = 0; i < 10; i += 1)
alert(i);
</code></pre>
<p>但如果过了一段时间,你给这个循环添加了另一行代码?</p>
<pre><code>// bad practice
for (var i = 0; i < 10; i += 1)
alert(i);
alert(i + " is " + (i % 2 ? "odd" : "even"));
</code></pre>
<p>第二个alert实际处于循环体之外,但这里的缩进会迷惑你。长远考虑最好还是写上花括号,即便是在只有一个语句的语句块中也应如此:</p>
<pre><code>// better
for (var i = 0; i < 10; i += 1) {
alert(i);
}
</code></pre>
<p>同理,if条件句也应当如此:</p>
<pre><code>// bad
if (true)
alert(1);
else
alert(2);
// better
if (true) {
alert(1);
} else {
alert(2);
}
</code></pre>
<p><a name="a19"></a></p>
<h3>左花括号的位置</h3>
<p>开发人员对于左大括号的位置有着不同的偏好,在同一行呢还是在下一行?</p>
<pre><code>if (true) {
alert("It's TRUE!");
}
</code></pre>
<p>或者:</p>
<pre><code>if (true)
{
alert("It's TRUE!");
}
</code></pre>
<p>在这个例子中,看起来只是个人偏好问题。但有时候花括号位置的不同则会影响程序的执行。因为JavaScript会“自动插入分号”。JavaScript对行结束时的分号并无要求,它会自动将分号补全。因此,当函数return语句返回了一个对象直接量,而对象的左花括号和return不在同一行时,程序的执行就和预想的不同了:</p>
<pre><code>// warning: unexpected return value
function func() {
return
{
name: "Batman"
};
}
</code></pre>
<p>可以看出程序作者的意图是返回一个包含了name属性的对象,但实际情况不是这样。因为return后会填补一个分号,函数的返回值就是undefined。这段代码等价于:</p>
<pre><code>// warning: unexpected return value
function func() {
return undefined;
// unreachable code follows...
{
name: "Batman"
};
}
</code></pre>
<p>结论,总是使用花括号,而且总是将左花括号与上一条语句放在同一行:</p>
<pre><code>function func() {
return {
name: "Batman"
};
}
</code></pre>
<blockquote>
<p>关于分号应当注意:和花括号一样,应当总是使用分号,尽管在JavaScript解析代码时会补全行末省略的分号。严格遵守这条规则,可以让代码更加严谨,同时可以避免前面例子中所出现的歧义。</p>
</blockquote>
<p><a name="a20"></a></p>
<h3> 空格</h3>
<p>空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在JavaScript中,你可以按照同样的逻辑在表达式(相当于逗号)和语句结束(相对于完成了某个“想法”)后面添加间隔。</p>
<p>适合使用空格的地方包括:</p>
<ul>
<li>for循环中的分号之后,比如 <code>for (var i = 0; i < 10; i += 1) {...}</code></li>
<li>for循环中初始化多个变量,比如 <code>for (var i = 0, max = 10; i < max; i += 1) {...}</code></li>
<li>分隔数组项的逗号之后,<code>var a = [1, 2, 3];</code></li>
<li>对象属性后的逗号以及名值对之间的冒号之后,<code>var o = {a: 1, b: 2};</code></li>
<li>函数参数中,<code>myFunc(a, b, c)</code></li>
<li>函数声明的花括号之前,<code>function myFunc() {}</code></li>
<li>匿名函数表达式function之后,<code>var myFunc = function () {};</code></li>
</ul>
<p>另外,我们推荐在运算符和操作数之间添加空格。也就是说在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=符号前后都添加空格。</p>
<pre><code>// generous and consistent spacing
// makes the code easier to read
// allowing it to "breathe"
var d = 0,
a = b + 1;
if (a && b && c) {
d = a % c;
a += d;
}
// antipattern
// missing or inconsistent spaces
// make the code confusing
var d= 0,
a =b+1;
if (a&& b&&c) {
d=a %c;
a+= d;
}
</code></pre>
<p>最后,还应当注意,最好在花括号旁边添加空格:</p>
<ul>
<li>在函数、if-else语句、循环、对象直接量的左花括号之前补充空格({)</li>
<li>在右花括号和else和while之间补充空格</li>
</ul>
<blockquote>
<p>垂直空白的使用经常被我们忽略,你可以使用空行来将代码单元分隔开,就像文学作品中使用段落作分隔一样。</p>
</blockquote>
<p><a name="a21"></a></p>
<h2>命名规范</h2>
<p>另外一种可以提升你代码的可预测性和可维护性的方法是采用命名规范。也就是说变量和函数的命名都遵照同种习惯。</p>
<p>下面是一些建议的命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范本身更加重要。</p>
<p><a name="a22"></a></p>
<h3>构造器命名中的大小写</h3>
<p>JavaScript中没有类,但有构造函数,可以通过new来调用构造函数:</p>
<pre><code>var adam = new Person();
</code></pre>
<p>由于构造函数毕竟还是函数,不管我们将它用作构造器还是函数,当然希望只通过函数名就可分辨出它是构造器还是普通函数。</p>
<p>首字母大写可以提示你这是一个构造函数,而首字母小写的函数一般只认为它是普通的函数,不应该通过new来调用它:</p>
<pre><code>function MyConstructor() {...}
function myFunction() {...}
</code></pre>
<p>下一章将介绍一些强制将函数用作构造器的编程模式,但遵守我们所提到的命名规范会更好的帮助程序员阅读源码。</p>
<p><a name="a23"></a></p>
<h3>单词分隔</h3>
<p>当你的变量名或函数名中含有多个单词时,单词之间的分隔也应当遵循统一的约定。最常见的做法是“驼峰式”命名,单词都是小写,每个单词的首字母是大写。</p>
<p>对于构造函数,可以使用“大驼峰式”命名,比如MyConstructor(),对于函数和方法,可以采用“小驼峰式”命名,比如myFunction(),calculateArea()和getFirstName()。</p>
<p>那么对于那些不是函数的变量应当如何命名呢?变量名通常采用小驼峰式命名,还有一个不错的做法是,变量所有字母都是小写,单词之间用下划线分隔,比如,first<em>name,favorite</em>bands和old<em>company</em>name,这种方法可以帮助你区分函数和其他标识符——原始数据类型或对象。</p>
<p>ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名称是罕见的(正则表达式对象的lastIndex和ignoreCase属性)。</p>
<p>在ECMAScript中的属性和方法均使用驼峰式命名,尽管包含多单词的属性名称(正则表达式对象中的lastIndex和ignoreCase)并不常见。</p>
<p><a name="a24"></a></p>
<h3>其他命名风格</h3>
<p>有时开发人员使用命名规范来弥补或代替语言特性的不足。</p>
<p>比如,JavaScript中无法定义常量(尽管有一些内置常量比如Number.MAX_VALUE),所以开发者都采用了这种命名习惯,对于那些程序运行周期内不会更改的变量使用全大写字母来命名。比如:</p>
<pre><code>// precious constants, please don't touch
var PI = 3.14,
MAX_WIDTH = 800;
</code></pre>
<p>除了使用大写字母的命名方式之外,还有另一种命名规约:全局变量都大写。这种命名方式和“减少全局变量”的约定相辅相成,并让全局变量很容易辨认。</p>
<p>除了常量和全局变量的命名惯例,这里讨论另外一种命名惯例,即私有变量的命名。尽管在JavaScript是可以实现真正的私有变量的,但开发人员更喜欢在私有成员或方法名之前加上下划线前缀,比如下面的例子:</p>
<pre><code>var person = {
getName: function () {
return this._getFirst() + ' ' + this._getLast();
},
_getFirst: function () {
// ...
},
_getLast: function () {
// ...
}
};
</code></pre>
<p>在这个例子中,getName()的身份是一个公有方法,属于稳定的API,而<em>getFirst()和</em>getLast()则是私有方法。尽管这两个方法本质上和公有方法无异,但在方法名前加下划线前缀就是为了警告用户不要直接使用这两个私有方法,因为不能保证它们在下一个版本中还能正常工作。JSLint会对私有方法作检查,除非设置了JSLint的nomen选项为false。</p>
<p>下面介绍一些_private风格写法的变种:</p>
<ul>
<li>在名字尾部添加下划下以表明私有,比如<code>name_</code>和<code>getElements_()</code></li>
<li>使用一个下划线前缀表明受保护的属性_protected,用两个下划线前缀表明私有属性__private</li>
<li>在Firefox中实现了一些非标准的内置属性,这些属性在开头和结束都有两个下划线,比如<code>__proto__</code>和<code>__parent__</code></li>
</ul>
<p><a name="a25"></a></p>
<h2>书写注释</h2>
<p>写代码就要写注释,即便你认为你的代码不会被别人读到。当你对一个问题非常熟悉时,你会很快找到问题代码,但当过了几个星期后再来读这段代码,则需要绞尽脑汁的回想代码的逻辑。</p>
<p>你不必对显而易见的代码作过多的注释:每个变量和每一行都作注释。但你需要对所有的函数、他们的参数和返回值补充注释,对于那些有趣的或怪异的算法和技术也应当配备注释。对于阅读你的代码的其他人来说,注释就是一种提示,只要阅读注释、函数名以及参数,就算不读代码也能大概理解程序的逻辑。比如,这里有五到六行代码完成了某个功能,如果提供了一行描述这段代码功能的注释,读程序的人就不必再去关注代码的细节实现了。代码注释的写法并没有硬性规定,有些代码片段(比如正则表达式)的确需要比代码本身还多的注释。</p>
<blockquote>
<p>由于过时的注释会带来很多误导,这比不写注释还糟糕。因此保持注释时刻更新的习惯非常重要,尽管对很多人来说这很难做到。</p>
</blockquote>
<p>在下一小节我们会讲到,注释可以自动生成文档。</p>
<p><a name="a26"></a></p>
<h2>书写API文档</h2>
<p>很多人都觉得写文档是一件枯燥且吃力不讨好的事情,但实际情况不是这样。我们可以通过代码注释自动生成文档,这样就不用再去专门写文档了。很多人觉得这是一个不错的点子,因为根据某些关键字和格式化的文档自动生成可阅读的参考手册本身就是“某种编程”。</p>
<p>传统的APIdoc诞生自Java世界,这个工具名叫“javadoc”,和Java SDK(软件开发工具包)一起提供。但这个创意迅速被其他语言借鉴。JavaScript领域有两个非常优秀的开源工具,它们是JSDoc Toolkit(http://code.google.com/p/jsdoc-toolkit/ )和YUIDoc(http://yuilibrary.com/projects/yuidoc )。</p>
<p>生成API文档的过程包括:</p>
<ul>
<li>以特定的格式来组织书写源代码</li>
<li>运行工具来对代码和注释进行解析</li>
<li>发布工具运行的结果,通常是HTML页面</li>
</ul>
<p>你需要学习这种特殊的语法,包括十几种标签,写法类似于:</p>
<pre><code>/**
* @tag value
*/
</code></pre>
<p>比如这里有一个函数reverse(),可以对字符串进行反序操作。它的参数和返回值都是字符串。给它补充注释如下:</p>
<pre><code>/**
* Reverse a string
*
* @param {String} input String to reverse
* @return {String} The reversed string
*/
var reverse = function (input) {
// ...
return output;
};
</code></pre>
<p>可以看到,@param是用来说明输入参数的标签,@return是用来说明返回值的标签,文档生成工具最终会为将这种带注释的源代码解析成格式化好的HTML文档。</p>
<p><a name="a27"></a></p>
<h3>一个例子:YUIDoc</h3>
<p>YUIDoc最初的目的是为YUI库(Yahoo! User Interface)生成文档,但也可以应用于任何项目,为了更充分的使用YUIDoc你需要学习它的注释规范,比如模块和类的写法(当然在JavaScript中是没有类的概念的)。</p>
<p>让我们看一个用YUIDoc生成文档的完整例子。</p>
<p>图2-1展示了最终生成的文档的模样,你可以根据项目需要随意定制HTML模板,让生成的文档更加友好和个性化。</p>
<p>这里同样提供了在线的demo,请参照 http://jspatterns.com/book/2/。</p>
<p>这个例子中所有的应用作为一个模块(myapp)放在一个文件里(app.js),后续的章节会更详细的介绍模块,现在只需知道用可以用一个YUIDoc的标签来表示模块即可。</p>
<p>图2-1 YUIDoc生成的文档</p>
<p><img src="http://img02.taobaocdn.com/tps/i2/T1fSCgXdBsXXXXXXXX-781-647.png" alt="pic" /></p>
<p>app.js的开始部分:</p>
<pre><code>/**
* My JavaScript application
*
* @module myapp
*/
</code></pre>
<p>然后定义了一个空对象作为模块的命名空间:</p>
<pre><code>var MYAPP = {};
</code></pre>
<p>紧接着定义了一个包含两个方法的对象math_stuff,这两个方法分别是sum()和multi():</p>
<pre><code>/**
* A math utility
* @namespace MYAPP
* @class math_stuff
*/
MYAPP.math_stuff = {
/**
* Sums two numbers
*
* @method sum
* @param {Number} a First number
* @param {Number} b The second number
* @return {Number} The sum of the two inputs
*/
sum: function (a, b) {
return a + b;
},
/**
* Multiplies two numbers
*
* @method multi
* @param {Number} a First number
* @param {Number} b The second number
* @return {Number} The two inputs multiplied
*/
multi: function (a, b) {
return a * b;
}
};
</code></pre>