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
|
---
title: 深入了解物件模型
slug: Web/JavaScript/Guide/Details_of_the_Object_Model
translation_of: Web/JavaScript/Guide/Details_of_the_Object_Model
---
<p>{{jsSidebar("JavaScript Guide")}} {{PreviousNext("Web/JavaScript/Guide/Working_with_Objects", "Web/JavaScript/Guide/Iterators_and_Generators")}}</p>
<p>JavaScript 是一種基於原型,而不是基於類的物件導向語言。由於這個根本的區別,使它在如何創建物件的層級結構,以及如何繼承屬性和它的值上,顯得有點模糊。本文將闡明這個問題。</p>
<p>本文假設您已經有點 JavaScript 的基礎,並且用 JavaScript 的函數創建過簡單的物件。</p>
<h2 id="class-based_vs_prototype-based_languages">基於類 (Class-Based) 與 基於原型 (Prototype-Based) 語言的比較</h2>
<p>基於類的物件導向語言,比如 Java 和 C++,是建基於兩種概念之上:類和實例。</p>
<ul>
<li><em>類(Class)</em>:定義了某一種物件所擁有的屬性(可以將 Java 中的方法和屬性值,以及 C++ 中的成員視為屬性)。類是抽象的事物,而不是其所描述的物件中的特定成員。例如 <code>Employee</code> 類可以用來代表所有僱員。</li>
<li><em>實例(Instance)</em>:是類產生的實體,或者說,是它的一個成員。例如, <code>Victoria</code> 可以是 <code>Employee</code> 類的一個實例,表示一個特定的僱員個體。實例具有其類完全一致的屬性(不多也不少)。</li>
</ul>
<p>基於原型的語言(例如 JavaScript)並不存在這種區別:它只有物件。基於原型的語言具有所謂<em>原型物件(Prototypical Object)</em>的概念。新物件在初始化時以原型物件為範本獲得屬性。任何物件都可以指定其自身的屬性,在創建時或運行時都可以。而且,任何物件都可以關聯為另一個物件的<em>原型(Prototype)</em>,從而允許後者共用前者的屬性。</p>
<h3 id="定義類">定義類</h3>
<p>在基於類的語言中,類被定義在分開的<em>類定義(Class Definition)</em>。在類定義中,允許定義特殊的方法,稱為<em>建構函數(Constructor)</em>,用以創建該類的實例。建構函數可以指定實例屬性的初始值,以及它的初始化處理。使用 <code>new</code> 操作符和建構函數來創建類的實例。</p>
<p>JavaScript 也遵循類似的模型,但卻沒有類定義。JavaScript 使用建構函數來定義物件的屬性及初始值,所有的 JavaScript 函數都可以作為建構函數。使用 <code>new</code> 操作符來建立實例。</p>
<h3 id="子類_(Subclass)_和繼承_(Inheritance)">子類 (Subclass) 和繼承 (Inheritance)</h3>
<p>基於類的語言是通過類定義來構建類的層級結構。在類定義中,可以指定新的類為一個現存的類的<em>子類</em>。子類將繼承超類的全部屬性,並可以添加新的屬性或者修改繼承的屬性。例如,假設 <code>Employee</code> 類只有 <code>name</code> 和 <code>dept</code> 屬性,而 <code>Manager</code> 是 <code>Employee</code> 的子類並添加了 <code>reports</code> 屬性。這時,<code>Manager</code> 類的實例將具有三個屬性:<code>name,</code><code>dept 和</code> <code>reports</code>。</p>
<p>JavaScript 的繼承通過關聯另一個有構建函數的原型物件來實現,這樣,您可以創建完全一樣的 <code>Employee</code> — <code>Manager</code> 範例,不過使用的方法略有不同。首先,定義 <code>Employee</code> 構建函數,指定 <code>name</code> 和 <code>dept</code> 屬性。然後,定義 <code>Manager</code> 構建函數,指定 <code>reports</code> 屬性。最後,將一個新的 <code>Employee</code> 物件賦值給 <code>Manager</code> 構建函數的 <code>prototype</code> 屬性。這樣,當創建一個新的 <code>Manager</code> 物件時,它將從 <code>Employee</code> 物件中繼承 <code>name</code> 及 <code>dept</code> 屬性。</p>
<h3 id="添加和移除屬性">添加和移除屬性</h3>
<p>在基於類的語言中,通常要在編譯時建立類,然後在編譯時或者運行時產生實例。一旦定義了類,便無法改變類的屬性數目或者類型。但在 JavaScript 中,允許運行時增加或者移除任何物件的屬性。如果在物件的原型物件中增加屬性,則以該物件作為原型的所有物件也將獲得該屬性。</p>
<h3 id="區別摘要">區別摘要</h3>
<p>下面的表格列出了上述區別。本節的後續部分將描述有關使用 JavaScript 構建函數和原型創建物件層級結構的詳細資訊,並與在 Java 中的做法做對比。</p>
<table>
<caption>表 8.1 基於類(Java)和基於原型(JavaScript)的物件系統的比較</caption>
<thead>
<tr>
<th scope="col">基於類的(Java)</th>
<th scope="col">基於原型的(JavaScript)</th>
</tr>
</thead>
<tbody>
<tr>
<td>類和實例是不同的事物。</td>
<td>所有物件均為實例。</td>
</tr>
<tr>
<td>通過類定義來定義類;通過建構函數來產生實體。</td>
<td>通過建構函數來定義和創建一組物件。</td>
</tr>
<tr>
<td>通過 <code>new</code> 操作符來創建物件。</td>
<td>相同。</td>
</tr>
<tr>
<td>通過類定義來定義現存類的子類,從而建構物件的層級結構。</td>
<td>通過將一個物件作為原型指定關聯於建構函數來建構物件的層級結構。</td>
</tr>
<tr>
<td>遵循類鏈繼承屬性。</td>
<td>遵循原型鏈繼承屬性。</td>
</tr>
<tr>
<td>類定義指定類的所有實例的<em>所有</em>屬性。無法在運行時添加屬性。</td>
<td>建構函數或原型指定初始的屬性集。允許動態地向單個的物件或者整個物件集中添加屬性,或者從中移除屬性。</td>
</tr>
</tbody>
</table>
<h2 id="僱員示例">僱員示例</h2>
<p>本節的餘下部分將使用如下圖所示的僱員層級結構。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4452/=figure8.1.png"></p>
<p><small><strong>圖例 8.1:一個簡單的物件層級</strong></small></p>
<p>該示例中使用以下物件:</p>
<ul>
<li><code>Employee</code> 具有 <code>name</code> 屬性(其值預設為空的字串)和 <code>dept</code> 屬性(其值預設為 "general")。</li>
<li><code>Manager</code> 基於 <code>Employee。它添加了</code> <code>reports</code> 屬性(其值預設為空的陣列,意在以 <code>Employee</code> 物件的陣列作為它的值)。</li>
<li><code>WorkerBee</code> 同樣基於 <code>Employee</code>。它添加了 <code>projects</code> 屬性(其值預設為空的陣列,意在以字串陣列作為它的值)。</li>
<li><code>SalesPerson</code> 基於 <code>WorkerBee</code>。它添加了 <code>quota</code> 屬性(其值預設為 100)。它還重載了 <code>dept</code> 屬性值為 "sales",表明所有的銷售人員都屬於同一部門。</li>
<li><code>Engineer</code> 基於 <code>WorkerBee</code>。它添加了 <code>machine</code> 屬性(其值預設為空的字串)同時重載了 <code>dept</code> 屬性值為 "engineering"。</li>
</ul>
<h2 id="創建層級結構">創建層級結構</h2>
<p>有幾種不同的方式,可以用於定義適當的建構函數,藉以實現僱員的層級結構。如何選擇很大程度上取決於您希望在您的應用程式中能做到什麼。</p>
<p>本節展現了如何使用非常簡單的(同時也是相當不靈活的)定義,使得繼承得以實現。在這些定義中,無法在創建物件時指定屬性的值。新創建的物件僅僅獲得了預設值,當然允許隨後加以修改。圖例 8.2 展現了這些簡單的定義形成的層級結構。</p>
<p>在 真實的應用程式中,您很可能想定義允許在創建物件時給出屬性值的建構函數。(參見 <a href="#更靈活的建構函數">更靈活的建構函數</a> 獲得進一步的資訊)。對於現在而言,這些簡單的定義示範了繼承是如何發生的。</p>
<p><img alt="figure8.2.png" src="/@api/deki/files/4390/=figure8.2.png"><br>
<small><strong>圖例 8.2:Employee 物件定義</strong></small></p>
<p>以下 <code>Employee</code> 的 Java 和 JavaScript 的定義相類似。唯一的不同是在 Java 中需要指定每個屬性的類型,而在 JavaScript 中則不必指定,同時 Java 的類必須創建一個顯式的建構函數方法。</p>
<table class="standard-table">
<thead>
<tr>
<th scope="col">JavaScript</th>
<th scope="col">Java</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre class="brush: js">
function Employee () {
this.name = "";
this.dept = "general";
}
</pre>
</td>
<td>
<pre class="brush: java">
public class Employee {
public String name;
public String dept;
public Employee () {
this.name = "";
this.dept = "general";
}
}
</pre>
</td>
</tr>
</tbody>
</table>
<p><code>Manager</code> 和 <code>WorkerBee</code> 的定義顯示了在如何指定繼承鏈中上一層物件方面的不同點。在 JavaScript 中,需要為建構函數的 <code>prototype</code> 屬性添加一個原型實例作為它的屬性值。您可以在定義了建構函數之後的任何時間添加這一屬性。而在 Java 中,則需要在類定義中指定超類,且不能在類定義之外改變超類。</p>
<table class="standard-table">
<thead>
<tr>
<th scope="col">JavaScript</th>
<th scope="col">Java</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre class="brush: js">
function Manager () {
this.reports = [];
}
Manager.prototype = new Employee;
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
</pre>
</td>
<td>
<pre class="brush: java">
public class Manager extends Employee {
public Employee[] reports;
public Manager () {
this.reports = new Employee[0];
}
}
public class WorkerBee extends Employee {
public String[] projects;
public WorkerBee () {
this.projects = new String[0];
}
}
</pre>
</td>
</tr>
</tbody>
</table>
<p><code>Engineer</code> 和 <code>SalesPerson</code> 的定義創建了派生自 <code>WorkerBee</code> 進而派生自 <code>Employee</code> 的物件。這些類型的物件將具有在這個鏈之上的所有物件的屬性。同時,這些定義重載了繼承的 <code>dept</code> 屬性值,賦予這些屬性特定於這些物件的新的屬性值。</p>
<table class="standard-table">
<thead>
<tr>
<th scope="col">JavaScript</th>
<th scope="col">Java</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre class="brush: js">
function SalesPerson () {
this.dept = "sales";
this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;
function Engineer () {
this.dept = "engineering";
this.machine = "";
}
Engineer.prototype = new WorkerBee;
</pre>
</td>
<td>
<pre class="brush: java">
public class SalesPerson extends WorkerBee {
public double quota;
public SalesPerson () {
this.dept = "sales";
this.quota = 100.0;
}
}
public class Engineer extends WorkerBee {
public String machine;
public Engineer () {
this.dept = "engineering";
this.machine = "";
}
}
</pre>
</td>
</tr>
</tbody>
</table>
<p>使用這些定義,可以創建這些物件的實例。這些實例將獲得其屬性的預設值。圖例 8.3 展現了使用這些 JavaScript 定義創建新定義,並顯示了新物件中的屬性值。</p>
<p>{{ note('術語 <em><em>實例(instance)</em></em>在 基於類的語言中具有特定的技術含義。在這些語言中,實例是指類的個體成員,與類有著根本性的不同。在 JavaScript 中,“實例”並不具有這種技術含義,因為 JavaScript 中不存在類和實例之間的這種差異。然而,在談論 JavaScript 時,“實例”可以非正式地用於表示用特定的建構函數創建的物件。所以,在這個例子中,你可以非正式地 <code>jane</code> 是 <code>Engineer</code> 的一個實例。與之類似,儘管術語<em>父(parent)</em>,<em>子(child)</em>,<em>祖先(ancestor)</em>,和<em>後代(descendant)</em>在 JavaScript 中並沒有正式的含義,您可以非正式地使用這些術語用於指代原型鏈中處於更高層次或者更低層次的物件。') }}</p>
<p><img alt="figure8.3.png" src="/@api/deki/files/4403/=figure8.3.png"><br>
<small><strong>圖例 8.3:通過簡單的定義創建物件</strong></small></p>
<h2 id="物件的屬性">物件的屬性</h2>
<p>本節將討論物件如何從原型鏈中的其它物件中繼承屬性,以及在運行時添加屬性的相關細節。</p>
<h3 id="繼承屬性">繼承屬性</h3>
<p>假設通過如下語句創建一個 <code>mark</code> 物件作為 <code>WorkerBee</code>(如 <a href="#8.3">圖例 8.3</a> 所示):</p>
<pre class="brush: js">var mark = new WorkerBee;
</pre>
<p>當 JavaScript 發現 <code>new</code> 操作符,它將創建一個普通的物件,並將其作為關鍵字 <code>this</code> 的值傳遞給 <code>WorkerBee</code> 的建構函數。該建構函數顯式地設置 <code>projects</code> 屬性的值,然後隱式地將其內部的 <code>__proto__</code> 屬性設置為 <code>WorkerBee.prototype</code> 的值(屬性的名稱前後均有兩個底線)。<code>__proto__</code> 屬性決定了用於返回屬性值的原型鏈。一旦這些屬性得以設置,JavaScript 返回新創建的物件,然會設定陳述式設置變數 <code>mark</code> 的值為該物件。</p>
<p>這個過程不會顯式地為 <code>mark</code> 物件從原型鏈中所繼承的屬性設置值(本地值)。當請求屬性的值時,JavaScript 將首先檢查物件自身中是否設置了該屬性的值,如果有,則返回該值。如果本地值不存在,則 JavaScript 將檢查原型鏈(通過 <code>__proto__</code> 屬性)。如果原型鏈中的某個物件具有該屬性的值,則返回這個值。如果沒有找到該屬性,JavaScript 則認為物件中不存在該屬性。這樣,<code>mark</code> 物件中將具有如下的屬性和對應的值:</p>
<pre class="brush: js">mark.name = "";
mark.dept = "general";
mark.projects = [];
</pre>
<p><code>mark</code> 對象從 <code>mark.__proto__</code> 中保存的原型物件中繼承了 <code>name</code> 和 <code>dept</code> 屬性的值。並由 <code>WorkerBee</code> 建構函數為 <code>projects</code> 屬性設置了本地值。 這就是 JavaScript 中的屬性和屬性值的繼承。這個過程的一些微妙之處將在 <a href="#再談屬性繼承">再談屬性繼承</a> 中進一步討論。</p>
<p>由於這些建構函數不支援設置實例特定的值,所以,這些屬性值僅僅是泛泛地由創建自 <code>WorkerBee</code> 的所有物件所共用的預設值。當然,允許修改這些屬性的值。所以,您也可以為這些屬性指定特定的值,如下所示:</p>
<pre class="brush: js">mark.name = "Doe, Mark";
mark.dept = "admin";
mark.projects = ["navigator"];</pre>
<h3 id="添加屬性">添加屬性</h3>
<p>在 JavaScript 中,可以在運行時為任何物件添加屬性,而不必受限於建構函數提供的屬性。添加特定於某個物件的屬性,只需要為該物件指定一個屬性值,如下所示:</p>
<pre class="brush: js">mark.bonus = 3000;
</pre>
<p>這樣 <code>mark</code> 物件就有了 <code>bonus</code> 屬性,而其它 <code>WorkerBee</code> 則沒有該屬性。</p>
<p>如果向某個建構函數的原型物件中添加新的屬性,則該屬性將添加到從這個原型中繼承屬性的所有物件的中。例如,可以通過如下的語句向所有僱員中添加 <code>specialty</code> 屬性:</p>
<pre class="brush: js">Employee.prototype.specialty = "none";
</pre>
<p>一旦 JavaScript 執行該語句,則 <code>mark</code> 物件也將具有 <code>specialty</code> 屬性,其值為 <code>"none"</code>。下圖展現了在 <code>Employee</code> 原型中添加該屬性,然後在 <code>Engineer</code> 的原型中重載該屬性的效果。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4422/=figure8.4.png"><br>
<small><strong>Figure 8.4: Adding properties</strong></small></p>
<h2 id="more_flexible_constructors">更靈活的建構函數</h2>
<p>到目前為止所展現的建構函數不允許在創建新的實例時指定屬性值。正如 Java 一樣,可以為建構函數提供參數以便初始化實例的屬性值。下圖展現其中一種做法。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4423/=figure8.5.png"><br>
<small><strong>Figure 8.5: Specifying properties in a constructor, take 1</strong></small></p>
<p>下面的表格中羅列了這些物件在 Java 和 JavaScript 中的定義。</p>
<table class="standard-table">
<thead>
<tr>
<th scope="col">JavaScript</th>
<th scope="col">Java</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre class="brush: js">
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
}
</pre>
</td>
<td>
<pre class="brush: java">
public class Employee {
public String name;
public String dept;
public Employee () {
this("", "general");
}
public Employee (String name) {
this(name, "general");
}
public Employee (String name, String dept) {
this.name = name;
this.dept = dept;
}
}
</pre>
</td>
</tr>
<tr>
<td>
<pre class="brush: js">
function WorkerBee (projs) {
this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
</pre>
</td>
<td>
<pre class="brush: java">
public class WorkerBee extends Employee {
public String[] projects;
public WorkerBee () {
this(new String[0]);
}
public WorkerBee (String[] projs) {
projects = projs;
}
}
</pre>
</td>
</tr>
<tr>
<td>
<pre class="brush: js">
function Engineer (mach) {
this.dept = "engineering";
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
</pre>
</td>
<td>
<pre class="brush: java">
public class Engineer extends WorkerBee {
public String machine;
public Engineer () {
dept = "engineering";
machine = "";
}
public Engineer (String mach) {
dept = "engineering";
machine = mach;
}
}
</pre>
</td>
</tr>
</tbody>
</table>
<p>這些 JavaScript 定義使用了設置預設值的一種特殊慣用法:</p>
<pre class="brush: js">this.name = name || "";
</pre>
<p>JavaScript 的邏輯 OR 操作符(<code>||)</code>將求解它的第一個參數。如果該參數的值可以轉換為真,則操作符返回該值。否則,操作符返回第二個參數的值。因此,這行代碼首先檢查 <code>name</code> 是否具有一個對 <code>name</code> 屬性有用的值。如果有,則設置其為 <code>this.name</code> 的值。否則,設置 <code>this.name</code> 的值為空的字串。為求簡潔,本章將使用這一慣用法,儘管咋一看它有些費解。</p>
<p>{{ note('如果調用建構函數時,指定了可以轉換為 <code><code>false</code></code> 的參數(比如 <code>0</code> (零)和空字串<code><code>("")),結果可能出乎調用者意料。此時,將使用預設值(譯者注:而不是指定的參數值 0 和 "")。</code></code>') }}</p>
<p>基於這些定義,當創建物件的實例時,可以為本地定義的屬性指定值。正如 <a href="#8.5">圖例 8.5</a> 所示一樣,您可以通過如下語句創建新的 <code>Engineer</code>:</p>
<pre class="brush: js">var jane = new Engineer("belau");
</pre>
<p>此時,<code>Jane</code> 的屬性如下所示:</p>
<pre class="brush: js">jane.name == "";
jane.dept == "engineering";
jane.projects == [];
jane.machine == "belau"
</pre>
<p>基於上述定義,無法為諸如 <code>name</code> 這樣的繼承屬性指定初始值。在 JavaScript 中,如果想為繼承的屬性指定初始值,建構函數中需要更多的代碼。</p>
<p>到目前為止,建構函數已經能夠創建一個普通物件,然後為新物件指定本地的屬性和屬性值。您還可以通過直接調用原型鏈上的更高層次物件的建構函數,讓建構函數添加更多的屬性。下面的圖例展現這種新定義。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4430/=figure8.6.png"><br>
<small><strong>Figure 8.6 Specifying properties in a constructor, take 2</strong></small></p>
<p>讓我們仔細看看這些定義的其中之一。以下是 <code>Engineer</code> 建構函數的定義:</p>
<pre class="brush: js">function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
</pre>
<p>假設您創建了一個新的 <code>Engineer</code> 物件,如下所示:</p>
<pre class="brush: js">var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
</pre>
<p>JavaScript 遵循以下步驟:</p>
<ol>
<li><code>new</code> 操作符創建了一個新的普通物件,並將其 <code>__proto__</code> 屬性設置為 <code>Engineer.prototype</code>。</li>
<li><code>new</code> 操作符將該新對象作為 <code>this</code> 關鍵字的值傳遞給 <code>Engineer</code> 建構函數。</li>
<li>建構函數為該新物件創建了一個名為 <code>base</code> 的新屬性,並將 <code>WorkerBee</code> 的建構函數指定為 <code>base</code> 屬性的值。這使得 <code>WorkerBee</code> 建構函數成為 <code>Engineer</code> 物件的一個方法。<code>base</code> 屬性的名字沒有特殊性。可以使用任何合法的屬性名稱;<code>base</code> 僅僅是為了貼近它的用意。</li>
<li>
<p>建構函數調用 <code>base</code> 方法,將傳遞給該建構函數的參數中的兩個,作為參數傳遞給 <code>base</code> 方法,同時還傳遞一個字串參數 <code>"engineering"。顯式地在建構函數中使用</code> <code>"engineering"</code> 表明所有 <code>Engineer</code> 物件繼承的 <code>dept</code> 屬性具有相同的值,且該值重載了繼承自 <code>Employee</code> 的值。</p>
</li>
<li>因為 <code>base</code> 是 <code>Engineer</code> 的一個方法,在調用 <code>base</code> 時,JavaScript 將在步驟 1 中創建的對象綁定給 <code>this</code> 關鍵字。這樣,<code>WorkerBee</code> 函數接著將 <code>"Doe, Jane"</code> 和 <code>"engineering"</code> 參數傳遞給 <code>Employee</code> 建構函數。當從 <code>Employee</code> 建構函數返回時,<code>WorkerBee</code> 函數用剩下的參數設置 <code>projects</code> 屬性。</li>
<li>當從 <code>base</code> 方法返回時,<code>Engineer</code> 建構函數將物件的 <code>machine</code> 屬性初始化為 <code>"belau"</code>。</li>
<li>當從建構函數返回時,JavaScript 將新物件賦值給 <code>jane</code> 變數。</li>
</ol>
<p>您可以認為,在 <code>Engineer</code> 的建構函數中調用 <code>WorkerBee</code> 的建構函數,也就為 <code>Engineer</code> 物件設置好了適當繼承。事實並非如此。調用 <code>WorkerBee</code> 建構函數確保了<code>Engineer</code> 物件以所有被調用的建構函數中所指定的屬性作為起步。但是,如果之後在 <code>Employee</code> 或者 <code>WorkerBee</code> 原型中添加了屬性,那些屬性不會被 <code>Engineer</code> 物件繼承。例如,假設如下語句:</p>
<pre class="brush: js">function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";
</pre>
<p>物件 <code>jane</code> 不會繼承 <code>specialty</code> 屬性。必需顯式地設置原型才能確保動態的技能。假設修改為如下的語句:</p>
<pre class="brush: js">function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";
</pre>
<p>現在 <code>jane</code> 物件的 <code>specialty</code> 屬性為 "none" 了。</p>
<p>繼承的另一種途徑是使用<a href="/en-US/docs/JavaScript/Reference/Global_Objects/Function/call" title="en-US/docs/JavaScript/Reference/Global Objects/Function/call"><code>call()</code></a> / <a href="/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply" title="en-US/docs/JavaScript/Reference/Global Objects/Function/apply"><code>apply()</code></a> 方法。下面的方式都是等價的:</p>
<table>
<tbody>
<tr>
<td>
<pre class="brush: js">
function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
</pre>
</td>
<td>
<pre class="brush: js">
function Engineer (name, projs, mach) {
WorkerBee.call(this, name, "engineering", projs);
this.machine = mach || "";
}
</pre>
</td>
</tr>
</tbody>
</table>
<p>使用 javascript 的 <code>call()</code> 方法相對明瞭一些,因為無需 <code>base</code> 方法了。</p>
<h2 id="再談屬性的繼承">再談屬性的繼承</h2>
<p>前面的小節中描述了 JavaScript 建構函數和原型如何提供層級結構和繼承的實現。本節中將討論之前未曾明確的一些細微之處。</p>
<h3 id="本地的值和繼承的值">本地的值和繼承的值</h3>
<p>正如本章前面所述,在訪問一個物件的屬性時,JavaScript 將按照如下的步驟處理:</p>
<ol>
<li>檢查是否存在本地的值。如果存在,返回該值。</li>
<li>如果本地值不存在,檢查原型鏈(通過 <code>__proto__</code> 屬性)。</li>
<li>如果原型鏈中的某個物件具有指定屬性的值,則返回該值。</li>
<li>如果這樣的屬性不存在,則物件沒有該屬性。</li>
</ol>
<p>以上步驟的結果依賴於您是如何定義的。最早的例子中具有如下定義:</p>
<pre class="brush: js">function Employee () {
this.name = "";
this.dept = "general";
}
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
</pre>
<p>基於這些定義,假定通過如下的語句創建 <code>WorkerBee</code> 的實例 <code>amy:</code></p>
<pre class="brush: js">var amy = new WorkerBee;
</pre>
<p>則 <code>amy</code> 物件將具有一個本地屬性,<code>projects。</code><code>name</code> 和 <code>dept</code> 屬性則不是 <code>amy</code> 物件本地的,而是從 <code>amy</code> 物件的 <code>__proto__</code> 屬性獲得的。因此,<code>amy</code> 將具有如下的屬性值:</p>
<pre class="brush: js">amy.name == "";
amy.dept == "general";
amy.projects == [];
</pre>
<p>現在,假定修改了關聯於 <code>Employee</code> 的原型中的 <code>name</code> 屬性的值:</p>
<pre class="brush: js">Employee.prototype.name = "Unknown"
</pre>
<p>乍一看,您可能期望新的值會傳播給所有 <code>Employee</code> 的實例。然而,並非如此。</p>
<p>在創建 <code>Employee</code> 對象的 <em>任何</em> 實例時,該實例的 <code>name</code> 屬性將獲得一個本地值(空的字串)。這意味著在創建一個新的 <code>Employee</code> 物件作為 <code>WorkerBee</code> 的原型時,<code>WorkerBee.prototype</code> 的 <code>name</code> 屬性將具有一個本地值。這樣,當 JavaScript 查找 <code>amy</code> 物件(<code>WorkerBee</code> 的實例)的 <code>name</code> 屬性時,JavaScript 將找到 <code>WorkerBee.prototype</code> 中的本地值。因此,也就不會繼續在原型鏈中向上找到 <code>Employee.prototype</code> 了。</p>
<p>如果想在運行時修改物件的屬性值並且希望該值被所有該物件的後代所繼承,不能在該物件的建構函數中定義該屬性。而是應該將該屬性添加到該物件所關聯的原型中。例如,假設將前面的代碼作如下修改:</p>
<pre class="brush: js">function Employee () {
this.dept = "general";
}
Employee.prototype.name = "";
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
var amy = new WorkerBee;
Employee.prototype.name = "Unknown";
</pre>
<p>這時,<code>amy</code> 的 <code>name</code> 屬性將為 "Unknown"。</p>
<p>正如這些例子所示,如果希望物件的屬性具有預設值,且希望在運行時修改這些預設值,應該在物件的原型中設置這些屬性,而不是在建構函數中。</p>
<h3 id="判斷實例的關係">判斷實例的關係</h3>
<p>JavaScript 的屬性查找機制首先在物件自身的屬性中查找,如果指定的屬性名稱沒有找到,將在物件的特殊屬性 <code>__proto__</code> 中查找。這個過程是遞迴的;被稱為“在原型鏈中查找”。</p>
<p>特殊的 <code>__proto__</code> 屬性是在構建物件時設置的;設置為建構函數的 <code>prototype</code> 屬性的值。所以運算式 <code>new Foo()</code> 將創建一個物件,其 <code>__proto__ == <code>Foo.prototype</code></code>。因而,修改 <code>Foo.prototype</code> 的屬性,將改變所有通過 <code>new Foo()</code> 創建的物件的屬性的查找。</p>
<p>每個物件都有一個 <code>__proto__</code> 物件屬性(除了 <code>Object);每個函數都有一個</code> <code>prototype</code> 物件屬性。因此,通過“原型繼承(prototype inheritance)”,物件與其它物件之間形成關係。通過比較物件的 <code>__proto__</code> 屬性和函數的 <code>prototype</code> 屬性可以檢測物件的繼承關係。JavaScript 提供了便捷方法:<code>instanceof</code> 操作符可以用來將一個物件和一個函數做檢測,如果物件繼承自函數的原型,則該操作符返回真。例如:</p>
<pre class="brush: js">var f = new Foo();
var isTrue = (f instanceof Foo);</pre>
<p>作為詳細一點的例子,假定我們使用和在 <a href="#繼承屬性">繼承屬性</a> 中相同的一組定義。創建 <code>Engineer</code> 物件如下:</p>
<pre class="brush: js">var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
</pre>
<p>對於該物件,以下所有語句均為真:</p>
<pre class="brush: js">chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
</pre>
<p>基於此,可以寫出一個如下所示的 <code>instanceOf</code> 函數:</p>
<pre class="brush: js">function instanceOf(object, constructor) {
while (object != null) {
if (object == constructor.prototype)
return true;
if (typeof object == 'xml') {
return constructor.prototype == XML.prototype;
}
object = object.__proto__;
}
return false;
}
</pre>
<div class="notecard note">
<p><strong>備註:</strong> 在上面的實現中,檢查物件的類型是否為 "xml" 的目的在於解決新近版本的 JavaScript 中表達 XML 物件的特異之處。如果您想瞭解其中瑣碎細節,可以參考 {{ bug(634150) }}。</p>
</div>
<pre class="brush: js">instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)
</pre>
<p>但如下運算式為假:</p>
<pre class="brush: js">instanceOf (chris, SalesPerson)</pre>
<h3 id="建構函數中的全域資訊">建構函數中的全域資訊</h3>
<p>在創建建構函數時,在建構函數中設置全域資訊要小心。例如,假設希望為每一個僱員分配一個唯一標識。可能會為 <code>Employee</code> 使用如下定義:</p>
<pre class="brush: js">var idCounter = 1;
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
this.id = idCounter++;
}
</pre>
<p>基於該定義,在創建新的 <code>Employee</code> 時,建構函數為其分配了序列中的下一個識別字。然後遞增全域的識別字計數器。因此,如果,如果隨後的語句如下,則 <code>victoria.id</code> 為 1 而 <code>harry.id</code> 為 2:</p>
<pre class="brush: js">var victoria = new Employee("Pigbert, Victoria", "pubs")
var harry = new Employee("Tschopik, Harry", "sales")
</pre>
<p>乍一看似乎沒問題。但是,無論什麼目的,在每一次創建 <code>Employee</code> 對象時,<code>idCounter</code> 都將被遞增一次。如果創建本章中所描述的整個 <code>Employee</code> 層級結構,每次設置原型的時候,<code>Employee</code> 建構函數都將被調用一次。假設有如下代碼:</p>
<pre class="brush: js">var idCounter = 1;
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
this.id = idCounter++;
}
function Manager (name, dept, reports) {...}
Manager.prototype = new Employee;
function WorkerBee (name, dept, projs) {...}
WorkerBee.prototype = new Employee;
function Engineer (name, projs, mach) {...}
Engineer.prototype = new WorkerBee;
function SalesPerson (name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;
var mac = new Engineer("Wood, Mac");
</pre>
<p>還可以進一步假設上面省略掉的定義中包含 <code>base</code> 屬性而且調用了原型鏈中高於它們的建構函數。即便在現在這個情況下,在 <code>mac</code> 物件創建時,<code>mac.id</code> 為 5。</p>
<p>依賴于應用程式,計數器額外的遞增可能有問題,也可能沒問題。如果確實需要準確的計數器,則以下建構函數可以作為一個可行的方案:</p>
<pre class="brush: js">function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
if (name)
this.id = idCounter++;
}
</pre>
<p>在用作原型而創建新的 <code>Employee</code> 實例時,不會指定參數。使用這個建構函數定義,如果不指定參數,建構函數不會指定識別字,也不會遞增計數器。而如果想讓 <code>Employee</code> 分配到識別字,則必需為僱員指定姓名。在這個例子中,<code>mac.id</code> 將為 1。</p>
<h3 id="沒有多繼承">沒有多繼承</h3>
<p>某些物件導向語言支援多重繼承。也就是說,物件可以從無關的多個父物件中繼承屬性和屬性值。JavaScript 不支持多重繼承。</p>
<p>JavaScript 屬性值的繼承是在運行時通過檢索物件的原型鏈來實現的。因為物件只有一個原型與之關聯,所以 JavaScript 無法動態地從多個原型鏈中繼承。</p>
<p>在 JavaScript 中,可以在建構函數中調用多個其它的建構函數。這一點造成了多重繼承的假像。例如,考慮如下語句:</p>
<pre class="brush: js">function Hobbyist (hobby) {
this.hobby = hobby || "scuba";
}
function Engineer (name, projs, mach, hobby) {
this.base1 = WorkerBee;
this.base1(name, "engineering", projs);
this.base2 = Hobbyist;
this.base2(hobby);
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")
</pre>
<p>進一步假設使用本章前面所屬的 <code>WorkerBee</code> 的定義。此時 <code>dennis</code> 物件具有如下屬性:</p>
<pre class="brush: js">dennis.name == "Doe, Dennis"
dennis.dept == "engineering"
dennis.projects == ["collabra"]
dennis.machine == "hugo"
dennis.hobby == "scuba"
</pre>
<p><code>dennis</code> 確實從 <code>Hobbyist</code> 建構函數中獲得了 <code>hobby</code> 屬性。但是,假設添加了一個屬性到 <code>Hobbyist</code> 建構函數的原型:</p>
<pre class="brush: js">Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
</pre>
<p><code>dennis</code> 物件不會繼承這個新屬性。</p>
<div>{{ PreviousNext("JavaScript/Guide/Predefined_Core_Objects", "JavaScript/Guide/Inheritance_Revisited") }}</div>
|