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
|
---
title: 重新介绍 JavaScript(JS 教程)
slug: Web/JavaScript/A_re-introduction_to_JavaScript
tags:
- JavaScript
- 指南
- 教程
- 进阶
- 高级
translation_of: Web/JavaScript/A_re-introduction_to_JavaScript
---
<div>{{jsSidebar}}</div>
<p>为什么会有这一篇“重新介绍”呢?因为 {{Glossary("JavaScript")}} 堪称<a href="http://javascript.crockford.com/javascript.html">世界上被人误解最深的编程语言</a>。虽然常被嘲为“玩具语言”,但在它看似简洁的外衣下,还隐藏着强大的语言特性。 JavaScript 目前广泛应用于众多知名应用中,对于网页和移动开发者来说,深入理解 JavaScript 就尤为必要。</p>
<p>我们有必要先从这门语言的历史谈起。在1995 年 Netscape 一位名为 Brendan Eich 的工程师创造了 JavaScript,随后在 1996 年初,JavaScript 首先被应用于 Netscape 2 浏览器上。最初的 JavaScript 名为 LiveScript,但是因为一个糟糕的营销策略而被重新命名,该策略企图利用Sun Microsystem的Java语言的流行性,将它的名字从最初的 LiveScript 更改为 JavaScript——尽管两者之间并没有什么共同点。这便是之后混淆产生的根源。</p>
<p>几个月后,Microsoft 随 IE 3 发布推出了一个与之基本兼容的语言 JScript。又过了几个月,Netscape 将 JavaScript 提交至 <a href="http://www.ecma-international.org">Ecma International</a>(一个欧洲标准化组织), {{Glossary("ECMAScript")}} 标准第一版便在 1997 年诞生了,随后在 1999 年以 <a href="http://www.ecma-international.org/publications/standards/Ecma-262.htm">ECMAScript 第三版</a>的形式进行了更新,从那之后这个标准没有发生过大的改动。由于委员会在语言特性的讨论上发生分歧,ECMAScript 第四版尚未推出便被废除,但随后于 2009 年 12 月发布的 ECMAScript 第五版引入了第四版草案加入的许多特性。第六版标准已经于 2015 年 6 月发布。</p>
<div class="note">
<p><strong>注意:</strong> 由于这种用法更常见,从这里开始,我们将使用 JavaScript 来指代 ECMAScript 。</p>
</div>
<p>与大多数编程语言不同,JavaScript 没有输入或输出的概念。它是一个在宿主环境(host environment)下运行的脚本语言,任何与外界沟通的机制都是由宿主环境提供的。浏览器是最常见的宿主环境,但在非常多的其他程序中也包含 JavaScript 解释器,如 Adobe Acrobat、Adobe Photoshop、SVG 图像、Yahoo! 的 Widget 引擎,<a href="http://nodejs.org">Node.js</a> 之类的服务器端环境,NoSQL 数据库(如开源的 <a href="http://couchdb.apache.org">Apache CouchDB</a>)、嵌入式计算机,以及包括 <a href="http://www.gnome.org">GNOME</a> (注:GNU/Linux 上最流行的 GUI 之一)在内的桌面环境等等。</p>
<h2 id="概览">概览</h2>
<p>JavaScript 是一种多范式的动态语言,它包含类型、运算符、标准内置( built-in)对象和方法。它的语法来源于 Java 和 C,所以这两种语言的许多语法特性同样适用于 JavaScript。JavaScript 通过原型链而不是类来支持面向对象编程(有关 ES6 类的内容参考这里{{jsxref("Classes")}},有关对象原型参考见此<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">继承与原型链</a>)。JavaScript同样支持函数式编程——因为它们也是对象,函数也可以被保存在变量中,并且像其他对象一样被传递。</p>
<p>先从任何编程语言都不可缺少的组成部分——“类型”开始。JavaScript 程序可以修改值(value),这些值都有各自的类型。JavaScript 中的类型包括:</p>
<ul>
<li>{{jsxref("Number")}}(数字)</li>
<li>{{jsxref("String")}}(字符串)</li>
<li>{{jsxref("Boolean")}}(布尔)</li>
<li>{{jsxref("Function")}}(函数)</li>
<li>{{jsxref("Object")}}(对象)</li>
<li>{{jsxref("Symbol")}}(ES2015 新增)</li>
</ul>
<p>…哦,还有看上去有些…奇怪的 {{jsxref("undefined")}}(未定义)类型和 {{jsxref("null")}}(空)类型。此外还有{{jsxref("Array")}}(数组)类型,以及分别用于表示日期和正则表达式的 {{jsxref("Date")}}(日期)和 {{jsxref("RegExp")}}(正则表达式),这三种类型都是特殊的对象。严格意义上说,Function(函数)也是一种特殊的对象。所以准确来说,JavaScript 中的类型应该包括这些:</p>
<ul>
<li>{{jsxref("Number")}}(数字)</li>
<li>{{jsxref("String")}}(字符串)</li>
<li>{{jsxref("Boolean")}}(布尔)</li>
<li>{{jsxref("Symbol")}}(符号)(ES2015 新增)</li>
<li>{{jsxref("Object")}}(对象)
<ul>
<li>{{jsxref("Function")}}(函数)</li>
<li>{{jsxref("Array")}}(数组)</li>
<li>{{jsxref("Date")}}(日期)</li>
<li>{{jsxref("RegExp")}}(正则表达式)</li>
</ul>
</li>
<li>{{jsxref("null")}}(空)</li>
<li>{{jsxref("undefined")}}(未定义)</li>
</ul>
<p>JavaScript 还有一种内置的 {{jsxref("Error")}}(错误)类型。但是,如果我们继续使用上面的分类,事情便容易得多;所以,现在,我们先讨论上面这些类型。</p>
<h2 id="数字">数字</h2>
<p>根据语言规范,JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”("double-precision 64-bit format IEEE 754 values")表示数字。——在JavaScript(除了{{jsxref("BigInt")}})当中,<strong>并不存在整数/整型(Integer)。</strong>因此在处理如下的场景时候,您一定要小心:</p>
<pre class="notranslate">console.log(3 / 2); // 1.5,<em>not</em> 1
console.log(Math.floor(3 / 2)); // 1
</pre>
<p>一个看上去是整数的东西,其实都是浮点数。</p>
<p>当然,您也需要小心这种情况:</p>
<pre class="brush: js notranslate">0.1 + 0.2 = 0.30000000000000004
</pre>
<p>在具体实现时,整数值通常被视为32位整型变量,在个别实现(如某些浏览器)中也以32位整型变量的形式进行存储,直到它被用于执行某些32位整型不支持的操作,这是为了便于进行位操作。</p>
<p>JavaScript 支持标准的<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators">算术运算符</a>,包括加法、减法、取模(或取余)等等。还有一个之前没有提及的内置对象 {{jsxref("Math")}}(数学对象),用以处理更多的高级数学函数和常数:</p>
<pre class="brush: js notranslate">Math.sin(3.5);
var circumference = 2 * Math.PI * r;
</pre>
<p>你可以使用内置函数 {{jsxref("Global_Objects/parseInt", "parseInt()")}} 将字符串转换为整型。该函数的第二个可选参数表示字符串所表示数字的基(进制):</p>
<pre class="brush: js notranslate">parseInt("123", 10); // 123
parseInt("010", 10); // 10
</pre>
<p>一些老版本的浏览器会将首字符为“0”的字符串当做八进制数字,2013 年以前的 JavaScript 实现会返回一个意外的结果:</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">parseInt("010"); // 8
parseInt("0x10"); // 16</code></pre>
<p>这是因为字符串以数字 0 开头,{{jsxref("Global_Objects/parseInt", "parseInt()")}}函数会把这样的字符串视作八进制数字;同理,0x开头的字符串则视为十六进制数字。</p>
<p>如果想把一个二进制数字字符串转换成整数值,只要把第二个参数设置为 2 就可以了:</p>
<pre class="brush: js notranslate">parseInt("11", 2); // 3
</pre>
<p>JavaScript 还有一个类似的内置函数 {{jsxref("Global_Objects/parseFloat", "parseFloat()")}},用以解析浮点数字符串,与{{jsxref("Global_Objects/parseInt", "parseInt()")}}不同的地方是,<code>parseFloat()</code> 只应用于解析十进制数字。</p>
<p>一元运算符 + 也可以把数字字符串转换成数值:</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">+ "42"; // 42
+ "010"; // 10
+ "0x10"; // 16</code></pre>
<p>如果给定的字符串不存在数值形式,函数会返回一个特殊的值 {{jsxref("NaN")}}(Not a Number 的缩写):</p>
<pre class="brush: js notranslate">parseInt("hello", 10); // NaN
</pre>
<p>要小心NaN:如果把 <code>NaN</code> 作为参数进行任何数学运算,结果也会是 <code>NaN</code>:</p>
<pre class="brush: js notranslate">NaN + 5; //NaN
</pre>
<p>可以使用内置函数 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/isNaN"><code>isNaN()</code></a> 来判断一个变量是否为 <code>NaN</code>:</p>
<pre class="brush: js notranslate">isNaN(NaN); // true
</pre>
<p>JavaScript 还有两个特殊值:<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Infinity"><code>Infinity</code></a>(正无穷)和 <code>-Infinity</code>(负无穷):</p>
<pre class="brush: js notranslate">1 / 0; // Infinity
-1 / 0; // -Infinity
</pre>
<p>可以使用内置函数 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/isFinite"><code>isFinite()</code></a> 来判断一个变量是否是一个有穷数, 如果类型为<code>Infinity</code>, <code>-Infinity</code> 或 <code>NaN则返回false</code>:</p>
<pre class="brush: js notranslate">isFinite(1/0); // false
isFinite(Infinity); // false
isFinite(-Infinity); // false
isFinite(NaN); // false
isFinite(0); // true
isFinite(2e64); // true
isFinite("0"); // true
// 如果是纯数值类型的检测,则返回 false:
Number.isFinite("0"); // false</pre>
<div class="note"><strong>备注:</strong> {{jsxref("Global_Objects/parseInt", "parseInt()")}} 和 {{jsxref("Global_Objects/parseFloat", "parseFloat()")}} 函数会尝试逐个解析字符串中的字符,直到遇上一个无法被解析成数字的字符,然后返回该字符前所有数字字符组成的数字。但是运算符 "+"对字符串的转换方式与之不同, 只要字符串含有无法被解析成数字的字符,该字符串就将被转换成 <code>NaN</code>。可分别使用这两种方法解析“10.2abc”这一字符串,并比较得到的结果,来理解这两种方法的区别。</div>
<h2 id="字符串">字符串</h2>
<p>JavaScript 中的字符串是一串<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Values,_variables,_and_literals#Unicode.E7.BC.96.E7.A0.81">Unicode 字符</a>序列。这对于那些需要和多语种网页打交道的开发者来说是个好消息。更准确地说,它们是一串UTF-16编码单元的序列,每一个编码单元由一个 16 位二进制数表示。每一个Unicode字符由一个或两个编码单元来表示。</p>
<p>如果想表示一个单独的字符,只需使用长度为 1 的字符串。</p>
<p>通过访问字符串的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/length"><code>length</code></a>(编码单元的个数)属性,可以得到它的长度。</p>
<pre class="brush: js notranslate">"hello".length; // 5
</pre>
<p>这是我们第一次碰到 JavaScript 对象。我们有没有提过你可以像 {{jsxref("Object", "object", "", 1)}} 一样使用字符串?是的,字符串也有 {{jsxref("String", "methods", "#Methods", 1)}}(方法)能让你操作字符串和获取字符串的信息。</p>
<pre class="brush: js notranslate">"hello".charAt(0); // "h"
"hello, world".replace("world", "mars"); // "hello, mars"
"hello".toUpperCase(); // "HELLO"
</pre>
<h2 id="其他类型">其他类型</h2>
<p>与其他类型不同,JavaScript 中的 {{jsxref("null")}} 表示一个空值(non-value),必须使用 null 关键字才能访问,{{jsxref("undefined")}} 是一个“undefined(未定义)”类型的对象,表示一个未初始化的值,也就是还没有被分配的值。我们之后再具体讨论变量,但有一点可以先简单说明一下,JavaScript 允许声明变量但不对其赋值,一个未被赋值的变量就是 <code>undefined</code> 类型。还有一点需要说明的是,<code>undefined</code> 实际上是一个不允许修改的常量。</p>
<p>JavaScript 包含布尔类型,这个类型的变量有两个可能的值,分别是 <code>true</code> 和 <code>false</code>(两者都是关键字)。根据具体需要,JavaScript 按照如下规则将变量转换成布尔类型:</p>
<ol>
<li><code>false</code>、<code>0</code>、空字符串(<code>""</code>)、<code>NaN</code>、<code>null</code> 和 <code>undefined</code> 被转换为 <code>false</code></li>
<li>所有其他值被转换为 <code>true</code></li>
</ol>
<p>也可以使用 <code>Boolean()</code> 函数进行显式转换:</p>
<pre class="brush: js notranslate">Boolean(''); // false
Boolean(234); // true
</pre>
<p>不过一般没必要这么做,因为 JavaScript 会在需要一个布尔变量时隐式完成这个转换操作(比如在 <code>if</code> 条件语句中)。所以,有时我们可以把转换成布尔值后的变量分别称为 真值(true values)——即值为 true 和 假值(false values)——即值为 false;也可以分别称为“真的”(truthy)和“假的”(falsy)。</p>
<p>JavaScript 支持包括 <code>&&</code>(逻辑与)、<code>||</code> (逻辑或)和<code>!</code>(逻辑非)在内的一些逻辑运算符。下面会有所提到。</p>
<h2 id="变量">变量</h2>
<p>在 JavaScript 中声明一个新变量的方法是使用关键字 <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let">let</a></code> 、<code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const">const</a></code> 和 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/var"><code>var</code></a>:</p>
<p><code><strong>let</strong></code> 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">let a;
let name = 'Simon';</code></pre>
<p>下面是使用 <code><strong>let</strong></code> 声明变量作用域的例子:</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">// myLetVariable 在这里 *不能* 被引用
for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
// myLetVariable 只能在这里引用
}
// myLetVariable 在这里 *不能* 被引用</code></pre>
<p><code><strong>const</strong></code> 允许声明一个不可变的常量。这个常量在定义域内总是可见的。</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">const Pi = 3.14; // 设置 Pi 的值
Pi = 1; // 将会抛出一个错误因为你改变了一个常量的值。</code></pre>
<p><code><strong>var</strong></code> 是最常见的声明变量的关键字。它没有其他两个关键字的种种限制。这是因为它是传统上在 JavaScript 声明变量的唯一方法。使用 <strong><code>var</code></strong> 声明的变量在它所声明的整个函数都是可见的。</p>
<pre class="brush: js notranslate">var a;
var name = "simon";
</pre>
<p>一个使用 <strong><code>var</code> </strong>声明变量的语句块的例子:</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">// myVarVariable在这里 *能* 被引用
for (var myVarVariable = 0; myVarVariable < 5; myVarVariable++) {
// myVarVariable 整个</code>函数<code class="language-js">中都能被引用
}
// myVarVariable 在这里 *能* 被引用</code></pre>
<p>如果声明了一个变量却没有对其赋值,那么这个变量的类型就是 <code>undefined</code>。</p>
<p>JavaScript 与其他语言的(如 Java)的重要区别是在 JavaScript 中语句块(blocks)是没有作用域的,只有函数有作用域。因此如果在一个复合语句中(如 if 控制结构中)使用 var 声明一个变量,那么它的作用域是整个函数(复合语句在函数中)。 但是从 ECMAScript Edition 6 开始将有所不同的, <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let">let</a></code> 和 <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const">const</a></code> 关键字允许你创建块作用域的变量。</p>
<h2 id="运算符">运算符</h2>
<p>JavaScript的算术操作符包括 <code>+</code>、<code>-</code>、<code>*</code>、<code>/</code> 和 <code>%</code> ——求余(<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#求余_%28%29">与模运算相同</a>)。赋值使用 <code>=</code> 运算符,此外还有一些复合运算符,如 <code>+=</code> 和 <code>-=</code>,它们等价于 <code>x = x <em>operator</em> y</code>。</p>
<pre class="brush: js notranslate">x += 5; // 等价于 x = x + 5;
</pre>
<p>可以使用 <code>++</code> 和 <code>--</code> 分别实现变量的自增和自减。两者都可以作为前缀或后缀操作符使用。</p>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#.E5.8A.A0.E6.B3.95_(.2B)"><code>+</code> 操作符</a>还可以用来连接字符串:</p>
<pre class="brush: js notranslate">"hello" + " world"; // hello world
</pre>
<p>如果你用一个字符串加上一个数字(或其他值),那么操作数都会被首先转换为字符串。如下所示:</p>
<pre class="brush: js notranslate">"3" + 4 + 5; // 345
3 + 4 + "5"; // 75
</pre>
<p>这里不难看出一个实用的技巧——通过与空字符串相加,可以将某个变量快速转换成字符串类型。</p>
<p>JavaScript 中的<a href="/zh-CN/docs/Web/JavaScript/Reference/Operators/Comparison_Operators">比较操作</a>使用 <code><</code>、<code>></code>、<code><=</code> 和 <code>>=</code>,这些运算符对于数字和字符串都通用。相等的比较稍微复杂一些。由两个“<code>=</code>(等号)”组成的相等运算符有类型自适应的功能,具体例子如下:</p>
<pre class="brush: js notranslate">123 == "123" // true
1 == true; // true
</pre>
<p>如果在比较前不需要自动类型转换,应该使用由三个“<code>=</code>(等号)”组成的相等运算符:</p>
<pre class="brush: js notranslate">1 === true; //false
123 === "123"; // false
</pre>
<p>JavaScript 还支持 <code>!=</code> 和 <code>!==</code> 两种不等运算符,具体区别与两种相等运算符的区别类似。</p>
<p>JavaScript 还提供了 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators">位操作符</a>。</p>
<h2 id="控制结构">控制结构</h2>
<p>JavaScript 的控制结构与其他类 C 语言类似。可以使用 <code>if</code> 和 <code>else</code> 来定义条件语句,还可以连起来使用:</p>
<pre class="brush: js notranslate">var name = "kittens";
if (name == "puppies") {
name += "!";
} else if (name == "kittens") {
name += "!!";
} else {
name = "!" + name;
}
name == "kittens!!"; // true
</pre>
<p>JavaScript 支持 <code>while</code> 循环和 <code>do-while</code> 循环。前者适合常见的基本循环操作,如果需要循环体至少被执行一次则可以使用 <code>do-while</code>:</p>
<pre class="brush: js notranslate">while (true) {
// 一个无限循环!
}
var input;
do {
input = get_input();
} while (inputIsNotValid(input))
</pre>
<p>JavaScript 的 <code><a href="/zh-CN/docs/Web/JavaScript/Reference/Statements/for">for</a></code> 循环与 C 和 Java 中的相同:使用时可以在一行代码中提供控制信息。</p>
<pre class="brush: js notranslate">for (var i = 0; i < 5; i++) {
// 将会执行五次
}
</pre>
<p>JavaScript 也还包括其他两种重要的 for 循环: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of"><code>for</code>...<code>of</code></a></p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">for (let value of array) {
// do something with value
}</code></pre>
<p>和 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in"><code>for</code>...<code>in</code></a> :</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">for (let property in object) {
// do something with object property
}</code></pre>
<p><code>&&</code> 和 <code>||</code> 运算符使用短路逻辑(short-circuit logic),是否会执行第二个语句(操作数)取决于第一个操作数的结果。在需要访问某个对象的属性时,使用这个特性可以事先检测该对象是否为空:</p>
<pre class="brush: js notranslate">var name = o && o.getName();
</pre>
<p>或用于缓存值(当错误值无效时):</p>
<pre class="brush: js notranslate">var name = cachedName || (cachedName = getName());
</pre>
<p>类似地,JavaScript 也有一个用于条件表达式的三元操作符:</p>
<pre class="brush: js notranslate">var allowed = (age > 18) ? "yes" : "no";
</pre>
<p>在需要多重分支时可以使用 <code>基于一个数字或字符串的switch</code> 语句:</p>
<pre class="brush: js notranslate">switch(action) {
case 'draw':
drawIt();
break;
case 'eat':
eatIt();
break;
default:
doNothing();
}
</pre>
<p>如果你不使用 <code>break</code> 语句,JavaScript 解释器将会执行之后 <code>case</code> 中的代码。除非是为了调试,一般你并不需要这个特性,所以大多数时候不要忘了加上 <code>break。</code></p>
<pre class="brush: js notranslate">switch(a) {
case 1: // 继续向下
case 2:
eatIt();
break;
default:
doNothing();
}
</pre>
<p><code>default</code> 语句是可选的。<code>switch</code> 和 <code>case</code> 都可以使用需要运算才能得到结果的表达式;在 <code>switch</code> 的表达式和 <code>case</code> 的表达式是使用 <code>===</code> 严格相等运算符进行比较的:</p>
<pre class="brush: js notranslate">switch(1 + 3){
case 2 + 2:
yay();
break;
default:
neverhappens();
}
</pre>
<h2 id="对象">对象</h2>
<p>JavaScript 中的对象,Object,可以简单理解成“名称-值”对(而不是键值对:现在,ES 2015 的映射表(Map),比对象更接近键值对),不难联想 JavaScript 中的对象与下面这些概念类似:</p>
<ul>
<li>Python 中的字典(Dictionary)</li>
<li>Perl 和 Ruby 中的散列/哈希(Hash)</li>
<li>C/C++ 中的散列表(Hash table)</li>
<li>Java 中的散列映射表(HashMap)</li>
<li>PHP 中的关联数组(Associative array)</li>
</ul>
<p>这样的数据结构设计合理,能应付各类复杂需求,所以被各类编程语言广泛采用。正因为 JavaScript 中的一切(除了核心类型,core object)都是对象,所以 JavaScript 程序必然与大量的散列表查找操作有着千丝万缕的联系,而散列表擅长的正是高速查找。</p>
<p>“名称”部分是一个 JavaScript 字符串,“值”部分可以是任何 JavaScript 的数据类型——包括对象。这使用户可以根据具体需求,创建出相当复杂的数据结构。</p>
<p>有两种简单方法可以创建一个空对象:</p>
<pre class="brush: js notranslate">var obj = new Object();
</pre>
<p>和:</p>
<pre class="brush: js notranslate">var obj = {};
</pre>
<p>这两种方法在语义上是相同的。第二种更方便的方法叫作“对象字面量(object literal)”法。这种也是 JSON 格式的核心语法,一般我们优先选择第二种方法。</p>
<p>“对象字面量”也可以用来在对象实例中定义一个对象:</p>
<pre class="brush: js notranslate">var obj = {
name: "Carrot",
_for: "Max",//'for' 是保留字之一,使用'_for'代替
details: {
color: "orange",
size: 12
}
}
</pre>
<p>对象的属性可以通过链式(chain)表示方法进行访问:</p>
<pre class="brush: js notranslate">obj.details.color; // orange
obj["details"]["size"]; // 12
</pre>
<p>下面的例子创建了一个对象原型,<code><strong>Person</strong></code>,和这个原型的实例,<strong><code>You</code></strong>。</p>
<pre class="brush: js notranslate">function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义一个对象
var You = new Person('You', 24);
// 我们创建了一个新的 Person,名称是 "You"
// ("You" 是第一个参数, 24 是第二个参数..)
</pre>
<p>完成创建后,对象属性可以通过如下两种方式进行赋值和访问:</p>
<pre class="brush: js notranslate">// 点表示法(dot notation)
obj.name = 'Simon';
var name = obj.name;
</pre>
<p>和:</p>
<pre class="brush: js line-numbers language-js notranslate"><code class="language-js">// 括号表示法(bracket notation)
obj['name'] = 'Simon';
var name = obj['name'];
// can use a variable to define a key
var user = prompt('what is your key?')
obj[user] = prompt('what is its value?')</code></pre>
<p>这两种方法在语义上也是相同的。第二种方法的优点在于属性的名称被看作一个字符串,这就意味着它可以在运行时被计算,缺点在于这样的代码有可能无法在后期被解释器优化。它也可以被用来访问某些以<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords">预留关键字</a>作为名称的属性的值:</p>
<pre class="brush: js notranslate">obj.for = 'Simon'; // 语法错误,因为 for 是一个预留关键字
obj["for"] = 'Simon'; // 工作正常
</pre>
<div class="note">
<p><strong>注意:</strong>从 ECMAScript 5 开始,预留关键字可以作为对象的属性名(reserved words may be used as object property names "in the buff")。 这意味着当定义对象字面量时不需要用双引号了。参见 ES5 <a href="http://es5.github.io/#x7.6.1">Spec</a>.</p>
</div>
<p>关于对象和原型的详情参见: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype">Object.prototype</a>. 解释对象原型和对象原型链可以参见:<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">继承与原型链</a>。</p>
<div class="note">
<p><strong>注意:</strong>从 ECMAScript 2015 开始,对象键可以在创建时使用括号表示法由变量定义。{[phoneType]: 12345} 可以用来替换 <code>var userPhone = {}; userPhone[phoneType] = 12345</code> .</p>
</div>
<h2 id="数组">数组</h2>
<p>JavaScript 中的数组是一种特殊的对象。它的工作原理与普通对象类似(以数字为属性名,但只能通过<code>[]</code> 来访问),但数组还有一个特殊的属性——<code>length</code>(长度)属性。这个属性的值通常比数组最大索引大 1。</p>
<p>创建数组的传统方法是:</p>
<pre class="brush: js notranslate">var a = new Array();
a[0] = "dog";
a[1] = "cat";
a[2] = "hen";
a.length; // 3
</pre>
<p>使用数组字面量(array literal)法更加方便:</p>
<pre class="brush: js notranslate">var a = ["dog", "cat", "hen"];
a.length; // 3
</pre>
<p>注意,<code>Array.length </code>并不总是等于数组中元素的个数,如下所示:</p>
<pre class="brush: js notranslate">var a = ["dog", "cat", "hen"];
a[100] = "fox";
a.length; // 101
</pre>
<p>记住:数组的长度是比数组最大索引值多一的数。</p>
<p>如果试图访问一个不存在的数组索引,会得到 <code>undefined</code>:</p>
<pre class="brush: js notranslate">typeof(a[90]); // undefined
</pre>
<p>可以通过如下方式遍历一个数组:</p>
<pre class="brush: js notranslate">for (var i = 0; i < a.length; i++) {
// Do something with a[i]
}
</pre>
<p>ES2015 引入了更加简洁的 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of"><code>for</code>...<code>of</code></a> 循环,可以用它来遍历可迭代对象,例如数组:</p>
<pre class="brush: js notranslate">for (const currentValue of a) {
// Do something with currentValue
}
</pre>
<p>遍历数组的另一种方法是使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...in"><code>for...in</code></a> 循环, 然而这并不是遍历数组元素而是数组的索引。注意,如果哪个家伙直接向 <code>Array.prototype</code> 添加了新的属性,使用这样的循环这些属性也同样会被遍历。所以并不推荐使用这种方法遍历数组:</p>
<pre class="brush: js notranslate">for (var i in a) {
// 操作 a[i]
}
</pre>
<p>ECMAScript 5 增加了另一个遍历数组的方法,<code><a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach">forEach()</a></code>:</p>
<pre class="brush: js notranslate">["dog", "cat", "hen"].forEach(function(currentValue, index, array) {
// 操作 currentValue 或者 array[index]
});
</pre>
<p>如果想在数组后追加元素,只需要:</p>
<pre class="brush: js notranslate">a.push(item);</pre>
<p>除了 <code>forEach()</code> 和 <code>push()</code>,Array(数组)类还自带了许多方法。建议查看 <a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array">Array 方法的完整文档</a>。</p>
<table>
<thead>
<tr>
<th scope="col">方法名称</th>
<th scope="col">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>a.toString()</code></td>
<td>返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。</td>
</tr>
<tr>
<td><code>a.toLocaleString()</code></td>
<td>根据宿主环境的区域设置,返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。</td>
</tr>
<tr>
<td><code>a.concat(item1[, item2[, ...[, itemN]]])</code></td>
<td>返回一个数组,这个数组包含原先 <code>a</code> 和 <code>item1、item2、……、itemN</code> 中的所有元素。</td>
</tr>
<tr>
<td><code>a.join(sep)</code></td>
<td>返回一个包含数组中所有元素的字符串,每个元素通过指定的 <code>sep</code> 分隔。</td>
</tr>
<tr>
<td><code>a.pop()</code></td>
<td>删除并返回数组中的最后一个元素。</td>
</tr>
<tr>
<td><code>a.push(item1, ..., itemN)</code></td>
<td>将 <code>item1、item2、……、itemN</code> 追加至数组 <code>a</code>。</td>
</tr>
<tr>
<td><code>a.reverse()</code></td>
<td>数组逆序(会更改原数组 <code>a</code>)。</td>
</tr>
<tr>
<td><code>a.shift()</code></td>
<td>删除并返回数组中第一个元素。</td>
</tr>
<tr>
<td><code>a.slice(start, end)</code></td>
<td>返回子数组,以 <code>a[start]</code> 开头,以 <code>a[end]</code> 前一个元素结尾。</td>
</tr>
<tr>
<td><code>a.sort([cmpfn])</code></td>
<td>
<p>依据可选的比较函数 <code>cmpfn</code> 进行排序,如果未指定比较函数,则按字符顺序比较(即使被比较元素是数字)。</p>
</td>
</tr>
<tr>
<td><code>a.splice(start, delcount[, item1[, ...[, itemN]]])</code></td>
<td>
<p>从 <code>start</code> 开始,删除 <code>delcount</code> 个元素,然后插入所有的 <code>item</code>。</p>
</td>
</tr>
<tr>
<td><code>a.unshift(item1[, item2[, ...[, itemN]]])</code></td>
<td>
<p>将 <code>item</code> 插入数组头部,返回数组新长度(考虑 <code>undefined</code>)。</p>
</td>
</tr>
</tbody>
</table>
<h2 id="函数">函数</h2>
<p>学习 JavaScript 最重要的就是要理解对象和函数两个部分。最简单的函数就像下面这个这么简单:</p>
<pre class="brush: js notranslate">function add(x, y) {
var total = x + y;
return total;
}
</pre>
<p>这个例子包括你需要了解的关于基本函数的所有部分。一个 JavaScript 函数可以包含 0 个或多个已命名的变量。函数体中的表达式数量也没有限制。你可以声明函数自己的局部变量。<code>return</code> 语句在返回一个值并结束函数。如果没有使用 <code>return</code> 语句,或者一个没有值的 <code>return</code> 语句,JavaScript 会返回 <code>undefined</code>。</p>
<p>已命名的参数更像是一个指示而没有其他作用。如果调用函数时没有提供足够的参数,缺少的参数会被 <code>undefined</code> 替代。</p>
<pre class="brush: js notranslate">add(); // NaN
// 不能在 undefined 对象上进行加法操作
</pre>
<p>你还可以传入多于函数本身需要参数个数的参数:</p>
<pre class="brush: js notranslate">add(2, 3, 4); // 5
// 将前两个值相加,4 被忽略了
</pre>
<p>这看上去有点蠢。函数实际上是访问了函数体中一个名为 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments"><code>arguments</code></a> 的内部对象,这个对象就如同一个类似于数组的对象一样,包括了所有被传入的参数。让我们重写一下上面的函数,使它可以接收任意个数的参数:</p>
<pre class="brush: js notranslate">function add() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum;
}
add(2, 3, 4, 5); // 14
</pre>
<p>这跟直接写成 <code>2 + 3 + 4 + 5</code> 也没什么区别。我们还是创建一个求平均数的函数吧:</p>
<pre class="brush: js notranslate">function avg() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
}
avg(2, 3, 4, 5); // 3.5</pre>
<p>这个就有用多了,但是却有些冗长。为了使代码变短一些,我们可以使用<a href="/zh-CN/docs/Web/JavaScript/Reference/Functions/Rest_parameters">剩余参数</a>来替换arguments的使用。在这方法中,我们可以传递任意数量的参数到函数中同时尽量减少我们的代码。这个<strong>剩余参数操作符</strong>在函数中以:<strong>...variable</strong> 的形式被使用,它将包含在调用函数时使用的未捕获整个参数列表到这个变量中。我们同样也可以将 <strong>for</strong> 循环替换为 <strong>for...of </strong> 循环来返回我们变量的值。</p>
<pre class="brush: js notranslate">function avg(...args) {
var sum = 0;
for (let value of args) {
sum += value;
}
return sum / args.length;
}
avg(2, 3, 4, 5); // 3.5
</pre>
<div class="blockIndicator note">
<p>在上面这段代码中,所有被传入该函数的参数都被变量 <strong>args </strong>所持有。</p>
<p>需要注意的是,无论“剩余参数操作符”被放置到函数声明的哪里,它都会把除了自己之前的所有参数存储起来。比如函数:function avg(<strong>firstValue</strong>, ...args) 会把传入函数的第一个值存入 <strong>firstValue</strong>,其他的参数存入 <strong>args</strong>。这是虽然一个很有用的语言特性,却也会带来新的问题。<code>avg()</code> 函数只接受逗号分开的参数列表 -- 但是如果你想要获取一个数组的平均值怎么办?一种方法是将函数按照如下方式重写:</p>
</div>
<pre class="brush: js notranslate">function avgArray(arr) {
var sum = 0;
for (var i = 0, j = arr.length; i < j; i++) {
sum += arr[i];
}
return sum / arr.length;
}
avgArray([2, 3, 4, 5]); // 3.5
</pre>
<p>但如果能重用我们已经创建的那个函数不是更好吗?幸运的是 JavaScript 允许你通过任意函数对象的 <code><a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply">apply()</a></code> 方法来传递给它一个数组作为参数列表。</p>
<pre class="brush: js notranslate">avg.apply(null, [2, 3, 4, 5]); // 3.5
</pre>
<p>传给 <code>apply()</code> 的第二个参数是一个数组,它将被当作 <code>avg()</code> 的参数列表使用,至于第一个参数 <code>null</code>,我们将在后面讨论。这也正说明了一个事实——函数也是对象。</p>
<div class="blockIndicator note">
<p>通过使用<a href="/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax">展开语法</a>,你也可以获得同样的效果。</p>
<p>比如说:<code>avg(...numbers)</code></p>
</div>
<p>JavaScript 允许你创建匿名函数:</p>
<pre class="brush: js notranslate">var avg = function() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
};
</pre>
<p>这个函数在语义上与 <code>function avg()</code> 相同。你可以在代码中的任何地方定义这个函数,就像写普通的表达式一样。基于这个特性,有人发明出一些有趣的技巧。与 C 中的块级作用域类似,下面这个例子隐藏了局部变量:</p>
<pre class="brush: js notranslate">var a = 1;
var b = 2;
(function() {
var b = 3;
a += b;
})();
a; // 4
b; // 2
</pre>
<p>JavaScript 允许以递归方式调用函数。递归在处理树形结构(比如浏览器 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Document_Object_Model">DOM</a>)时非常有用。</p>
<pre class="brush: js notranslate">function countChars(elm) {
if (elm.nodeType == 3) { // 文本节点
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
}
</pre>
<p>这里需要说明一个潜在问题——既然匿名函数没有名字,那该怎么递归调用它呢?在这一点上,JavaScript 允许你命名这个函数表达式。你可以命名立即调用的函数表达式(IIFE——Immediately Invoked Function Expression),如下所示:</p>
<pre class="brush: js notranslate">var charsInBody = (function counter(elm) {
if (elm.nodeType == 3) { // 文本节点
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += counter(child);
}
return count;
})(document.body);
</pre>
<p>如上所提供的函数表达式的名称的作用域仅仅是该函数自身。这允许引擎去做更多的优化,并且这种实现更可读、友好。该名称也显示在调试器和一些堆栈跟踪中,节省了调试时的时间。</p>
<p>需要注意的是 JavaScript 函数是它们本身的对象——就和 JavaScript 其他一切一样——你可以给它们添加属性或者更改它们的属性,这与前面的对象部分一样。</p>
<h2 id="自定义对象">自定义对象</h2>
<div class="note"><strong>备注:</strong>关于 JavaScript 中面向对象编程更详细的信息,请参考 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript">JavaScript 面向对象简介</a>。</div>
<p>在经典的面向对象语言中,对象是指数据和在这些数据上进行的操作的集合。与 C++ 和 Java 不同,JavaScript 是一种基于原型的编程语言,并没有 class 语句,而是把函数用作类。那么让我们来定义一个人名对象,这个对象包括人的姓和名两个域(field)。名字的表示有两种方法:“名 姓(First Last)”或“姓, 名(Last, First)”。使用我们前面讨论过的函数和对象概念,可以像这样完成定义:</p>
<pre class="brush: js notranslate">function makePerson(first, last) {
return {
first: first,
last: last
};
}
function personFullName(person) {
return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
return person.last + ', ' + person.first;
}
var s = makePerson('Simon', 'Willison');
personFullName(s); // "Simon Willison"
personFullNameReversed(s); // "Willison, Simon"</pre>
<p>上面的写法虽然可以满足要求,但是看起来很麻烦,因为需要在全局命名空间中写很多函数。既然函数本身就是对象,如果需要使一个函数隶属于一个对象,那么不难得到:</p>
<pre class="brush: js notranslate">function makePerson(first, last) {
return {
first: first,
last: last,
fullName: function() {
return this.first + ' ' + this.last;
},
fullNameReversed: function() {
return this.last + ', ' + this.first;
}
}
}
s = makePerson("Simon", "Willison");
s.fullName(); // "Simon Willison"
s.fullNameReversed(); // Willison, Simon</pre>
<p>上面的代码里有一些我们之前没有见过的东西:关键字 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this"><code>this</code></a>。当使用在函数中时,<code>this</code> 指代当前的对象,也就是调用了函数的对象。如果在一个对象上使用<a href="https://developer.mozilla.org/en/JavaScript/Reference/Operators/Member_Operators">点或者方括号</a>来访问属性或方法,这个对象就成了 <code>this</code>。如果并没有使用“点”运算符调用某个对象,那么 <code>this</code> 将指向全局对象(global object)。这是一个经常出错的地方。例如:</p>
<pre class="brush: js notranslate">s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName(); // undefined undefined
</pre>
<p>当我们调用 <code>fullName()</code> 时,<code>this</code> 实际上是指向全局对象的,并没有名为 <code>first</code> 或 <code>last</code> 的全局变量,所以它们两个的返回值都会是 <code>undefined</code>。</p>
<p>下面使用关键字 <code>this</code> 改进已有的 <code>makePerson</code>函数:</p>
<pre class="brush: js notranslate">function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = function() {
return this.first + ' ' + this.last;
}
this.fullNameReversed = function() {
return this.last + ', ' + this.first;
}
}
var s = new Person("Simon", "Willison");
</pre>
<p>我们引入了另外一个关键字:<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new"><code>new</code></a>,它和 <code>this</code> 密切相关。它的作用是创建一个崭新的空对象,然后使用指向那个对象的 <code>this</code> 调用特定的函数。注意,含有 <code>this</code> 的特定函数不会返回任何值,只会修改 <code>this</code> 对象本身。<code>new</code> 关键字将生成的 <code>this</code> 对象返回给调用方,而被 <code>new</code> 调用的函数称为构造函数。习惯的做法是将这些函数的首字母大写,这样用 <code>new</code> 调用他们的时候就容易识别了。</p>
<p>不过,这个改进的函数还是和上一个例子一样,在单独调用<code>fullName()</code> 时,会产生相同的问题。</p>
<p>我们的 Person 对象现在已经相当完善了,但还有一些不太好的地方。每次我们创建一个 Person 对象的时候,我们都在其中创建了两个新的函数对象——如果这个代码可以共享不是更好吗?</p>
<pre class="brush: js notranslate">function personFullName() {
return this.first + ' ' + this.last;
}
function personFullNameReversed() {
return this.last + ', ' + this.first;
}
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = personFullName;
this.fullNameReversed = personFullNameReversed;
}
</pre>
<p>这种写法的好处是,我们只需要创建一次方法函数,在构造函数中引用它们。那是否还有更好的方法呢?答案是肯定的。</p>
<pre class="brush: js notranslate">function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.fullName = function() {
return this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed = function() {
return this.last + ', ' + this.first;
}
</pre>
<p><code>Person.prototype</code> 是一个可以被<code>Person</code>的所有实例共享的对象。它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问 <code>Person </code>某个实例(例如上个例子中的s)一个没有定义的属性时,解释器会首先检查这个 <code>Person.prototype</code> 来判断是否存在这样一个属性。所以,任何分配给 <code>Person.prototype</code> 的东西对通过 <code>this</code> 对象构造的实例都是可用的。</p>
<p>这个特性功能十分强大,JavaScript 允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时(runtime)给已存在的对象添加额外的方法:</p>
<pre class="brush: js notranslate">s = new Person("Simon", "Willison");
s.firstNameCaps(); // TypeError on line 1: s.firstNameCaps is not a function
Person.prototype.firstNameCaps = function() {
return this.first.toUpperCase()
}
s.firstNameCaps(); // SIMON
</pre>
<p>有趣的是,你还可以给 JavaScript 的内置函数原型(prototype)添加东西。让我们给 <code>String</code> 添加一个方法用来返回逆序的字符串:</p>
<pre class="brush: js notranslate">var s = "Simon";
s.reversed(); // TypeError on line 1: s.reversed is not a function
String.prototype.reversed = function() {
var r = "";
for (var i = this.length - 1; i >= 0; i--) {
r += this[i];
}
return r;
}
s.reversed(); // nomiS
</pre>
<p>定义新方法也可以在字符串字面量上用(string literal)。</p>
<pre class="brush: js notranslate">"This can now be reversed".reversed(); // desrever eb won nac sihT
</pre>
<p>正如我前面提到的,原型组成链的一部分。那条链的根节点是 <code>Object.prototype</code>,它包括 <code>toString()</code> 方法——将对象转换成字符串时调用的方法。这对于调试我们的 <code>Person</code> 对象很有用:</p>
<pre class="brush: js notranslate">var s = new Person("Simon", "Willison");
s; // [object Object]
Person.prototype.toString = function() {
return '<Person: ' + this.fullName() + '>';
}
s.toString(); // <Person: Simon Willison>
</pre>
<p>你是否还记得之前我们说的 <code>avg.apply()</code> 中的第一个参数 <code>null</code>?现在我们可以回头看看这个东西了。<code>apply()</code> 的第一个参数应该是一个被当作 <code>this</code> 来看待的对象。下面是一个 <code>new</code> 方法的简单实现:</p>
<pre class="brush: js notranslate">function trivialNew(constructor, ...args) {
var o = {}; // 创建一个对象
constructor.apply(o, args);
return o;
}
</pre>
<p>这并不是 <code>new</code> 的完整实现,因为它没有创建原型(prototype)链。想举例说明 new 的实现有些困难,因为你不会经常用到这个,但是适当了解一下还是很有用的。在这一小段代码里,<code>...args</code>(包括省略号)叫作<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/rest_parameters">剩余参数(rest arguments)</a>。如名所示,这个东西包含了剩下的参数。</p>
<p>因此,调用</p>
<pre class="brush: js notranslate">var bill = trivialNew(Person, "William", "Orange");</pre>
<p>可认为和调用如下语句是等效的</p>
<pre class="brush: js notranslate">var bill = new Person("William", "Orange");</pre>
<p><code>apply()</code> 有一个姐妹函数,名叫 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call"><code>call</code></a>,它也可以允许你设置 <code>this</code>,但它带有一个扩展的参数列表而不是一个数组。</p>
<pre class="brush: js notranslate">function lastNameCaps() {
return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// 和以下方式等价
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();
</pre>
<h3 id="内部函数">内部函数</h3>
<p>JavaScript 允许在一个函数内部定义函数,这一点我们在之前的 <code>makePerson()</code> 例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是,它们可以访问父函数作用域中的变量:</p>
<pre class="brush: js notranslate">function parentFunc() {
var a = 1;
function nestedFunc() {
var b = 4; // parentFunc 无法访问 b
return a + b;
}
return nestedFunc(); // 5
}</pre>
<p>如果某个函数依赖于其他的一两个函数,而这一两个函数对你其余的代码没有用处,你可以将它们嵌套在会被调用的那个函数内部,这样做可以减少全局作用域下的函数的数量,这有利于编写易于维护的代码。</p>
<p>这也是一个减少使用全局变量的好方法。当编写复杂代码时,程序员往往试图使用全局变量,将值共享给多个函数,但这样做会使代码很难维护。内部函数可以共享父函数的变量,所以你可以使用这个特性把一些函数捆绑在一起,这样可以有效地防止“污染”你的全局命名空间——你可以称它为“局部全局(local global)”。虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。</p>
<h2 id="闭包">闭包</h2>
<p>闭包是 JavaScript 中最强大的抽象概念之一——但它也是最容易造成困惑的。它究竟是做什么的呢?</p>
<pre class="brush: js notranslate">function makeAdder(a) {
return function(b) {
return a + b;
}
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?
</pre>
<p><code>makeAdder</code> 这个名字本身,便应该能说明函数是用来做什么的:它会用一个参数来创建一个新的“adder”函数,再用另一个参数来调用被创建的函数时,<code>makeAdder</code> 会将一前一后两个参数相加。</p>
<p>从被创建的函数的视角来看的话,这两个参数的来源问题会更显而易见:新函数自带一个参数——在新函数被创建时,便钦定、钦点了前一个参数(如上方代码中的 a、5 和 20,参考 <code>makeAdder</code> 的结构,它应当位于新函数外部);新函数被调用时,又接收了后一个参数(如上方代码中的 b、6 和 7,位于新函数内部)。最终,新函数被调用的时候,前一个参数便会和由外层函数传入的后一个参数相加。</p>
<p>这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数已经返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则 <code>adder</code> 函数将不能工作。也就是说,这里存在 <code>makeAdder</code> 的局部变量的两个不同的“副本”——一个是 <code>a</code> 等于 5,另一个是 <code>a</code> 等于 20。那些函数的运行结果就如下所示:</p>
<pre class="brush: js notranslate">add5(6); // 返回 11
add20(7); // 返回 27
</pre>
<p>下面来说说,到底发生了什么了不得的事情。每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它使用一切被传入函数的变量进行初始化(初始化后,它包含一切被传入函数的变量)。这与那些保存的所有全局变量和函数的全局对象(global object)相类似,但仍有一些很重要的区别:第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(如浏览器的 <code>window</code> 对象)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有 可以遍历当前作用域对象中的属性 的方法。</p>
<p>所以,当调用 <code>makeAdder</code> 时,解释器创建了一个作用域对象,它带有一个属性:<code>a</code>,这个属性被当作参数传入 <code>makeAdder</code> 函数。然后 <code>makeAdder</code> 返回一个新创建的函数(暂记为 <code>adder</code>)。通常,JavaScript 的垃圾回收器会在这时回收 <code>makeAdder</code> 创建的作用域对象(暂记为 <code>b</code>),但是,<code>makeAdder</code> 的返回值,新函数 <code>adder</code>,拥有一个指向作用域对象 <code>b</code> 的引用。最终,作用域对象 <code>b</code> 不会被垃圾回收器回收,直到没有任何引用指向新函数 <code>adder</code>。</p>
<p>作用域对象组成了一个名为作用域链(scope chain)的(调用)链。它和 JavaScript 的对象系统使用的原型(prototype)链相类似。</p>
<p>一个<strong>闭包</strong>,就是 一个函数 与其 被创建时所带有的作用域对象 的组合。闭包允许你保存状态——所以,它们可以用来代替对象。<a href="http://stackoverflow.com/questions/111102/how-do-javascript-closures-work">这个 StackOverflow 帖子里</a>有一些关于闭包的详细介绍。</p>
|