-
Notifications
You must be signed in to change notification settings - Fork 0
/
2019-01-23-你不知道的JavaScript(上)——行为委托.html
1457 lines (882 loc) · 145 KB
/
2019-01-23-你不知道的JavaScript(上)——行为委托.html
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 class="theme-next mist use-motion" lang="zh-Hans">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<meta name="theme-color" content="#222">
<script src="/lib/pace/pace.min.js?v=1.0.2"></script>
<link href="/lib/pace/pace-theme-minimal.min.css?v=1.0.2" rel="stylesheet">
<meta http-equiv="Cache-Control" content="no-transform" />
<meta http-equiv="Cache-Control" content="no-siteapp" />
<link href="/lib/fancybox/source/jquery.fancybox.css?v=2.1.5" rel="stylesheet" type="text/css" />
<link href="/lib/font-awesome/css/font-awesome.min.css?v=4.6.2" rel="stylesheet" type="text/css" />
<link href="/css/main.css?v=5.1.4" rel="stylesheet" type="text/css" />
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png?v=5.1.4">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png?v=5.1.4">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png?v=5.1.4">
<link rel="mask-icon" href="/images/logo.svg?v=5.1.4" color="#222">
<meta name="keywords" content="你不知道的JavaScript," />
<link rel="alternate" href="/atom.xml" title="赖同学" type="application/atom+xml" />
<meta name="description" content="这个系列的作品是上一次当当网有活动买的,记得是上一年九月份开学季的时候了。后面一直有其他的事情,或者自身一些因素,迟迟没有开封这本书。今天立下一个 flag,希望可以在两个月内看完并记录这个系列的三本书,保持学习的激情,不断弥补自己的基础不够扎实的缺点。 作者的github 书籍的购买链接,自己搜。 你不知道的JavaScript(上)——行为委托回顾上一章的理论:[[Prototype]] 机制">
<meta name="keywords" content="你不知道的JavaScript">
<meta property="og:type" content="article">
<meta property="og:title" content="你不知道的JavaScript(上)——行为委托">
<meta property="og:url" content="http://laibh.top/2019-01-23-你不知道的JavaScript(上)——行为委托.html">
<meta property="og:site_name" content="赖同学">
<meta property="og:description" content="这个系列的作品是上一次当当网有活动买的,记得是上一年九月份开学季的时候了。后面一直有其他的事情,或者自身一些因素,迟迟没有开封这本书。今天立下一个 flag,希望可以在两个月内看完并记录这个系列的三本书,保持学习的激情,不断弥补自己的基础不够扎实的缺点。 作者的github 书籍的购买链接,自己搜。 你不知道的JavaScript(上)——行为委托回顾上一章的理论:[[Prototype]] 机制">
<meta property="og:locale" content="zh-Hans">
<meta property="og:image" content="http://laibh.top/images/2019-01-23-你不知道的JavaScript(上">
<meta property="og:image" content="http://laibh.top/images/2019-01-23-你不知道的JavaScript(上">
<meta property="og:image" content="http://laibh.top/images/2019-01-23-你不知道的JavaScript(上">
<meta property="og:updated_time" content="2022-03-04T10:00:38.460Z">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="你不知道的JavaScript(上)——行为委托">
<meta name="twitter:description" content="这个系列的作品是上一次当当网有活动买的,记得是上一年九月份开学季的时候了。后面一直有其他的事情,或者自身一些因素,迟迟没有开封这本书。今天立下一个 flag,希望可以在两个月内看完并记录这个系列的三本书,保持学习的激情,不断弥补自己的基础不够扎实的缺点。 作者的github 书籍的购买链接,自己搜。 你不知道的JavaScript(上)——行为委托回顾上一章的理论:[[Prototype]] 机制">
<meta name="twitter:image" content="http://laibh.top/images/2019-01-23-你不知道的JavaScript(上">
<script type="text/javascript" id="hexo.configurations">
var NexT = window.NexT || {};
var CONFIG = {
root: '/',
scheme: 'Mist',
version: '5.1.4',
sidebar: {"position":"left","display":"post","offset":12,"b2t":false,"scrollpercent":true,"onmobile":false},
fancybox: true,
tabs: true,
motion: {"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},
duoshuo: {
userId: '0',
author: '博主'
},
algolia: {
applicationID: '1YNH8Y3MP9',
apiKey: '61c189facf700193dfcbb902369ce227',
indexName: 'MyBlog',
hits: {"per_page":10},
labels: {"input_placeholder":"想要找些什么呢","hits_empty":"${query} 没有被找到,再试试","hits_stats":"在 ${time} ms 查找了${hits}个结果"}
}
};
</script>
<link rel="canonical" href="http://laibh.top/2019-01-23-你不知道的JavaScript(上)——行为委托.html"/>
<title>你不知道的JavaScript(上)——行为委托 | 赖同学</title>
</head>
<body itemscope itemtype="http://schema.org/WebPage" lang="zh-Hans">
<div class="container sidebar-position-left page-post-detail">
<div class="headband"></div>
<header id="header" class="header" itemscope itemtype="http://schema.org/WPHeader">
<div class="header-inner"><div class="site-brand-wrapper">
<div class="site-meta ">
<div class="custom-logo-site-title">
<a href="/" class="brand" rel="start">
<span class="logo-line-before"><i></i></span>
<span class="site-title">赖同学</span>
<span class="logo-line-after"><i></i></span>
</a>
</div>
<h1 class="site-subtitle" itemprop="description"></h1>
</div>
<div class="site-nav-toggle">
<button>
<span class="btn-bar"></span>
<span class="btn-bar"></span>
<span class="btn-bar"></span>
</button>
</div>
</div>
<nav class="site-nav">
<ul id="menu" class="menu">
<li class="menu-item menu-item-home">
<a href="/" rel="section">
<i class="menu-item-icon fa fa-fw fa-home"></i> <br />
首页
</a>
</li>
<li class="menu-item menu-item-tags">
<a href="/tags/" rel="section">
<i class="menu-item-icon fa fa-fw fa-tags"></i> <br />
标签
</a>
</li>
<li class="menu-item menu-item-categories">
<a href="/categories/" rel="section">
<i class="menu-item-icon fa fa-fw fa-th"></i> <br />
分类
</a>
</li>
<li class="menu-item menu-item-archives">
<a href="/archives/" rel="section">
<i class="menu-item-icon fa fa-fw fa-archive"></i> <br />
归档
</a>
</li>
<li class="menu-item menu-item-sitemap">
<a href="/sitemap.xml" rel="section">
<i class="menu-item-icon fa fa-fw fa-sitemap"></i> <br />
站点地图
</a>
</li>
<li class="menu-item menu-item-guestbook">
<a href="/guestbook" rel="section">
<i class="menu-item-icon fa fa-fw fa-comment"></i> <br />
留言
</a>
</li>
<li class="menu-item menu-item-search">
<a href="javascript:;" class="popup-trigger">
<i class="menu-item-icon fa fa-search fa-fw"></i> <br />
搜索
</a>
</li>
</ul>
<div class="site-search">
<div class="algolia-popup popup search-popup">
<div class="algolia-search">
<div class="algolia-search-input-icon">
<i class="fa fa-search"></i>
</div>
<div class="algolia-search-input" id="algolia-search-input"></div>
</div>
<div class="algolia-results">
<div id="algolia-stats"></div>
<div id="algolia-hits"></div>
<div id="algolia-pagination" class="algolia-pagination"></div>
</div>
<span class="popup-btn-close">
<i class="fa fa-times-circle"></i>
</span>
</div>
</div>
</nav>
</div>
</header>
<main id="main" class="main">
<div class="main-inner">
<div class="content-wrap">
<div id="content" class="content">
<div id="posts" class="posts-expand">
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://laibh.top/2019-01-23-你不知道的JavaScript(上)——行为委托.html">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="赖彬鸿">
<meta itemprop="description" content="">
<meta itemprop="image" content="/images/myPhoto.jpg">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="赖同学">
</span>
<header class="post-header">
<h2 class="post-title" itemprop="name headline">你不知道的JavaScript(上)——行为委托</h2>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2019-01-23T18:30:00+08:00">
2019-01-23
</time>
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-calendar-check-o"></i>
</span>
<span class="post-meta-item-text">更新于:</span>
<time title="更新于" itemprop="dateModified" datetime="2022-03-04T18:00:38+08:00">
2022-03-04
</time>
</span>
<span class="post-category" >
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/JavaScript/" itemprop="url" rel="index">
<span itemprop="name">JavaScript</span>
</a>
</span>
</span>
<span id="/2019-01-23-你不知道的JavaScript(上)——行为委托.html" class="leancloud_visitors" data-flag-title="你不知道的JavaScript(上)——行为委托">
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-eye"></i>
</span>
<span class="post-meta-item-text">阅读次数:</span>
<span class="leancloud-visitors-count"></span>
</span>
<div class="post-wordcount">
<span class="post-meta-item-icon">
<i class="fa fa-file-word-o"></i>
</span>
<span class="post-meta-item-text">字数统计:</span>
<span title="字数统计">
10,797
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p>这个系列的作品是上一次当当网有活动买的,记得是上一年九月份开学季的时候了。后面一直有其他的事情,或者自身一些因素,迟迟没有开封这本书。今天立下一个 flag,希望可以在两个月内看完并记录这个系列的三本书,保持学习的激情,不断弥补自己的基础不够扎实的缺点。</p>
<p><a href="https://github.com/getify/You-Dont-Know-JS">作者的github</a></p>
<p>书籍的购买链接,自己搜。</p>
<h1 id="你不知道的JavaScript-上-——行为委托"><a href="#你不知道的JavaScript-上-——行为委托" class="headerlink" title="你不知道的JavaScript(上)——行为委托"></a>你不知道的JavaScript(上)——行为委托</h1><p>回顾上一章的理论:[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象。</p>
<p>如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的 [[Prototype]] ,以此类推,这一系列对象的链接被称为“原型链”。</p>
<p>换句话说,JavaScript 中这个机制的本质就是对象之间的关联关系。</p>
<h2 id="面向委托的设计"><a href="#面向委托的设计" class="headerlink" title="面向委托的设计"></a>面向委托的设计</h2><p>试着把思路从类和继承的设计模式转换到委托行为的设计模式。</p>
<h3 id="类理论"><a href="#类理论" class="headerlink" title="类理论"></a>类理论</h3><p>假设我们需要在软件中建模一些类似的任务(“XYZ”,“ABC”等)。</p>
<p>如果使用类,那设计方法可能是这样的:定义一个通用父(基)类,可以将其命名为 Task,在 Task 类中定义所有任务都有的行为。接着定义子类 XYZ 和 ABC,它们都继承自 Task 并且添加一些特殊的行为来处理对应的任务。</p>
<p>非常重要的是,类设计模式鼓励你在继承时使用方法重写(和多态),比如在 XYZ 任务中重写 Task 中定义的一些通用的方法,甚至在添加新行为时通过 super 调用这个方法的原始版本。你会发现许多行为可以先 “抽象”到父类然后再用子类进行特殊化(重写)。</p>
<p>下面的是对应的伪代码:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">class Task{</span><br><span class="line"> id:</span><br><span class="line"> // 构造函数 Task()</span><br><span class="line"> Task(ID){id = ID}</span><br><span class="line"> outputTask(){output(id);}</span><br><span class="line">}</span><br><span class="line">class XYZ inherits Task{</span><br><span class="line"> label;</span><br><span class="line"> // 构造函数 XYZ()</span><br><span class="line"> XYZ(ID,Label){super(ID);label = Label}</span><br><span class="line"> outputTask(){super(); output(label)}</span><br><span class="line">}</span><br><span class="line">class ABC inherits Task{</span><br><span class="line"> //..</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>现在可以实例化子类 XYZ 的一些副本然后使用这些实例来执行任务 “XYZ”。这些实例会复制 Task 定义的通用行为以及 XYZ 定义的特殊行为。同理,ABC 类的实例也会复制 Task 的行为和 ABC 的行为。在构造完成后,你通常只需要操作这些实例(而不是类),因为每个实例都有你需要完成任务的所有行为。</p>
<h3 id="委托理论"><a href="#委托理论" class="headerlink" title="委托理论"></a>委托理论</h3><p>现在我们试着用委托行为来思考同样的问题。</p>
<p>首先你会定义一个名为 Task 的对象(它既不是对象也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着,对于每个任务(“XYZ”,”ABC”)你都会定义一个对象来存储对应的数据和行为。把特定的任务对象都关联到 Task 功能对象上,让它们在需要的时候可以进行委托。</p>
<p>基本上可以想象成,执行任务的 “XYZ” 需要两个对象(XYZ 和 Task)协作完成,但是我们并不需要这些行为放在一起,通过类的复制,我们可以把它们分别放在各自独立的对象中,需要时可以运行 XYZ 对象委托给 Task。</p>
<p>下面是推荐的代码形式,非常简单:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">Task = {</span><br><span class="line"> setID:<span class="function"><span class="keyword">function</span>(<span class="params">ID</span>)</span>{<span class="keyword">this</span>.id = ID},</span><br><span class="line"> outputID:<span class="function"><span class="keyword">function</span>)</span>{<span class="built_in">console</span>.log(<span class="keyword">this</span>.id)}</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 让 XYZ 委托 Task</span></span><br><span class="line">XYZ = <span class="built_in">Object</span>.create(Task);</span><br><span class="line"></span><br><span class="line">XYZ.prepareTask = <span class="function"><span class="keyword">function</span>(<span class="params">ID,Label</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.setID();</span><br><span class="line"> <span class="keyword">this</span>.label = Label;</span><br><span class="line">}</span><br><span class="line">XYZ.outputTaskDetails = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.outputID();</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="keyword">this</span>.label);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// ABC = Object.create(Task);</span></span><br><span class="line"><span class="comment">// ABC .. = ...</span></span><br></pre></td></tr></table></figure>
<p>这段代码中,Task 和 XYZ 并不是类(或者函数),它们是对象。XYZ 通过 Object.create(..) 创建,它的 [[Prototype]] 委托了对象。</p>
<p>相比于面向类(或者说面向对象),作者会把这种编码风格称为“对象关联”(OLOO,object linked to other objects)。我们真正关心的是 XYZ 对象(和 ABC 对象)委托了 Task 对象。</p>
<p>在 JavaScript 中,[[Prototype]] 机制会把对象关联到其他对象。</p>
<p>对象关联风格的代码还有一些不足之处:</p>
<ol>
<li><p>在上面的例子中,id 和 label 数据成员都是直接存在 XYZ 上(而不是 Task),通常来说,[[Prototype]] 委托中最好把状态保存在委托者(XYZ、ABC)而不是委托目标(Task)上。</p>
</li>
<li><p>在类设计模式中,故意让父类(Task)和子类(XYZ)中都有 outputTask 方法,这样就可以利用重写(多态)的优势,在委托行为中则恰好相反,我们会尽量避免在 [[Prototype]] 链的不同级别使用相同的命名,否则就需要使用笨拙并且脆弱的语法来消除引用歧义。</p>
<p>这个设计模式要求就进来少使用容易被重写的通用方法名,提倡使用更有描述性的方法名,尤其是写清相对对象行为的类型。这样做实际上可以创建出更容易理解和维护的代码。因为方法名(不仅在定义的位置,而是贯穿整个代码)更加清晰(自文档)。</p>
</li>
<li><p>this.setID(ID),XYZ 中的方法首先会寻找 XYZ 自身是否有 setID(..),但是 XYZ 中并没有整个方法名,因为会通过 [[Prototype]] 委托关联到 Task 继续寻找,这时就可以找到 setID(..) 整个方法。此外,由于调用位置触发了 this 的隐式绑定规则,因此虽然 setID(..) 方法在 Task 中,运行时 this 仍然会绑定到 XYZ,这正是我们想要的。后面的outputID(..) 也是同样的原理。</p>
<p>换句话说,我们和 XYZ 进行交互时,可以使用 Task 中通用的方法,因为 XYZ 委托了 Task.</p>
</li>
</ol>
<p>委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task).</p>
<p>这个是一种极其强大的设计模式,和父类、子类、继承、多态等概念完全不同。对象并不是按照父类到子类的关系垂直组织的,而是通过任意方法的委托并排组织的。</p>
<blockquote>
<p>在 API 接口的设计中,委托最好在内部实现,不用直接暴露出去。在之前的例子中我们没有让开发者通过 API 直接调用 XYZ.setID()(当然,可以这么做)。相反,我们把委托隐藏了在 API 内部,XYZ.propareTas() 会委托 Task.setID(..)。</p>
</blockquote>
<h4 id="1-相互委托-禁止"><a href="#1-相互委托-禁止" class="headerlink" title="1.相互委托( 禁止)"></a><strong>1.相互委托( 禁止)</strong></h4><p>你无法在两个或两个以上互相(双向)委托的对象之间创建循环委托。如果你把 B 关联到 A 然后试着把 A 关联到 B,就会出现。</p>
<p>这种方法是禁止的。如果你引用了一个两边都不存在的属性或者是方法,那就会在 [[Prototype]] 链上产生一个无限递归的循环。但是如果所有的引用都被严格限制的话,B是可以委托 A的,反之亦然。因为,互相委托理论上是可以正常工作的,在某些情况下这是非常有用的。</p>
<p>之所以要禁止互相委托,是因为引擎的开发者发现在设置时检查(并禁止)一次无限循环引用要更加高效,否则每次从对象中查找属性时都需要进行检查。</p>
<h4 id="2-调试"><a href="#2-调试" class="headerlink" title="2.调试"></a><strong>2.调试</strong></h4><p>通常来说,JavaScript 规范并不会控制浏览器中开发者对于特定值或者结构的表达方式,浏览器和引擎可以自己选择合适的方式来解析,因此浏览器和工具的解析结构并不一定相同。比如下面的这段代码的结果只能在谷歌的开发者工具中看到。</p>
<p>这段传统的“类构造函数” JavaScript 代码在谷歌开发者工具的控制台中结果是这样的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params"></span>)</span>{}</span><br><span class="line"><span class="keyword">var</span> a1 = <span class="keyword">new</span> Foo();</span><br><span class="line">a1; <span class="comment">// Foo{}</span></span><br><span class="line"><span class="comment">// 在 FireFox 中运行却是 Object{}</span></span><br></pre></td></tr></table></figure>
<p>谷歌浏览器想说的 “{ } 是一个空对象,由名为 Foo 的函数构造”,Firefox 想说的是 “{} 是一个空对象,由Object 构造”,之所有有这种细微的差别,是因为谷歌会动态跟踪并把实际构造过程的函数当做一个内置属性,但是其他浏览器并不会跟踪这些额外的信息。</p>
<p>可以用 JavaScript 中的机制来解释一些谷歌的跟踪原理:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params"></span>)</span>{}</span><br><span class="line"><span class="keyword">var</span> a1 = <span class="keyword">new</span> Foo();</span><br><span class="line">a1.constructor(); <span class="comment">// Foo(){}</span></span><br><span class="line">a1.constructor.name; <span class="comment">//"Foo"</span></span><br></pre></td></tr></table></figure>
<p>谷歌是不是直接输出了对象的 .constructor.name ?令人迷惑的是,答案既是又不是。</p>
<p>思考下面的代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params"></span>)</span>{}</span><br><span class="line"><span class="keyword">var</span> a1 = <span class="keyword">new</span> Foo();</span><br><span class="line">Foo.prototype.constructor = <span class="function"><span class="keyword">function</span> <span class="title">Gotcha</span>(<span class="params"></span>)</span>{};</span><br><span class="line">a1.constructor; <span class="comment">// Gotcha(){}</span></span><br><span class="line">a1.constructor.name; <span class="comment">// "Gotcha"</span></span><br><span class="line">a1; <span class="comment">// Foo(){}</span></span><br></pre></td></tr></table></figure>
<p>即使我们把 a1.constructor.name 修改为一个合理的值,谷歌浏览器的控制台仍然会输出 Foo。</p>
<p>所以上面的那个问题答案是“不是”。谷歌浏览器是通过另一种方式进行跟踪的。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Foo = {};</span><br><span class="line"><span class="keyword">var</span> a1 = <span class="built_in">Object</span>.create(Foo);</span><br><span class="line">a1; <span class="comment">// Object{}</span></span><br><span class="line"><span class="built_in">Object</span>.defineProperty(Foo,<span class="string">"constructor"</span>,{</span><br><span class="line"> enumerable:<span class="literal">false</span>,</span><br><span class="line"> value:<span class="function"><span class="keyword">function</span> <span class="title">Gotcha</span>(<span class="params"></span>)</span>{}</span><br><span class="line">});</span><br><span class="line">a1; <span class="comment">//Gotcha{}</span></span><br></pre></td></tr></table></figure>
<p>这个bug现在已经被修复了,谷歌浏览器内部跟踪(只用于调试输出)“构造函数名称”的方法是它自身的一种扩展行为,并不包含在 JavaScript 规范中。</p>
<p>如果使用的并不是 “构造函数”来生成对象,比如使用本章介绍的对象关联风格来编写代码,那么谷歌浏览器就无法跟踪对象内部的 “构造函数名称”,这样的对象输出的就是 “Object{}”,意思是“Object() 构造出来的对象”。</p>
<p>当然,这并不是对象关联的风格的缺点,当你使用对象关联风格来编写代码并使用行为委托设计模式时,并不需要关注谁“构造了”对象(就是使用 new 来编写的那个函数)。只有使用类风格来编写代码时,谷歌浏览器内部的 “构造函数名称”跟踪才有意义,使用对象关联时这个功能起不到任何的作用。</p>
<h3 id="比较思维模型"><a href="#比较思维模型" class="headerlink" title="比较思维模型"></a>比较思维模型</h3><p>现在已经明白了“类”和“委托”这两种设计模式的理论区别,接下来看看他们在思维模型方面的区别。</p>
<p>通过一些实例代码来比较一下两种设计模式(面向对象和对象关联)具体的实现方法,下面是典型(“原型”)面向对象风格:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params">who</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.me = who;</span><br><span class="line">}</span><br><span class="line">Foo.prototype.identify = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"I am"</span> + <span class="keyword">this</span>.me;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Bar</span>(<span class="params">who</span>)</span>{</span><br><span class="line"> Foo.call(<span class="keyword">this</span>,who);</span><br><span class="line">}</span><br><span class="line">Bar.prototype = <span class="built_in">Object</span>.create(Foo.prototype);</span><br><span class="line">Bar.prototype.speak = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> alert(<span class="string">'Hello,'</span>+<span class="keyword">this</span>.identify()+<span class="string">"."</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> b1 = <span class="keyword">new</span> Bar(<span class="string">"b1"</span>);</span><br><span class="line"><span class="keyword">var</span> b2 = <span class="keyword">new</span> Bar(<span class="string">"b2"</span>);</span><br><span class="line">b1.speak();</span><br><span class="line">b2.speak();</span><br></pre></td></tr></table></figure>
<p>子类 Bar 委托了 父类 Foo,然后生成了 b1 和 b2 两个实例。b1 委托了 Bar.prototype,Bar.prototype 委托了Foo.prototype。</p>
<p>下面看看如何使用面向对象关联风格来编写完全相同的代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Foo = {</span><br><span class="line"> init:<span class="function"><span class="keyword">function</span>(<span class="params">who</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.me = who;</span><br><span class="line"> }</span><br><span class="line"> identify:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"I am"</span> + <span class="keyword">this</span>.me;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">Bar = <span class="built_in">Object</span>.create(Foo);</span><br><span class="line">Bar.speak = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> alert(<span class="string">'Hello,'</span>+<span class="keyword">this</span>.identify()+<span class="string">"."</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> b1 = <span class="built_in">Object</span>.create(Bar);</span><br><span class="line"><span class="keyword">var</span> b2 = <span class="built_in">Object</span>.create(Bar); </span><br><span class="line">b1.init();</span><br><span class="line">b2.init();</span><br><span class="line">b1.speak();</span><br><span class="line">b2.speak();</span><br></pre></td></tr></table></figure>
<p>这段代码中我们同样利用了 [[Prototype]] 把b1 委托给了 Bar 并把 Bar 委托给了 Foo,和上一段方面一模一样,我们仍然实现了三个对象之间的关联。</p>
<p>但是非常重要的一点是,这段代码简洁了很多。我们只是把对象关联起来,并不需要那些既复杂又令人困惑的模仿类的行为(构造函数、原型已经 new).</p>
<p>首先,类风格的代码的思维模型强调实体以及实体间的关系</p>
<p><img src="/images/2019-01-23-你不知道的JavaScript(上" alt="类风格代码的思维模型">之行为委托-类风格代码的思维模型.png)</p>
<p>看起来很复杂,从图中可以看出这是一张非常复杂的关系图,此外,你跟着图片中的箭头走就会发现,JavaScript 中有很强的内部连贯性。</p>
<p>举个例子,JavaScript 指哪个的函数之所以可以访问 call、apply、bind 是因为函数本身也是对象。而函数对象同样有 [[Prototype]] 属性并关联到 Function.prototype 对象,因此所有函数对象都可以通过委托来调用这些默认方法。</p>
<p>下面看简化版的,更加清晰——只展示了必要的对象和关系:</p>
<p><img src="/images/2019-01-23-你不知道的JavaScript(上" alt="类风格代码的思维模型(简化版)">之行为委托-类风格代码的思维模型(简化版).png)</p>
<p>虚线表示的是 Bar.prototype 继承 Foo.prototype 之后丢失的 .constructor 属性引用,它们还没有被修复。即使移除这些虚线,这个思维模型在你处理关联时仍然非常复杂。</p>
<p>看看对象关联风格代码的思维模型:</p>
<p><img src="/images/2019-01-23-你不知道的JavaScript(上" alt="对象关联风格代码的思维模型">之行为委托-对象关联风格代码的思维模型)</p>
<p>通过这些可以看出,对象关联风格的代码显然更加简洁,因为这种代码只关注一件事情:对象之间的关联关系。</p>
<p>其他 “类”技巧都是非常复杂并且令人困惑的。去掉它们之后,事情会变得简单许多(同时保留所有功能)。</p>
<h2 id="类和对象"><a href="#类和对象" class="headerlink" title="类和对象"></a>类和对象</h2><p>我们已经看到了 “类” 和 “行为委托”在理论和思维模型方面的区别,现在看看真实场景如何应用这些方法。</p>
<p>看看 Web 开发中非常经典的一些前端场景:创建 UI 控件(按钮、下拉列表等等)。</p>
<h3 id="控件“类”"><a href="#控件“类”" class="headerlink" title="控件“类”"></a>控件“类”</h3><p>可能你已经习惯了面向对象的设计模式,所以很快会想到一个包含所有通用控件行为的父类和继承父类的特殊控件子类。</p>
<blockquote>
<p>这里将使用JQ 来操作 DOM 和 CSS,因为这些操作和我们讨论的内容没有太多的关系。</p>
</blockquote>
<p>下面这段代码展示的是如何不使用任何 “类”辅助库或者语法的情况下,使用 纯 JavaScript 实现 类风格的代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 父类</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Widget</span>(<span class="params">width,height</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.width = width || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.height = height || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = <span class="literal">null</span>;</span><br><span class="line">}</span><br><span class="line">Widget.prototype.render = <span class="function"><span class="keyword">function</span>(<span class="params">$where</span>)</span>{</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.$elem){</span><br><span class="line"> <span class="keyword">this</span>.$elem.css({</span><br><span class="line"> width:<span class="keyword">this</span>.width + <span class="string">'px'</span>,</span><br><span class="line"> height:<span class="keyword">this</span>.height + <span class="string">'px'</span>,</span><br><span class="line"> }).appendTo($where); </span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 子类</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Button</span>(<span class="params">width,height,label</span>)</span>{</span><br><span class="line"> <span class="comment">// 调用 “super” 构造函数</span></span><br><span class="line"> Widget.call(<span class="keyword">this</span>,width,height);</span><br><span class="line"> <span class="keyword">this</span>.label = label || <span class="string">"Default"</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = $(<span class="string">"<button>"</span>).text(<span class="keyword">this</span>.label);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 让 Button 继承 Widget</span></span><br><span class="line">Button.prototype = <span class="built_in">Object</span>.create(Widget.prototype);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 重写 render(...)</span></span><br><span class="line">Button.prototype.render = <span class="function"><span class="keyword">function</span>(<span class="params">$where</span>)</span>{</span><br><span class="line"> <span class="comment">// “super” 调用</span></span><br><span class="line"> Widget.prototype.render.call(<span class="keyword">this</span>,$where);</span><br><span class="line"> <span class="keyword">this</span>.$elem.click(<span class="keyword">this</span>.onClick.bind(<span class="keyword">this</span>));</span><br><span class="line">}</span><br><span class="line">Button.prototype.onClick = <span class="function"><span class="keyword">function</span>(<span class="params">evt</span>)</span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Button "</span>+ <span class="keyword">this</span>.label +<span class="string">" clicked!"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">$(<span class="built_in">document</span>).ready(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">var</span> $body = $(<span class="built_in">document</span>.body);</span><br><span class="line"> <span class="keyword">var</span> btn1 = <span class="keyword">new</span> Button(<span class="number">125</span>,<span class="number">30</span>,<span class="string">"Hello"</span>);</span><br><span class="line"> <span class="keyword">var</span> btn2 = <span class="keyword">new</span> Button(<span class="number">150</span>,<span class="number">40</span>,<span class="string">"World"</span>);</span><br><span class="line"> btn1.render($body);</span><br><span class="line"> btn2.render($body);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>在面向对象设计模式中我们需要现在父类中定义基础的 render(..),然后在子类中重写它。子类并不会替换基础的 render(..),只是添加了一些按钮特有的欣慰。可以看到代码中丑陋的显式伪多态,即通过 Widget.call 和 Widget.prototype.render.call 从“子类”方法中引用“父类”中的基础方法。</p>
<h4 id="ES6-的-class-语法糖"><a href="#ES6-的-class-语法糖" class="headerlink" title="ES6 的 class 语法糖"></a><strong>ES6 的 class 语法糖</strong></h4><p>简单介绍如何使用 class 来实现相同的功能:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 父类</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Widget</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(width,height){</span><br><span class="line"> <span class="keyword">this</span>.width = width || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.height = height || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = <span class="literal">null</span>; </span><br><span class="line"> }</span><br><span class="line"> render($where){</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.$elem){</span><br><span class="line"> <span class="keyword">this</span>.$elem.css({</span><br><span class="line"> width:<span class="keyword">this</span>.width + <span class="string">'px'</span>,</span><br><span class="line"> height:<span class="keyword">this</span>.height + <span class="string">'px'</span>,</span><br><span class="line"> }).appendTo($where); </span><br><span class="line"> } </span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Button</span> <span class="keyword">extends</span> <span class="title">Widget</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(width,height,label){</span><br><span class="line"> <span class="keyword">super</span>(width,height);</span><br><span class="line"> <span class="keyword">this</span>.label = label || <span class="string">"Default"</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = $(<span class="string">"<button>"</span>).text(<span class="keyword">this</span>.label);</span><br><span class="line"> } </span><br><span class="line"> render($where){</span><br><span class="line"> <span class="keyword">super</span>.render($where)</span><br><span class="line"> <span class="keyword">this</span>.$elem.click(<span class="keyword">this</span>.onClick.bind(<span class="keyword">this</span>)); </span><br><span class="line"> }</span><br><span class="line"> onClick(evt){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Button "</span>+ <span class="keyword">this</span>.label +<span class="string">" clicked!"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">$(<span class="built_in">document</span>).ready(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">var</span> $body = $(<span class="built_in">document</span>.body);</span><br><span class="line"> <span class="keyword">var</span> btn1 = <span class="keyword">new</span> Button(<span class="number">125</span>,<span class="number">30</span>,<span class="string">"Hello"</span>);</span><br><span class="line"> <span class="keyword">var</span> btn2 = <span class="keyword">new</span> Button(<span class="number">150</span>,<span class="number">40</span>,<span class="string">"World"</span>);</span><br><span class="line"> btn1.render($body);</span><br><span class="line"> btn2.render($body);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>毫无疑问,使用了 ES6 的 class之后,上一段代码中很多丑陋的语法都不见了,super(..) 函数很棒。</p>
<p>尽管语法上得到了改进,但实际上这里并没有真正的类,class 仍然是通过 [[Prototype]] 机制实现的,因为我们仍然面临思维模式不匹配的问题。</p>
<p>无论你使用传统的原型语法还是 ES6 新语法糖,仍然需要用 “类”的概念来对问题进行建模。</p>
<h3 id="委托控件对象"><a href="#委托控件对象" class="headerlink" title="委托控件对象"></a>委托控件对象</h3><p>下面的例子使用对象关联风格委托来简单实现 Widget/Button:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Widget = {</span><br><span class="line"> init:<span class="function"><span class="keyword">function</span>(<span class="params">width,height</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.width = width || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.height = height || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = <span class="literal">null</span>; </span><br><span class="line"> },</span><br><span class="line"> insert:<span class="function"><span class="keyword">function</span>(<span class="params">$where</span>)</span>{</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.$elem){</span><br><span class="line"> <span class="keyword">this</span>.$elem.css({</span><br><span class="line"> width:<span class="keyword">this</span>.width + <span class="string">'px'</span>,</span><br><span class="line"> height:<span class="keyword">this</span>.height + <span class="string">'px'</span>,</span><br><span class="line"> }).appendTo($where); </span><br><span class="line"> } </span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> Button = <span class="built_in">Object</span>.create(Widget);</span><br><span class="line">Button.setup = <span class="function"><span class="keyword">function</span>(<span class="params">width,height,label</span>)</span>{</span><br><span class="line"> <span class="comment">// 委托调用</span></span><br><span class="line"> <span class="keyword">this</span>.init(width,height);</span><br><span class="line"> <span class="keyword">this</span>.label = label || <span class="string">"Default"</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = $(<span class="string">"<button>"</span>).text(<span class="keyword">this</span>.label); </span><br><span class="line">}</span><br><span class="line">Button.build = <span class="function"><span class="keyword">function</span>(<span class="params">$where</span>)</span>{</span><br><span class="line"> <span class="comment">// 委托调用</span></span><br><span class="line"> <span class="keyword">this</span>.insert($where)</span><br><span class="line"> <span class="keyword">this</span>.$elem.click(<span class="keyword">this</span>.onClick.bind(<span class="keyword">this</span>)); </span><br><span class="line">}</span><br><span class="line">Button.OnClick = <span class="function"><span class="keyword">function</span>(<span class="params">evt</span>)</span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Button "</span>+ <span class="keyword">this</span>.label +<span class="string">" clicked!"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">$(<span class="built_in">document</span>).ready(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">var</span> $body = $(<span class="built_in">document</span>.body);</span><br><span class="line"> <span class="keyword">var</span> btn1 = <span class="built_in">Object</span>.create(Button);</span><br><span class="line"> <span class="keyword">var</span> btn2 = <span class="built_in">Object</span>.create(Button);</span><br><span class="line"> btn1.setup(<span class="number">125</span>,<span class="number">30</span>,<span class="string">"Hello"</span>);</span><br><span class="line"> btn1.setup(<span class="number">150</span>,<span class="number">40</span>,<span class="string">"World"</span>); </span><br><span class="line"> btn1.build($body);</span><br><span class="line"> btn2.build($body);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>使用对象关联来编写代码时不需要把 Widget 和 Button 当做父类和子类,相反,Widget 只是一个对象,包含一个通用的函数,任何类型的控件都可以作为委托,Button 同样只是一个对象。</p>
<p>从设计模式的角度来说,我们并没有像类一样在两个对象中都定义相同的方法名 render(..) ,相反我们定义了两个更具有描述性的方法名(insert/build)。同理,初始化方法分别叫做 init 和 setup。</p>
<p>在委托设计的模式中,除了建议使用不相同的并且更具有描述性的方法名之外,还要通过对象关联避免使用丑陋的显式伪多态调用,而是用简单的相对委托调用 this.init() 和 this.insert。</p>
<p>从语法角度来说,我们没有使用任何的构造函数 、.prototype 或者 new.实际上也没有必要使用它们。</p>
<p>仔细观察的话,会发现之前的一次调用 (var btn1 = new Button(125,30,”Hello”);)变成了两次(var btn1 = Object.create(Button);btn1.setup(125,30,”Hello”);)咋一看这似乎是一个缺点(需要更多的代码)。</p>
<p>但是这一点其实也是对象关联风格相比较传统原型风格代码的优势的地方,为什么?</p>
<p>使用类构造的话,你需要(并不是硬性要求,但是强烈建议)在同一个步骤中实现构造和初始化。然后,许多情况下把这两步分开(就像对象关联代码一样)更灵活。</p>
<p>举例来说,假如在程序启动的时候创建一个实例池,然后一直等到实例被取出来并使用才执行特定的初始化过程。这个过程中两个函数调用是挨着的。但是完全可以根据需要让它们出现在不同的位置。</p>
<p><strong>对象关联可以更好地支持关注分离(separation of concerns)原则,创建和初始化并不需要合并为一个步骤</strong></p>
<h2 id="更简洁的设计"><a href="#更简洁的设计" class="headerlink" title="更简洁的设计"></a>更简洁的设计</h2><p>对象关联除了能让代码看起来更加简洁(并且就有更好的扩展性)外还可以通过行为委托模式简化代码结构。</p>
<p>下面的场景中我们有两个控制器对象,一个用来操作网页中的登录表单,一个用来与服务器进行验证(通信)。</p>
<p>我们需要一个辅助函数来创建 Ajax 通信,我们使用的是 jq,它不仅可以处理 Ajax 并且返回一个类 Promise 的结果,因此可以使用 .then() 来监听响应。</p>
<p>在传统的类设计模式中,我们会把基础的函数定义在名为 Controller 的类中,然后派生出两个子类 LoginController 和 AuthController ,它们都继承自 Controller 并且重写了一些基础行为:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 父类</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Controller</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.error = [];</span><br><span class="line">}</span><br><span class="line">Controller.prototype.showDialog = <span class="function"><span class="keyword">function</span>(<span class="params">title,msg</span>)</span>{</span><br><span class="line"> <span class="comment">// 给用户显示标题和消息</span></span><br><span class="line">}</span><br><span class="line">Controller.prototype.success = <span class="function"><span class="keyword">function</span>(<span class="params">msg</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.showDialog(<span class="string">"Success"</span>,msg);</span><br><span class="line">}</span><br><span class="line">Controller.prototype.failure = <span class="function"><span class="keyword">function</span>(<span class="params">err</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.errors.push(err);</span><br><span class="line"> <span class="keyword">this</span>.showDialog(<span class="string">"Error"</span>,msg);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 子类</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">LoginController</span>(<span class="params"></span>)</span>{</span><br><span class="line"> Controller.call(<span class="keyword">this</span>);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 把子类关联到父类</span></span><br><span class="line">LoginController.prototype = <span class="built_in">Object</span>.create(Controller.prototype);</span><br><span class="line">LoginController.prototype.getUser = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">document</span>.getElementById(<span class="string">"login_username"</span>).value;</span><br><span class="line">}</span><br><span class="line">LoginController.prototype.getPassword = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">document</span>.getElementById(<span class="string">"login_password"</span>).value;</span><br><span class="line">}</span><br><span class="line">LoginController.prototype.validateEntry = <span class="function"><span class="keyword">function</span>(<span class="params">user,pw</span>)</span>{</span><br><span class="line"> user = user || <span class="keyword">this</span>.getUser();</span><br><span class="line"> pw = pw || <span class="keyword">this</span>.getPassword();</span><br><span class="line"> <span class="keyword">if</span>(!(user && pw)){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.failure(<span class="string">"Please enter a username & password!"</span>)</span><br><span class="line"> }<span class="keyword">else</span> <span class="keyword">if</span>(pw.length < <span class="number">5</span>){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.failure(<span class="string">"Password must be 5+ characters!"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果这行到这里说明通过验证</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 重写基础的 failure()</span></span><br><span class="line">LoginController.prototype.failure = <span class="function"><span class="keyword">function</span>(<span class="params">err</span>)</span>{</span><br><span class="line"> <span class="comment">// "super" 调用</span></span><br><span class="line"> Controller.prototype.failure.call(<span class="keyword">this</span>,<span class="string">"Login invalid"</span> + err);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 子类</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">AuthController</span>(<span class="params">login</span>)</span>{</span><br><span class="line"> Controller.call(<span class="keyword">this</span>);</span><br><span class="line"> <span class="comment">// 合成</span></span><br><span class="line"> <span class="keyword">this</span>.login = login;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 把子类关联到父类</span></span><br><span class="line">AuthController.prototype = <span class="built_in">Object</span>.create(Controller.prototype);</span><br><span class="line">AuthController.prototype.server = <span class="function"><span class="keyword">function</span>(<span class="params">url,data</span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> $.ajax({</span><br><span class="line"> url:url,</span><br><span class="line"> data:data</span><br><span class="line"> })</span><br><span class="line">}</span><br><span class="line">AuthController.prototype.checkAuth = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">var</span> user = <span class="keyword">this</span>.login.getUser();</span><br><span class="line"> <span class="keyword">var</span> pw = <span class="keyword">this</span>.login.getPassword();</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.login.validateEntry(user,pw)){</span><br><span class="line"> <span class="keyword">this</span>.server(<span class="string">"/check-auth"</span>,{</span><br><span class="line"> user:user,</span><br><span class="line"> pw:pw</span><br><span class="line"> }).then(<span class="keyword">this</span>.succdess.bind(<span class="keyword">this</span>))</span><br><span class="line"> .fail(<span class="keyword">this</span>.failure.bind(<span class="keyword">this</span>))</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 重写 success</span></span><br><span class="line">AuthController.prototype.success = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="comment">// super 调用</span></span><br><span class="line"> Controller.prototype.success.call(<span class="keyword">this</span>,<span class="string">"Authenticated!"</span>);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 重写 failure</span></span><br><span class="line">AuthController.prototype.failure = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="comment">// super 调用</span></span><br><span class="line"> Controller.prototype.success.call(<span class="keyword">this</span>,<span class="string">"Auth Failed: "</span>+ err);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 除了继承,我们还要合成</span></span><br><span class="line"><span class="keyword">var</span> auth = <span class="keyword">new</span> AuthController(<span class="keyword">new</span> LoginController);</span><br><span class="line">auth.checkAuth();</span><br></pre></td></tr></table></figure>
<p>所有控制器共享的基础行为是 success 和 failure 和 showDialog。子类 LoginController 和 AuthController 通过重写 failure 和 success 来扩展默认基础类行为。此外,注意 AuthController 需要一个 LoginController 的实例来登录表单进行交互,因此这个实例变成了一个数据属性。</p>
<p>另外还要注意的是,我们在继承的基础上进行了一些合成。AuthController 需要使用 LoginController ,因为我们实例化后者并用一个类成员属性 this.login 来引用它,这样 AuthController 就可以调用 LoginController 的行为了。</p>
<blockquote>
<p>你可能想让 AuthController 继承 LoginController 或者相反,这样我们就通过继承实现了真正的合成,但是这就是类继承在问题领域建模时产生的问题,因为 AutController 和 LoginController 都不具备对方的基础行为,所以这种继承是不恰当的。我们的解决方法就是进行一些就简单的合成从而让它们既不必互相继承又可以互相合作。</p>
</blockquote>
<h3 id="反类"><a href="#反类" class="headerlink" title="反类"></a><strong>反类</strong></h3><p>但是,我们真的需要一个 Controller 父类,两个子类加上合成来对这个问题进行建模吗?能不能使用对象关联风格的行为委托来实现更简单的设计呢?当然可以。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> LoginController = {</span><br><span class="line"> error:[],</span><br><span class="line"> getUser:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">document</span>.getElementById(<span class="string">"login_username"</span>).value;</span><br><span class="line"> },</span><br><span class="line"> getPassword:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">document</span>.getElementById(<span class="string">"login_password"</span>).value;</span><br><span class="line"> },</span><br><span class="line"> vaildateEntry:<span class="function"><span class="keyword">function</span>(<span class="params">user,pw</span>)</span>{</span><br><span class="line"> user = user || <span class="keyword">this</span>.getUser();</span><br><span class="line"> pw = pw || <span class="keyword">this</span>.getPassword();</span><br><span class="line"> <span class="keyword">if</span>(!(user && pw)){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.failure(<span class="string">"Please enter a username & password!"</span>)</span><br><span class="line"> }<span class="keyword">else</span> <span class="keyword">if</span>(pw.length < <span class="number">5</span>){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.failure(<span class="string">"Password must be 5+ characters!"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果这行到这里说明通过验证</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>; </span><br><span class="line"> },</span><br><span class="line"> showDialog:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="comment">// 给用户显示标题和消息</span></span><br><span class="line"> },</span><br><span class="line"> failure:<span class="function"><span class="keyword">function</span>(<span class="params">err</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.error.push(err);</span><br><span class="line"> <span class="keyword">this</span>.showDialog(<span class="string">"Error"</span>,<span class="string">"Login invalid"</span> + err);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 让 AuthController 委托 LoginController</span></span><br><span class="line"><span class="keyword">var</span> AuthController = <span class="built_in">Object</span>.create(LoginController);</span><br><span class="line">AuthController.errors = [];</span><br><span class="line">AuthController.checkAuth = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">var</span> user = <span class="keyword">this</span>.getUser();</span><br><span class="line"> <span class="keyword">var</span> pw = <span class="keyword">this</span>.getPassword();</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.validateEntry(user,pw)){</span><br><span class="line"> <span class="keyword">this</span>.server(<span class="string">"/check-auth"</span>,{</span><br><span class="line"> user:user,</span><br><span class="line"> pw:pw</span><br><span class="line"> })</span><br><span class="line"> .then(<span class="keyword">this</span>.accepted.bind(<span class="keyword">this</span>))</span><br><span class="line"> .fail(<span class="keyword">this</span>.rejected.bind(<span class="keyword">this</span>))</span><br><span class="line"> } </span><br><span class="line">}</span><br><span class="line">AuthController.server = <span class="function"><span class="keyword">function</span>(<span class="params">url,data</span>)</span>{</span><br><span class="line"> <span class="keyword">return</span> $.ajax({</span><br><span class="line"> url:url,</span><br><span class="line"> data:data</span><br><span class="line"> })</span><br><span class="line">}</span><br><span class="line">AuthController.accepted = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.showDialog(<span class="string">"Success"</span>,<span class="string">"Authenticated!"</span>);</span><br><span class="line">}</span><br><span class="line">Authenticated.rejected = <span class="function"><span class="keyword">function</span>(<span class="params">err</span>)</span>{</span><br><span class="line"> <span class="keyword">this</span>.failure(<span class="string">"Auth Failed!"</span>,err); </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>由于 AuthController 只是一个对象,因为我们不需要实例化只要一行代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">AuthController.checkAuth();</span><br></pre></td></tr></table></figure>
<p>借助对象关联,可以简单地向上委托链上添加一个或者多个对象,而且同样不需要实例化:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> controller1 = <span class="built_in">Object</span>.create(AuthController);</span><br><span class="line"><span class="keyword">var</span> controller2 = <span class="built_in">Object</span>.create(AuthController);</span><br></pre></td></tr></table></figure>
<p>在行为委托模式中,AuthController 和 LoginController 只是对象,它们之间是兄弟关系,并不是父类和子类的关系,代码中 AuthController 委托了 LoginController ,反向委托也可以。</p>
<p>这种模式的重点在于只需要两个实体(LoginController 和 AuthController ),而之前的模式需要三个。</p>
<p>我们不需要 Controller 基本类来共享两个实体之间的行为,因为委托足以满足我们需要的功能,同样的,前面提到的,我们也不需要实例化类,因为它们根本就不是类,它们只是对象。此外,我们也不需要合成,因为两个对象可以通过委托进行合作。</p>
<p>最后,我们避免了面向类设计模式的多态。我们在不同的对象中没有使用的函数名 success 和 failure 这样就不需要使用丑陋的显式伪多态。相反,在 AuthController 中它们的名字分别是 accepted 和 rejected 可以更好地描述它们的行为。</p>
<p>总结:我们使用一种简单的设计来实现了相同的功能,这就是对象关联风格代码和行为委托设计模式的力量。</p>
<h2 id="更好的语法"><a href="#更好的语法" class="headerlink" title="更好的语法"></a>更好的语法</h2><p>ES6 的 class 语法可以简洁地定义为 类方法,这个特性让 class 乍看起来更有吸引力。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span>()</span>{</span><br><span class="line"> methodName(){<span class="comment">/*..*/</span>}</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在 ES6 中,我们可以在任意对象的字面形式使用简洁方法声明(concise method declaration),所欲对象关联风格的对象还可以这样声明。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> loginController = {</span><br><span class="line"> error:[],</span><br><span class="line"> getUser(){</span><br><span class="line"> <span class="comment">//,,,</span></span><br><span class="line"> },</span><br><span class="line"> getPassword(){</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>唯一的区别就是对象的字面形式仍然使用 “,”来隔开元素,而 class 不需要。</p>
<p>此外,在 ES6 中,可以使用对象的字面形式来改写之前繁杂的属性赋值语法,然后用 Object.setPrototypeOf(..) 来修改它的 [[Prototype]]</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用更好的对象字面形式语法和简洁方法</span></span><br><span class="line"><span class="keyword">var</span> AuthController = {</span><br><span class="line"> error:[],</span><br><span class="line"> checkAuth(){</span><br><span class="line"> <span class="comment">//..</span></span><br><span class="line"> },</span><br><span class="line"> server(url,data){</span><br><span class="line"> <span class="comment">//..</span></span><br><span class="line"> },</span><br><span class="line"> <span class="comment">//..</span></span><br><span class="line">};</span><br><span class="line"><span class="comment">// 现在把 AuthController 关联到 LoginController</span></span><br><span class="line"><span class="built_in">Object</span>.setPrototypeOf(AuthController,LoginController);</span><br></pre></td></tr></table></figure>
<p>使用 ES6 的简洁方法可以让对象关联风格更加人性化(并且仍然比典型的原型风格更加简洁和优秀)。</p>
<h3 id="反词法"><a href="#反词法" class="headerlink" title="反词法"></a><strong>反词法</strong></h3><p>简洁方法有一个非常小但是非常重要的缺点,思考下面的代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Foo = {</span><br><span class="line"> bar(){<span class="comment">/*..*/</span>},</span><br><span class="line"> baz:<span class="function"><span class="keyword">function</span> <span class="title">baz</span>(<span class="params"></span>)</span>{<span class="comment">/*..*/</span>}</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>去掉语法糖之后的代码如下所示</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Foo = {</span><br><span class="line"> bar:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{<span class="comment">/*..*/</span>},</span><br><span class="line"> baz:<span class="function"><span class="keyword">function</span> <span class="title">baz</span>(<span class="params"></span>)</span>{<span class="comment">/*..*/</span>}</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>由于函数对象本身没有名称标识符,所以 bar 的缩写形式实际上会变成一个匿名函数表达式并且赋值给 bar 属性,相比之下,具名函数表达式会额外地给 .baz 属性附加一个词法名称标识符 baz。</p>
<p>在前面我们分析了匿名函数表达式的三大主要缺点,,匿名函数没有 name 标识符,会导致:</p>
<ol>
<li>调用栈难以追踪</li>
<li>自我引用(递归、事件、(接触)绑定等等)更难</li>
<li>代码更难理解</li>
</ol>
<p>简洁方法没有第一和第三个缺点。</p>
<p>去掉语法糖的版本使用的是匿名表达式,通常来说不会在追踪栈中添加 name ,但是简洁方法很特殊,会给对应的函数对象设置一个内部的 name 属性,这样的理论上可以用在追踪栈中。(但是追踪的具体实现是不同的,因为无法保证可以使用)。</p>
<p>简洁方法无法避免第二个缺点,它们不具备自我引用的词法标识符,思考下面的代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Foo = {</span><br><span class="line"> bar:<span class="function"><span class="keyword">function</span>(<span class="params">x</span>)</span>{</span><br><span class="line"> <span class="keyword">if</span>(x<<span class="number">10</span>){</span><br><span class="line"> <span class="keyword">return</span> Foo.bar(x*<span class="number">2</span>); </span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> x;</span><br><span class="line"> },</span><br><span class="line"> baz:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">if</span>(x<<span class="number">10</span>){</span><br><span class="line"> <span class="keyword">return</span> Foo.baz(x*<span class="number">2</span>); </span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> x;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在上面的例子中使用 Foo.bar(x*2)就足够了,但是在许多情况下,无法使用这种方法,比如多个对象通过代理共享函数,使用 this 绑定,等等。这种情况下,最好使用的方法就是使用函数对象的 name 标识符来进行真正的自我引用。</p>
<p>使用简洁方法一定要小心这一点,如果需要自我引用的时候,那最好的方法就是使用传统的具名函数表达式来定义对应的函数,不要使用简洁方法。</p>
<h2 id="内省"><a href="#内省" class="headerlink" title="内省"></a>内省</h2><p>内省就是检查实例的类型,类实例的内省主要目的是通过创建方式来判断对象的结构和功能。</p>
<p>下面的代码就是使用 instanceof 来推测对象的 a1 功能:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="comment">//..</span></span><br><span class="line">}</span><br><span class="line">Foo.prototype.something = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="comment">// ..</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> a1 = <span class="keyword">new</span> Foo();</span><br><span class="line"><span class="comment">//之后</span></span><br><span class="line"><span class="keyword">if</span>(a1 <span class="keyword">instanceof</span> Foo){</span><br><span class="line"> a1.something();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>因为 Foo.prototype(不是 Foo) 在 a1 的 [[Prototype]] 链上,所以 instanceof 可以告诉我们 a1 是 Foo 类的一个实例,知道了这一点之后,我们就可以认为 a1 有 Foo “类”描述的功能。</p>
<p>当然,Foo 类并不存在,只有一个普通的函数 Foo,它引用了 a1 委托的对象(Foo.prototype)。从语法角度来说,instanceof 似乎是检查 a1 和 Foo 的关系,但是实际上它想说的是 a1 和 Foo.prototype (引用的对象)是互相关联的。</p>
<p>instanceof 语法会产生语义困惑而且非常不直观,如果想检查对象a1 和某个对象的关系,那必须使用另一个引用该对象的函数才可以——你不可以判断两个对象是否关联。</p>
<p>之前介绍的抽象的 Foo/Bar/b1 例子,简单来说是这样的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params"></span>)</span>{}</span><br><span class="line">Foo.prototype...</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Bar</span>(<span class="params"></span>)</span>{}</span><br><span class="line">Bar.prototype = <span class="built_in">Object</span>.create(Foo.prototype);</span><br><span class="line"><span class="keyword">var</span> b1 = <span class="keyword">new</span> Bar(<span class="string">"b1"</span>);</span><br></pre></td></tr></table></figure>
<p>如果要使用 instanceof 和 .prototype 语义来检查上面例子中的实体的关系,那必须这样做:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 让 Foo 和 Bar 互相关联</span></span><br><span class="line">Bar.prototype <span class="keyword">instanceof</span> Foo; <span class="comment">// true</span></span><br><span class="line"><span class="built_in">Object</span>.getPrototypeOf(Bar.prototype) === Foo.prototype; <span class="comment">// true</span></span><br><span class="line">Foo.prototype.isPrototypeOf(Bar.prototype); <span class="comment">// true</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 让 b1 关联到 Foo 和 Bar</span></span><br><span class="line">b1 <span class="keyword">instanceof</span> Foo; <span class="comment">// true</span></span><br><span class="line">b1 <span class="keyword">instanceof</span> Bar; <span class="comment">// true</span></span><br><span class="line"><span class="built_in">Object</span>.getPrototypeOf(b1) === Bar.prototype; <span class="comment">// true</span></span><br><span class="line">Bar.prototype.isPrototypeOf(b1); <span class="comment">// true</span></span><br><span class="line">Foo.prototype.isPrototypeOf(b1); <span class="comment">// true</span></span><br></pre></td></tr></table></figure>
<p>显然这是一种非常糟糕的方法,举例来说(使用类时)你最直观的想法可能是使用 Bar instanceof Foo(因为很容易把实例理解成继承),但是在 JavaScript 中是行不通的,你必须使用 Bar.prototype instanceof Foo.</p>
<p>还有一种常见的但是很脆弱的内省模式,这种模式被称为鸭子类型。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(a1.something){</span><br><span class="line"> a1.something();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>我们并没有检查 a1 和 委托 something()函数的对象之间的关系,而是假设如果如果 a1 通过了 测试 a1.something 的话,那么 a1 就一定可以调用这个方法(无论这个方法是否存在于它本身或是委托到其他对象)。</p>
<p>但是 “鸭子类型”通常会在测试之外做出很多关于对象功能的假设,这会带来很多风险(或者说脆弱的设计)。</p>
<p>ES6 的 Promise 就是典型的 “鸭子模式”。</p>
<p>由于各种各样的原因,我们需要判断一个对象引用是否是 Promise ,但是判断的方法就是检查对象中是否有 then 方法,换句话说,如果对象中有 then 方法,ES6 的Promise 就会认为这个对象是“可持续的”(thenable),因此会期望它具有 Promise 的所有标准行为。</p>
<p>如果有一个不是 Promise 但是具有 then 方法的对象,那就千万不要把它用在 ES6 中的 Promise 机制中,否则会出错。</p>
<p>使用对象关联时,所有的对象都是通过 [[Prototype]] 委托相互关联的,下面是内省的方法</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 让 Foo 和 Bar 互相关联</span></span><br><span class="line">Foo.isPrototypeOf(Bar); <span class="comment">// true</span></span><br><span class="line"><span class="built_in">Object</span>.getPrototypeOf(Bar) === Foo; <span class="comment">// true</span></span><br><span class="line"><span class="comment">// 让 b1 关联到 Foo 和 Bar</span></span><br><span class="line">Foo.isPrototypeOf(b1); <span class="comment">// true</span></span><br><span class="line">Bar.isPrototypeOf(b1); <span class="comment">// true</span></span><br><span class="line"><span class="built_in">Object</span>.getPrototypeOf(b1) === Bar; <span class="comment">// true</span></span><br></pre></td></tr></table></figure>
<p>我们没有使用 instanceof 因为它会产生一些和类有关的误解。我们认为 JavaScript 中对象关联比类风格的代码更加简洁(而且功能相同)</p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>在软件架构中你可以选择是否使用类和继承设计模式,大多数开发者理所当然地认为类是唯一的代码组织方式,但是本章中我们看到另一种少见但是更强大的设计行为:行为委托。</p>
<p>行为委托认为对象之间是兄弟关系,互相委托,不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努力实现类机制,也可以拥抱更自然的 [[Prototype]] 委托机制。</p>
<p>当你只用对象设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。</p>
<p>对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。</p>
<h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><h3 id="ES6-中的-Class"><a href="#ES6-中的-Class" class="headerlink" title="ES6 中的 Class"></a>ES6 中的 Class</h3><p>类是一种可选的(而不是必须)的设计模式,而且在 JavaScript 这样的 [[Prototype]] 语言中实现类是很别扭的。</p>
<p>缺点:繁琐杂乱的 .prototype 引用、试图调用原型链上层同名函数时的显式伪多态以及不可靠、不美观容易被误解成“构造函数“的 .construtor。</p>
<p>除此之外,类设计其实还有更深刻的问题,传统面向类的语言中父子类,子和实例之间其实是复制操作,但是在 [[Prototype]] 中并没有复制,相反,它们之间只有委托关联。</p>
<p>对象关联代码和行为委托使用了 [[Prototype]] 而不是将它们藏起来,对比起简洁性可以看出,类并不适用 JavaScirpt.</p>
<h4 id="class"><a href="#class" class="headerlink" title="class"></a><strong>class</strong></h4><p>ES6 的 class 机制,会介绍它的工作原理以及是否改进了之前提到的缺点。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 父类</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Widget</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(width,height){</span><br><span class="line"> <span class="keyword">this</span>.width = width || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.height = height || <span class="number">50</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = <span class="literal">null</span>; </span><br><span class="line"> }</span><br><span class="line"> render($where){</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.$elem){</span><br><span class="line"> <span class="keyword">this</span>.$elem.css({</span><br><span class="line"> width:<span class="keyword">this</span>.width + <span class="string">'px'</span>,</span><br><span class="line"> height:<span class="keyword">this</span>.height + <span class="string">'px'</span>,</span><br><span class="line"> }).appendTo($where); </span><br><span class="line"> } </span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Button</span> <span class="keyword">extends</span> <span class="title">Widget</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(width,height,label){</span><br><span class="line"> <span class="keyword">super</span>(width,height);</span><br><span class="line"> <span class="keyword">this</span>.label = label || <span class="string">"Default"</span>;</span><br><span class="line"> <span class="keyword">this</span>.$elem = $(<span class="string">"<button>"</span>).text(<span class="keyword">this</span>.label);</span><br><span class="line"> } </span><br><span class="line"> render($where){</span><br><span class="line"> <span class="keyword">super</span>.render($where)</span><br><span class="line"> <span class="keyword">this</span>.$elem.click(<span class="keyword">this</span>.onClick.bind(<span class="keyword">this</span>)); </span><br><span class="line"> }</span><br><span class="line"> onClick(evt){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Button "</span>+ <span class="keyword">this</span>.label +<span class="string">" clicked!"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">$(<span class="built_in">document</span>).ready(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">var</span> $body = $(<span class="built_in">document</span>.body);</span><br><span class="line"> <span class="keyword">var</span> btn1 = <span class="keyword">new</span> Button(<span class="number">125</span>,<span class="number">30</span>,<span class="string">"Hello"</span>);</span><br><span class="line"> <span class="keyword">var</span> btn2 = <span class="keyword">new</span> Button(<span class="number">150</span>,<span class="number">40</span>,<span class="string">"World"</span>);</span><br><span class="line"> btn1.render($body);</span><br><span class="line"> btn2.render($body);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>除了语法更加好看之外,ES6 还解决了什么问题呢?</p>
<ol>
<li>不再引用杂乱的 .prototype 了</li>
<li>Button 声明时直接“继承” 了 Widget,不再需要通过 Object.create(..) 来替换 .prototype 对象,也不需要设置 <code>.__proto__</code> 或者 Object.setPrototypeOf(..)</li>
<li>可以通过 super(..) 来实现相对多态,这样任何防范都可以引用原型链上层的同名方法,可以解决之前提到的问题:构造函数不属于类,所以无法互相引用——super() 可以完美解决构造函数的问题。</li>
<li>class 字面语法不能声明属性(只能声明方法)。看起来这是一种限制,但是它会排除掉很多不好的情况,如果没有这种限制的话,原型链末端的实例可能会意外地获取其他地方的属性(这些属性隐式被所有“实例”所共享)。所以 ,class语法实际上可以帮你避免犯错。</li>
<li>可以通过 extends 很自然扩展对象(子)类型,甚至是内置的对象(子)类型,比如 Array,RegExp。没有 class ..extends 语法时,想实现这一点是非常困难的,基本上只有框架的作者才能搞懂这一点。但是现在可以轻而易举地做到。</li>
</ol>
<p>平心而论,class 语法确实解决了典型原型风格代码中很多显而易见的(语法)问题和缺点。</p>
<h4 id="class-陷阱"><a href="#class-陷阱" class="headerlink" title="class 陷阱"></a><strong>class 陷阱</strong></h4><p>然后,class 语法并没有解决所有的问题,在 JavaScript 中使用 “类”设计模式仍然存在许多深层问题。</p>
<p>首先,你可能会认为 ES6 的 class 语法是向 JavaScript 中引入了一种新的 “类”机制,其实不是这样的,class 基本上只是现有的 [[Prototype]] (委托!)机制的一种语法糖。</p>
<p>也就是说,class 并不会像传统面向类的语言一样在声明时静态复制所有的行为。如果修改或者替换了父“类”中的一个方法,那么子“类”和所有实例都会受到影响,因为它们在定义时并没有进行复制,只是适用基于 [[Prototype]] 的实时委托:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(){</span><br><span class="line"> <span class="keyword">this</span>.sum = <span class="built_in">Math</span>.random();</span><br><span class="line"> }</span><br><span class="line"> rand(){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Random: "</span>+<span class="keyword">this</span>.num);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> c1 = <span class="keyword">new</span> C();</span><br><span class="line">c1.rand(); <span class="comment">// "Random: 0.4546976..."</span></span><br><span class="line">C.prototype.rand = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Random: "</span>+<span class="built_in">Math</span>.round(<span class="keyword">this</span>.num * <span class="number">1000</span>));</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> c2 = <span class="keyword">new</span> C();</span><br><span class="line">c2.rand(); <span class="comment">// "Random: 867"</span></span><br><span class="line">c1.rand(); <span class="comment">// "Random: 425"</span></span><br></pre></td></tr></table></figure>
<p>为什么要使用本质上不是类的 class 语法?</p>
<p>ES6 的 class 语法不是让传统类和委托对象之间的区别更加难以发现和理解吗?</p>
<p>class 语法无法定义类成员属性(只能定义方法),如果为了追踪实例之间的共享状态必须要这么做,那你就只能使用丑陋的 .prototype 语法,像这样:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(){</span><br><span class="line"> <span class="comment">// 确保修改的是共享状态而不是一个在实例上创建的一个屏蔽属性!</span></span><br><span class="line"> C.prototype.count++;</span><br><span class="line"> <span class="comment">// this.count 可以通过委托实现我们想要的功能</span></span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Hello: "</span> + <span class="keyword">this</span>.count);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 直接向 prototype 对象上添加一个共享状态</span></span><br><span class="line">C.prototype.count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> c1 = <span class="keyword">new</span> C();</span><br><span class="line"><span class="comment">// Hello:1</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> c2 = <span class="keyword">new</span> C();</span><br><span class="line"><span class="comment">// Hello:2</span></span><br><span class="line"></span><br><span class="line">c1.count === <span class="number">2</span>; <span class="comment">// true</span></span><br><span class="line">c1.count === c2.count; <span class="comment">// true</span></span><br></pre></td></tr></table></figure>
<p>这种方法最大的问题是,它违背了 class语法的本意,在实现中暴露(泄露)了 .prototype</p>
<p>如果使用 this.count++ 的话,我们很惊讶地发现在对象 c1 和 c2 上都创建了 .count 属性,而不是更新共享状态。class没有办法解决这个问题,并且干脆就不提供相应的语法支持,所有根本就不应该这样做。</p>
<p>此外,class 语法仍然面临意外屏蔽的问题:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span></span>{</span><br><span class="line"> <span class="keyword">constructor</span>(id){</span><br><span class="line"> <span class="comment">// 我们的id属性屏蔽了 id 方法</span></span><br><span class="line"> <span class="keyword">this</span>.id = id;</span><br><span class="line"> } </span><br><span class="line"> id(){</span><br><span class="line"> <span class="built_in">console</span>.log(id)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> c1 = <span class="keyword">new</span> C();</span><br><span class="line">c1.id(); <span class="comment">// TypeError</span></span><br></pre></td></tr></table></figure>
<p>除此之外,super 也存在一些细微的问题,你可能认为 super 的绑定方法和 this 类似,也就是说,无论目前的方法在原型链中处于什么位置,super 总会绑定到链的上一层。</p>
<p>然而,处于性能的考虑,super 并不是动态绑定到,它会在声明时“静态”绑定。如果你会用许多不同的方法把函数应用在不同的对象上,那你可能不知道,每次执行这些操作时都必须重新绑定 super。</p>
<p>此外,根据应用方式的不同,super 可能不会绑定到合适的对象,所以你可能需要 用 toMethod(..) 来手动绑定 super。你习惯了把方法应用到不同的对象上,从而可以自动利用 this 的隐式绑定规则,但是 super 是行不通的。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">P</span></span>{</span><br><span class="line"> foo(){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"P.foo"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">C</span> <span class="keyword">extends</span> <span class="title">P</span></span>{</span><br><span class="line"> foo(){</span><br><span class="line"> <span class="keyword">super</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> c1 = <span class="keyword">new</span> C();</span><br><span class="line">C1.foo(); <span class="comment">// "P.foo"</span></span><br><span class="line"><span class="keyword">var</span> D = {</span><br><span class="line"> foo:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"D.foo"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> E = {</span><br><span class="line"> foo:C.prototype.foo</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 把 E 委托到 D</span></span><br><span class="line"><span class="built_in">Object</span>.setPrototypeOf(E,D);</span><br><span class="line"></span><br><span class="line">E.foo(); <span class="comment">// "P.foo"</span></span><br></pre></td></tr></table></figure>
<p>你可能会认为 super 会动态绑定,那么可能期望 super() 会自动识别出 E 委托了 D,所以E.foo() 中的 super 应该调用 D.foo()。</p>
<p>但事实并不是这样的,处于性能考虑,super 并不像 this 一样是晚绑定(lated bound,或者说动态绑定)的,它在 [[HomeObject]].[[Prototype]] 上,[[HomeObject]] 会在创建时静态绑定。</p>
<p>在上面的例子中,super() 会调用 P.foo() ,因为方法的 [[HomeObject]] 仍然是 C,C.[[Prototype]] 是 P。确实可以手动修改 super 绑定,使用 toMethod(..) 绑定或者重新绑定方法的 [[HomeObject]](就像设置 [[Prototype]] 一样)就可以解决上面的问题:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> D = {</span><br><span class="line"> foo:<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"D.foo"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 把 E 委托到 D</span></span><br><span class="line"><span class="keyword">var</span> E = <span class="built_in">Object</span>.create(D);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 手动把 foo 的 [[HomeObject]] 绑定到 E,E.[[Prototype]] 是 D,所以 super() 是 D.foo() </span></span><br><span class="line">E.foo = C.prototype.foo.toMethod(E,<span class="string">"foo"</span>);</span><br><span class="line">E.foo(); <span class="comment">// "D.foo"</span></span><br></pre></td></tr></table></figure>
<blockquote>
<p>toMethod(..) 会复制方法并把 homeObject 当做第一个参数(也就是我们传入的 E),第二个参数(可选)是新方法的名称(默认是原方法名)</p>
</blockquote>
<p>除此之外,开发者有可能会遇到其他问题,这有待观察,无论如何,对于引擎自动绑定的 super 来说,你必须时刻警惕是否需要进行手动绑定。</p>
<h4 id="静态大于动态吗?"><a href="#静态大于动态吗?" class="headerlink" title="静态大于动态吗?"></a><strong>静态大于动态吗?</strong></h4><p>通过上面的这些特性可以看出,ES6 的 class 最大问题在于,(像传统的类一样)它的语法有时会让你认为,定义了一个 class 后,它就变成了一个(未来会被实例化)东西的静态定义。你会彻底忽略C是一个对象,是一个具体的可以直接交互的东西。</p>
<p>在传统的面向类的语言中,类定义之后就不会进行修改,所以类的设计模式就不支持修改。但是 JavaScript 是最强大的特性之一就是它的动态性,任何对象的定义都可以修改(除非你想设置为不可变的)。</p>
<p>class 似乎不赞成这样子做,所以强制让你使用丑陋的 .prototype 语法以及 super 问题等等,而且这种动态产生的问题,class 基本上没有提供解决方案。</p>
<p>总体来说,ES6 的 class 想伪装成一种很好的语法问题的解决方案,但是实际上却让问题更难解决而且让 JavaScrip 更加难以理解了。</p>
<blockquote>
<p>如果你使用 .bind() 函数来硬绑定函数,那么这个函数不会像普通函数那样被 ES6 的 extend 扩展于 子类中</p>
</blockquote>
<h4 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a><strong>小结</strong></h4><p>class 很好地伪装成 JavaScript 中类和继承设计模式的解决方案。但是它实际上起到了反作用:它隐藏了许多问题并且带来了更多更细小的但是很危险的问题。</p>
<p>class加上了过去20年对于 JavaScript 中 “类”的误解,在某些方面,它产生的问题比解决的还要多,而且让本来优雅简洁的 [[Prototype]] 机制变得非常别扭。</p>
</div>
<div>
<ul class="post-copyright">
<li class="post-copyright-author">
<strong>本文作者:</strong>
赖彬鸿
</li>
<li class="post-copyright-link">
<strong>本文链接:</strong>
<a href="http://laibh.top/2019-01-23-你不知道的JavaScript(上)——行为委托.html" title="你不知道的JavaScript(上)——行为委托">http://laibh.top/2019-01-23-你不知道的JavaScript(上)——行为委托.html</a>
</li>
<li class="post-copyright-license">
<strong>版权声明: </strong>
本博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/3.0/" rel="external nofollow" target="_blank">CC BY-NC-SA 3.0</a> 许可协议。转载请注明出处!
</li>
</ul>
</div>
<footer class="post-footer">
<div class="post-tags">
<a href="/tags/你不知道的JavaScript/" <i class="fa fa-tag"></i> 你不知道的JavaScript</a>
</div>
<div class="post-nav">
<div class="post-nav-next post-nav-item">
<a href="/2019-01-22-你不知道的JavaScript(上)——原型.html" rel="next" title="你不知道的JavaScript(上)——原型">
<i class="fa fa-chevron-left"></i> 你不知道的JavaScript(上)——原型
</a>
</div>
<span class="post-nav-divider"></span>
<div class="post-nav-prev post-nav-item">
<a href="/2019-01-26-JavaScript设计模式——前言准备.html" rel="prev" title="JavaScript设计模式——前言准备">
JavaScript设计模式——前言准备 <i class="fa fa-chevron-right"></i>
</a>
</div>
</div>
</footer>
</div>
</article>
<div class="post-spread">
<script>
window._bd_share_config = {
"common": {
"bdText": "",
"bdMini": "1",
"bdMiniList": false,
"bdPic": ""
},
"image": {
"viewList": ["tsina", "douban", "sqq", "qzone", "weixin", "twi", "fbook"],
"viewText": "分享到:",
"viewSize": "16"
},
"slide": {
"bdImg": "5",
"bdPos": "left",
"bdTop": "100"
}
}
</script>
<script>
with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='/static/api/js/share.js?v=89860593.js?'+~(-new Date()/36e5)];;
</script>
</div>
</div>
</div>
<div class="comments" id="comments">
<div id="lv-container" data-id="city" data-uid="MTAyMC8zOTcwMy8xNjIzMA"></div>
</div>
</div>
<div class="sidebar-toggle">
<div class="sidebar-toggle-line-wrap">
<span class="sidebar-toggle-line sidebar-toggle-line-first"></span>
<span class="sidebar-toggle-line sidebar-toggle-line-middle"></span>
<span class="sidebar-toggle-line sidebar-toggle-line-last"></span>
</div>
</div>
<aside id="sidebar" class="sidebar">
<div class="sidebar-inner">
<ul class="sidebar-nav motion-element">
<li class="sidebar-nav-toc sidebar-nav-active" data-target="post-toc-wrap">
文章目录
</li>
<li class="sidebar-nav-overview" data-target="site-overview-wrap">
站点概览
</li>
</ul>
<section class="site-overview-wrap sidebar-panel">
<div class="site-overview">
<div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
<img class="site-author-image" itemprop="image"
src="/images/myPhoto.jpg"
alt="赖彬鸿" />
<p class="site-author-name" itemprop="name">赖彬鸿</p>
<p class="site-description motion-element" itemprop="description"></p>
</div>
<nav class="site-state motion-element">
<div class="site-state-item site-state-posts">
<a href="/archives/">
<span class="site-state-item-count">135</span>
<span class="site-state-item-name">日志</span>
</a>
</div>
<div class="site-state-item site-state-categories">
<a href="/categories/index.html">
<span class="site-state-item-count">32</span>
<span class="site-state-item-name">分类</span>
</a>
</div>
<div class="site-state-item site-state-tags">
<a href="/tags/index.html">
<span class="site-state-item-count">40</span>
<span class="site-state-item-name">标签</span>
</a>
</div>
</nav>
<div class="feed-link motion-element">
<a href="/atom.xml" rel="alternate">
<i class="fa fa-rss"></i>
RSS
</a>
</div>
<div class="links-of-author motion-element">
<span class="links-of-author-item">
<a href="https://github.com/LbhFront-end" target="_blank" title="GitHub">
<i class="fa fa-fw fa-github"></i>GitHub</a>
</span>
<span class="links-of-author-item">
<a href="https://www.cnblogs.com/lbh2018/" target="_blank" title="博客园">
<i class="fa fa-fw fa-globe"></i>博客园</a>
</span>
<span class="links-of-author-item">
<a href="https://yq.aliyun.com/users/1802204154913774?spm=a2c4e.11153940.blogcont662526.4.6c0a34f6E2lR5o" target="_blank" title="云栖">
<i class="fa fa-fw fa-globe"></i>云栖</a>
</span>
<span class="links-of-author-item">
<a href="mailto:544289495@qq.com" target="_blank" title="E-Mail">
<i class="fa fa-fw fa-envelope"></i>E-Mail</a>
</span>
<span class="links-of-author-item">
<a href="tencent://AddContact/?fromId=45&fromSubId=1&subcmd=all&uin=544289495&website=www.oicqzone.com" target="_blank" title="QQ">
<i class="fa fa-fw fa-qq"></i>QQ</a>
</span>
<span class="links-of-author-item">
<a href="https://www.google.com.hk/search?safe=strict&source=hp&ei=JtLCXIriJ8G4-gS_-4qABQ&q=site%3Alaibh.top&btnK=Google+%E6%90%9C%E7%B4%A2&oq=site%3Alaibh.top&gs_l=psy-ab.3...1158.6834..7051...5.0..1.246.3720.2-17......0....1..gws-wiz.....0..0j0i10.rJMUHvdrbds" target="_blank" title="Google">
<i class="fa fa-fw fa-google"></i>Google</a>
</span>
</div>
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=90 src="//music.163.com/outchain/player?type=0&id=2353471182&auto=0&height=90"></iframe>
<div class="links-of-blogroll motion-element links-of-blogroll-inline">
<div class="links-of-blogroll-title">
<i class="fa fa-fw fa-link"></i>
友情链接
</div>
<ul class="links-of-blogroll-list">
<li class="links-of-blogroll-item">
<a href="http://www.chjtx.com/JRoll/" title="醉萝卜" target="_blank">醉萝卜</a>
</li>
<li class="links-of-blogroll-item">
<a href="http://hzd.plus/" title="Zhendong" target="_blank">Zhendong</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://www.cnblogs.com/cnyball" title="cnyballk" target="_blank">cnyballk</a>
</li>
<li class="links-of-blogroll-item">
<a href="http://johnzz.top/" title="John" target="_blank">John</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://xiaojun1994.top/" title="xiaojun1994" target="_blank">xiaojun1994</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://me.ursb.me" title="Airing" target="_blank">Airing</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://www.iyouhun.com" title="游魂" target="_blank">游魂</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://icoty.github.io/" title="荒野之萍" target="_blank">荒野之萍</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://im-one.github.io/" title="imOne" target="_blank">imOne</a>
</li>
<li class="links-of-blogroll-item">
<a href="http://blog.hourxu.com/" title="Ambre" target="_blank">Ambre</a>
</li>
<li class="links-of-blogroll-item">
<a href="http://www.huyujs.com" title="胡雨" target="_blank">胡雨</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://www.andou.live" title="安逗" target="_blank">安逗</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://www.jianshu.com/u/701a8bbf4f7e" title="陈健斌" target="_blank">陈健斌</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://itobys.github.io/" title="汤姆Tom酱" target="_blank">汤姆Tom酱</a>
</li>
<li class="links-of-blogroll-item">
<a href="https://breeze2.github.io/blog/" title="林毅锋" target="_blank">林毅锋</a>
</li>
<li class="links-of-blogroll-item">