├── js.html
├── images
├── MVC.png
├── MVP.png
├── MVVM.png
├── pubsub.png
├── Observer.png
├── Flyweight.png
├── MVC Model.png
├── MVC Model2.png
└── PublishSubscribe.png
├── visio
├── MVC.vsdx
├── MVP.vsdx
├── MVVM.vsdx
├── pubsub.vsdx
├── Observer.vsdx
├── Flyweight.vsdx
└── PublishSubscribe.vsdx
├── JavaScript权威指南.docx
├── JavaScript高级程序设计.docx
├── 高性能JS.md
├── JS类和继承.md
└── JS设计模式.md
/js.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/MVC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/MVC.png
--------------------------------------------------------------------------------
/images/MVP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/MVP.png
--------------------------------------------------------------------------------
/visio/MVC.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/MVC.vsdx
--------------------------------------------------------------------------------
/visio/MVP.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/MVP.vsdx
--------------------------------------------------------------------------------
/images/MVVM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/MVVM.png
--------------------------------------------------------------------------------
/images/pubsub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/pubsub.png
--------------------------------------------------------------------------------
/visio/MVVM.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/MVVM.vsdx
--------------------------------------------------------------------------------
/visio/pubsub.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/pubsub.vsdx
--------------------------------------------------------------------------------
/JavaScript权威指南.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/JavaScript权威指南.docx
--------------------------------------------------------------------------------
/images/Observer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/Observer.png
--------------------------------------------------------------------------------
/visio/Observer.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/Observer.vsdx
--------------------------------------------------------------------------------
/JavaScript高级程序设计.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/JavaScript高级程序设计.docx
--------------------------------------------------------------------------------
/images/Flyweight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/Flyweight.png
--------------------------------------------------------------------------------
/images/MVC Model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/MVC Model.png
--------------------------------------------------------------------------------
/images/MVC Model2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/MVC Model2.png
--------------------------------------------------------------------------------
/visio/Flyweight.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/Flyweight.vsdx
--------------------------------------------------------------------------------
/images/PublishSubscribe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/images/PublishSubscribe.png
--------------------------------------------------------------------------------
/visio/PublishSubscribe.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziyi2/js/HEAD/visio/PublishSubscribe.vsdx
--------------------------------------------------------------------------------
/高性能JS.md:
--------------------------------------------------------------------------------
1 | # 如何使JS提高运行性能
2 |
3 | ## 数据的存取
4 |
5 | Js中的存储方案
6 |
7 | - 字面量:字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined
8 | - 本地变量:var定义的数据存储单元
9 | - 数组元素:存储在js数组对象内部,以数字作为索引
10 | - 对象成员:存储在js对象内部,以字符串作为索引
11 |
12 | > 从字面量和局部变量中存取数据性能差异微不足道,访问数组和对象成员性能代价高一些。如果在乎运行速度,尽量使用字面量和局部变量,减少数组项和对象成员的使用。
13 |
14 |
15 | ### 作用域链
16 |
17 | 通过原型链知道函数都是Function对象的实例,Function对象和其他对象一样,拥有可以编程访问的属性,其中一个内部属性[[Scope]]包含了函数被创建的作用域中的对象的集合,这个集合被称为作用域链,作用域链决定了哪些数据可以被函数访问。作用域中的每个对象被称为可变对象,每个可变对象都以键值对的形式存在。
18 |
19 | ``` html
20 |
26 | ```
27 |
28 | 当add函数被初始化时(注意不是在add被执行的时候),在作用域链的顶部插入一个全局对象,代表着全局范围内定义的变量的集合,这些变量在add函数内可以被访问。
29 |
30 | | add函数 | 作用域链 | 全局对象 |
31 | | :-------- | :--------| :------ |
32 | | [[Scope]] | 0 | this/window/document/add(注意此时add本身也是全局变量) |
33 |
34 | > add函数的[[Scope]]属性指向了作用域链,作用域链中的首个对象指向全局对象(全局变量的集合)。
35 |
36 | 当add函数被执行时,会创建一个执行环境(执行上下文)的内部对象,一个执行环境定义了一个函数执行时的环境(函数每次的执行环境都是独一无二的),多次调用同一个函数会导致创建多个执行环境,函数执行完毕执行环境会被销毁。全局执行环境直到关闭网页或浏览器才会被销毁。
37 |
38 |
39 |
40 | 每个执行环境都有自己对应的作用域链,用于解析标识符。执行环境被创建的时候,作用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些对象按照出现在函数中的顺序被复制到执行环境的作用域链中(前面说的全局对象,是在初始化的时候就在[[Scope]]中创建完成,而这里所说的是在执行函数中的除全局对象以外的和执行函数相关的局部变量、命名参数、参数集合以及this),作用域链中复制的出现在函数中的对象(局部变量、命名参数、参数集合以及this)的新对象被称为活动对象。活动对象会被推入作用域链的最前端(后面是当前的执行环境的父执行环境的活动对象...一直到最后是全局对象)
41 |
42 | | add函数 | 作用域链 | 活动对象和全部对象 |
43 | | :-------- | :--------| :------ |
44 | | [[Scope]] | 0 | this/arguments/num1/num2/sum |
45 | | [[Scope]] | 1 | this/window/document/add(注意此时add本身也是全局变量) |
46 |
47 | 函数在执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程主要是依靠作用域链进行解析,首先搜索当前执行环境的活动对象,如果找到标识符对应的变量则终止搜索,否则继续搜索作用域链的下一个对象,搜索的过程会持续进行,直到全局对象,如果在全局对象中都没有匹配标识符的变量,那么该变量是未定义的(undefined)。
48 |
49 | > 搜索是会消耗性能的,可以清楚的知道当前执行环境的活动对象的搜索是最快的,而全局对象的搜索则是最耗性能的。并且可以发现如果存在相同的变量,根据作用域链的搜索顺序决定先搜索到的变量被执行。如果某个跨作用域链的值在当前执行环境中被引用不止一次时,可以存储到局部变量中从而提升搜索性能。
50 |
51 |
52 | 需要注意作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
53 |
54 |
55 | ### 改变作用域链
56 |
57 | #### with
58 |
59 | 尽量不要使用with,可能会使局部变量的访问代价更大,因为with会创建新的作用域。
60 |
61 |
62 | #### catch
63 |
64 |
65 | try代码如果发生错误,执行过程会自动跳转到catch子句,然后把异常对象推入一个变量对象并置于作用域的首位,在catch代码块内部,函数所有的局部变量将会放在第二个作用域链对象中。
66 |
67 | 为了尽量简化代码使得catch子句对性能的影响最小化,推荐的做法是将错误委托给一个函数来处理
68 |
69 | ``` html
70 |
77 | ```
78 |
79 | > 函数fn2是catch子句中唯一执行的代码,该函数接受错误产生的异常对象为参数,由于只执行一条语句,且没有局部变量的访问,作用域的临时改变不会影响代码性能。需要注意with或catch或eval子句被称为动态作用域,动态作用域只存在代码执行过程,无法通过静态分析,因此只有确实必要时才推荐使用动态作用域。
80 |
81 | ### 闭包、作用域和内存
82 |
83 | 闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数。
84 |
85 | 闭包可以访问局部作用域之外的数据,可能会导致性能问题。通常函数在执行完毕后会销毁执行环境,内存中仅保存全局作用域Global variable object,但是闭包的情况不同
86 |
87 | ``` html
88 |
99 | ```
100 |
101 | | fn执行环境 | 作用域链 | 活动对象和全部对象 |
102 | | :-------- | :--------| :------ |
103 | | [[Scope]] | 0 | this/arguments/id |
104 | | [[Scope]] | 1 | this/window/fn |
105 |
106 |
107 |
108 | | 匿名函数(闭包) | 作用域链 | 活动对象和全部对象 |
109 | | :-------- | :--------| :------ |
110 | | [[Scope]] | 0 | this/arguments/id |
111 | | [[Scope]] | 1 | this/window/fn |
112 |
113 | 当fn被执行的时候,fn执行环境对应的活动对象被创建(包含id变量),成为作用域链中的最前面的对象,需要注意此时匿名函数被创建(fn执行的过程中创建了匿名函数),匿名函数的[[Scope]]属性被初始化为fn执行环境对应的活动对象和全局对象(书上说是fn的作用域链,这里表述并不清楚,因为fn的作用域链是会被销毁的,但是fn的活动对象不会)。如果此时匿名函数被其他变量进行引用,那么尽管fn执行完毕,但是fn执行环境对应的活动对象并不会被销毁(执行环境中的作用域链仍然会被销毁,活动对象仍然存在内存中),因为很简单啊,匿名函数执行的时候还需要访问fn执行环境的活动对象啊(意味着使用闭包需要更多的内存开销)。
114 |
115 | 当被引用的匿名函数执行时,会创建新的执行环境,它的作用域链与属性[[Scope]]中所引用的两个对象一起被初始化,新的执行环境会创建新的活动对象并把它插入作用域链的首位
116 |
117 |
118 | | 匿名函数(闭包) | 作用域链 | 对象 | 类型 |
119 | | :-------- | :--------| :------ | :------ |
120 | | [[Scope]] | 0 | this/obj1/obj2/value1/value2/arguments | 闭包活动对象 |
121 | | [[Scope]] | 1 | this/arguments/id | fn活动对象 |
122 | | [[Scope]] | 2 | this/window/fn | 全局对象 |
123 |
124 | 需要注意的是闭包产生了新的作用域,fn活动对象被排在了闭包活动对象之后,此时访问fn活动对象不可避免的会带来性能损失(此时如果fn活动对象以及全局对象访问比较频繁,则仍然可以开辟局部变量进行缓存,提升执行速度)。
125 |
126 | ``` html
127 |
135 | ```
136 |
137 | ### 对象成员
138 |
139 | - 原型
140 | - 原型链
141 |
142 | 搜索方法和属性的时候和作用域链类似,先搜索实例对象的属性和方法,如果未找到,则继续搜索原型对象的属性和方法。因此搜索原型对象的属性和方法会带来一定的性能损耗(属性或方法在原型链中存在的位置越深,则消耗的性能越大)。需要注意的是读取实例对象的数据会比读取对象字面量或局部变量的数据更消耗性能。
143 |
144 | > 此时仍然可以通过缓存,将对象成员赋值给局部变量从而提升读写性能。
145 |
146 | ### 总结
147 |
148 | 如何提升存取性能
149 |
150 | - 1. 访问字面量和局部变量的速度最快,访问数组元素和对象成员相对较慢。
151 | - 2. 由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快,全局变量的访问速度最慢。
152 | - 3. 避免使用with和catch,除非是有必要的情况下。
153 | - 4. 嵌套的对象成员会明显影响性能,尽量少用,例如window.loacation.href。
154 | - 5. 属性和方法在原型链中的位置越深,则访问它的速度也越慢。
155 | - 6. 通常来说,需要访问多次的对象成员、数组元素、跨作用域变量可以保存在局部变量中从而提升js执行效率。
156 |
157 |
158 | ## 算法和流程控制
159 |
160 | 代码数量少并不意味着运行速度就快。代码数量多也不以为这运行速度一定慢。
161 |
162 | ### 循环
163 |
164 | 代码的执行时间大部分消耗在循环中。
165 |
166 | #### 循环类型
167 |
168 | - for
169 | - while
170 | - do...while
171 | - for...in...(性能最差)
172 | - for...of...(可以想象性能就算优化过也会比普通循环速度慢)
173 |
174 | > for...in...速度慢的原因是不仅遍历实例的属性还会遍历原型链中继承而来的属性。
175 |
176 | #### 循环性能
177 |
178 |
179 | ##### 性能测试
180 |
181 | ###### 循环次数较少
182 |
183 | ``` html
184 |
245 |
246 | ```
247 |
248 | > 数量极少的时候for速度最快,for...in...和for...of...性能差不多,在循环该测试是在chrome浏览器下。
249 |
250 | ###### 循环次数较多
251 |
252 |
253 | ``` html
254 |
290 |
291 | ```
292 |
293 | > 当循环次数10000甚至是100000以上时,这里以100000次为例,发现for循环性能明显高于for...of,而for...of的性能则又高于for...in...。但是在循环10000次以下的时候,又发现for...of的性能略低于for...in...,但是for...of...的性能是有极大优化空间的,所以for...of和for...in还是推荐使用for...of...。
294 |
295 |
296 | ##### 使用for循环仍然可以优化
297 |
298 |
299 | ``` html
300 |
322 |
323 | ```
324 |
325 | > 但是到百万级别次数甚至以上的时候发现居然还是++耗时少,因此这里觉得没有必要对++ --过多执着。
326 |
327 |
328 | ##### 减少迭代次数
329 |
330 | 循环的性能主要和以下两者有关
331 |
332 | - 每次迭代处理的事务
333 | - 迭代的次数
334 |
335 | 事务可能很难被减少,但是迭代的次数是可以通过代码减少的,一种有效的办法就是使用“Duff装置”(Duff's Device),Duff装置是一种循环体展开技术,它使得一次迭代实际上执行了多次迭代的操作
336 |
337 | ``` html
338 |
367 |
368 | ```
369 |
370 |
371 | ``` html
372 |
444 | ```
445 |
446 | > 发现使用Duff装置后确实能提升性能。
447 |
448 |
449 | ##### 基于函数的迭代
450 |
451 | 需要注意类似于forEach等方法,尽管基于函数的迭代提供了更为便利的迭代方法,但仍然比基于循环的迭代要慢,因为每个数组项调用外部方法打来的开销是速度慢的主要原因。
452 |
453 |
454 | ``` html
455 |
516 |
517 | ```
518 |
519 | ### 条件语句
520 |
521 | 在条件比较多的情况下switch语句比if-else性能高。
522 |
523 | > 大多数语言对switch语句的实现采用branch table(分支表)索引来进行优化,同时在js中switch语句比较值采用全等操作符,不会发生类型转换的性能损耗。当判断多余两个离散值时使用switch是更佳选择。
524 |
525 | #### 优化if-else
526 |
527 | 将最可能出现的条件放在最前面,从而减少条件判断次数
528 |
529 |
530 | ### 查找表
531 |
532 | if-else和switch比使用查找表慢很多,javascript中可以使用数组和普通对象来构建查找表,特别是在条件语句数量很大的时候。
533 |
534 | ``` html
535 |
559 | ```
560 |
561 | > 整个过程变成数组查询或者对象成员查询,不用书写任何判断语句。
562 |
563 | ### 递归
564 |
565 | 递归可能会导致栈溢出,各个浏览器的调用栈大小不同,最常见的栈溢出原因是不正确的终止条件。为了能在浏览器中安全地工作应尽量将递归替换成迭代或者Memoization(Tabulation,主要用于加速程序计算的优化技术,使得函数避免重复演算之前被处理过的参数,而返回已缓存的结果。)
566 |
567 | - 使用迭代替换递归
568 | - 使用memoization优化递归性能
569 |
570 |
571 | ## 定时器优化性能
572 |
573 | UI线程:大多数浏览器让一个单线程共用于执行JavaScript和更新用户界面。任何时候只能执行一种操作,如果JavaScript代码正在执行,那么用户界面可能无法响应输入,或者UI界面正在更新,那么JavaScript代码无法执行。
574 |
575 |
576 | ### 浏览器UI线程
577 |
578 | 用于执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程”。 UI线程的工作基于一个简单的队列系统,任务会被保存在队列中直到进程空闲(空闲了以后会把下一个任务提取出来运行,任务包括执行JavaScript代码、执行UI更新[包括重绘和重排])。
579 |
580 | #### 运行多久合理
581 |
582 | 单个JavaScript操作花费的总时间最大值不应该超过100ms,超过100ms用户会觉得自己与界面失去联系。
583 |
584 | #### 使用定时器让出时间片段
585 |
586 | 如果有些任务确实需要超过100ms才能执行完毕,那么应该使用时间切片分段完成执行任务,从而在时间切片的空闲中可以使得UI有时间段进行更新(执行JavaScript的同时间断性的让出UI线程的控制权用来更新用户界面)。
587 |
588 | > 执行JavaScript(固定的时间段) -> 更新用户界面 -> 执行JavaScript(固定的时间段)-> 更新用户界面 ...
589 |
590 | 为了在执行耗时极大的JavaScript代码时可以让出UI线程的控制权,可以使用定时器。使用setTimeout或setInterval定时器时,会告诉JavaScript引擎先等待一段时间(这段时间可以进行其他队列任务的操作,包括UI界面更新等),然后添加一个JavaScript任务到UI线程队列。需要注意使用定时器的定时时间只是告知何时被加入UI线程的任务队列,并不是何时去执行这段代码,因此真正执行定时器任务的时间一定是大于等于定时时间。
591 |
592 |
593 | ``` html
594 |
595 |
617 | ```
618 |
619 | > 真正执行的时间一定是大于定时时间的。
620 |
621 |
622 |
623 | ``` html
624 |
625 |
653 | ```
654 |
655 | > 此时基本上定时器失去了定时能力,因为三个定时任务被加入队列任务后,都必须等待JavaScript的map循环执行完毕后才能执行,由于定时时间已经超时,因此三个定时任务一等到UI线程空闲就立马一个个排队执行,需要注意的是加入队列的执行顺序仍然按照定时时间的长短来决定。
656 |
657 |
658 | ``` html
659 |
677 |
678 | ```
679 |
680 | > setInterval函数会重复添加JavaScript任务到UI队列,需要注意不同的地方是如果UI队列中已经存在同一个setInterval函数创建的任务,那么后续相同的任务不会被添加到UI队列中(一定是当前setInterval函数创建的任务执行完毕后才会新添加任务的UI队列)。
681 |
682 |
683 | ### 使用定时器处理数组
684 |
685 | - 处理过程不需要同步
686 | - 数据不需要按顺序处理
687 |
688 | 如果满足以上两者条件,可以使用定时器分解任务(防止大数据量数组计算导致UI队列无法更新视图产生停滞现象)。
689 |
690 | > 需要注意UI更新任务的时间如果小于25ms会不够用,因此定时的时长至少应大于25ms。
691 |
692 | 之前所说的JavaScript代码的执行时长不应该超过100ms,超过100ms用户会觉得自己与界面失去联系。如果需要处理大数据量数组,可以使用定时器保持50ms左右的运行时间,保证不会影响用户体验(50ms的时间足够UI更新视图)。
693 |
694 | 可以通过记录运行时间来处理
695 |
696 |
697 | ``` html
698 |
743 | ```
744 |
745 | > 需要注意这种使用方式是页面上只有一个定时器的时候比较适用,如果有多个定时器容易出现性能问题,因为UI线程只有一个。
746 |
747 | ### Web Workers
748 |
749 | 通过Web Workers API可以使代码运行在UI线程之外。JavaScript和UI共享同一个进程,所以可以互相访问,但是Web Workers API由于创建独立的线程,因此和UI线程之间需要通过通信才能传递信息,UI线程中的大数据量处理可以通知Web Workers API进行处理,处理完毕后又可以返回处理结果告知UI线程进行处理,需要注意的是Web Workers API的兼容性很差,只有在IE11中兼容。
750 |
751 |
752 | ## 编程优化
753 |
754 | ### 避免双重求值
755 |
756 | 避免使用代码字符串执行脚本语言,例如使用eval()、Function构造函数、setTimeout()和setInterval()。
757 |
758 | ``` html
759 |
766 | ```
767 |
768 | > 双重求值是一项代价昂贵的操作,比直接包含代码执行速度慢许多。
769 |
770 |
771 | ### 使用位操作
772 |
773 | 在位操作中,数字被转换为有符号32位格式,每次运算符会直接操作该32位数以得到结果,尽管需要转换,但是比布尔操作相比要快得多。
774 |
775 |
776 | ``` html
777 |
784 | ```
785 |
786 |
787 | ``` html
788 |
807 | ```
808 |
809 | #### 使用位掩码
810 |
811 |
812 | ``` html
813 |
838 | ```
839 |
840 | > 左移就是乘以2,右移就是除以2。
841 |
842 |
843 |
--------------------------------------------------------------------------------
/JS类和继承.md:
--------------------------------------------------------------------------------
1 | # js类和继承
2 |
3 |
4 | # JS设计模式
5 |
6 | ## 类、原型和继承
7 |
8 | ### ES5中类的继承
9 |
10 | #### 类(构造函数)
11 |
12 | 构造函数的名字通常用作类名,构造函数是类的公有标识
13 |
14 | ``` javascript
15 | // Person类
16 | function Person(name) {
17 | // 实例属性
18 | this.name = name
19 | // 实例方法
20 | this.getName = function() {
21 | return this.name
22 | }
23 | }
24 |
25 | // 类创建的对象叫类的实例对象
26 | var person = new Person('ziyi2')
27 | ```
28 |
29 | 类的实例对象都有一个不可枚举的属性constructor属性指向类(构造函数)
30 |
31 | ``` javascript
32 | // true
33 | console.log(person.constructor === Person)
34 | ```
35 |
36 | 构造函数是类的公有标识,但是原型是类的唯一标识
37 |
38 | ``` javascript
39 | // 检测实例对象所在的类 true
40 | // 实际上instanceof运算符并不会检测person是否由Person()构造函数初始化而来
41 | // 而是会检查person是否继承自Person.prototype
42 | console.log(person instanceof Person)
43 | // 继承至原型链顶端的Object类 true
44 | console.log(person instanceof Object)
45 | ```
46 |
47 | 所有实例的实例方法并不是同一个
48 |
49 | ``` javascript
50 | var person1 = new Person('ziyi1')
51 | // 创建一个实例对象就会创建一个新的对象物理空间 false
52 | console.log(person1.getName === person.getName)
53 | ```
54 |
55 | 如果不用new关键字,Person是一个普通的全局作用域中的函数
56 |
57 | ``` javascript
58 | Person('ziyi2')
59 | // 全局作用域中调用函数的this指向window全局对象 ziyi2
60 | console.log(window.name)
61 | // ziyi2
62 | console.log(window.getName())
63 | ```
64 |
65 | 注意类的静态属性和静态方法和实例的属性和方法的区别
66 |
67 | ``` javascript
68 | // 类属性
69 | Person.age = 23
70 | // 类方法
71 | Person.getAge = function() {
72 | return this.age
73 | }
74 | ```
75 |
76 | 构造函数创建实例对象的过程和工厂模式类似
77 |
78 | ``` javascript
79 | function createPerson(name) {
80 | var person = new Object()
81 | person.name = name
82 | person.getName = function() {
83 | return this.name
84 | }
85 | return person
86 | }
87 |
88 | let person1 = createPerson('ziyi1')
89 | let person2 = createPerson('ziyi2')
90 | // false
91 | console.log(person1.getName === person2.getName)
92 | ```
93 |
94 | > 工厂模式虽然抽象了创建具体对象的过程,解决了创建多个相似对象的问题,但是没有解决对象的识别问题,即如何知道对象的类型,而类(构造函数)创建的实例对象可以识别实例对象对应哪个原型对象(需要注意原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象,才属于同一个类的实例,而构造函数并不能作为类的唯一标识)。
95 |
96 | 构造函数的创建过程
97 |
98 | - 创建一个新对象
99 | - 将构造函数的作用域赋给新对象(this新对象)
100 | - 执行构造函数中的代码
101 | - 返回新对象(最终返回的就是new出来的实例对象,因此this指向实例对象)
102 |
103 | #### 原型
104 |
105 | ##### 原型特性
106 |
107 | ``` javascript
108 | // Person类(构造函数、Function类的实例对象、函数)
109 | function Person(name) {
110 | this.name = name
111 | }
112 | // Person类是一个Function类的实例 true
113 | // Person继承自Function
114 | console.log(Person instanceof Function)
115 | ```
116 |
117 | 只要创建一个类(构造函数、Function类的实例对象、函数),就会为类创建一个prototype属性,这个属性指向类的原型对象,在默认情况下,原型对象会自动获得一个constructor(构造函数)属性,这个属性对应类本身(构造函数)
118 |
119 | ```javascript
120 | console.log(Person.prototype.constructor === Person)
121 | ```
122 |
123 | 类的所有实例共享一个原型对象,如果实例对象的方法可以通用,可以通过原型对象共享方法,而不需要为每一个实例对象开辟一个需要使用的实例方法。
124 |
125 | ``` javascript
126 | // 原型对象的方法
127 | Person.prototype.getName = function() {
128 | return this.name
129 | }
130 | // Person类的实例对象
131 | var person = new Person('ziyi2')
132 | var person1 = new Person('ziyi1')
133 | // 两个实例对象引用的是同一个原型方法 true
134 | console.log(person1.getName === person.getName)
135 | ```
136 |
137 | 当调用构造函数创建新实例后,该实例内部将包含一个指向构造函数原型对象的[[Prototype]] 内部属性,脚本中没有标准的方式访问[[Prototype]],但在一些浏览器诸如Firefox、Safari、Chrome在每个对象上都支持属性__proto__,这个引用存在于实例与构造函数的原型对象之间,调用构造函数创建的实例都有[[Prototype]]属性,但是无法访问,可以通过isPrototypeOf()方法来确定原型对象是否对应当前实例对象
138 |
139 | ``` javascript
140 | // true
141 | console.log(person.__proto__ === Person.prototype)
142 | // true
143 | console.log(Person.prototype.isPrototypeOf(person))
144 | ```
145 |
146 | 读取原型链的方法和属性时,会向上遍历搜索,首先搜索实例对象本身有没有同名属性和方法,有则返回,如果没有,则继续搜索实例对象对应的原型对象的方法和属性。
147 |
148 | ```javascript
149 |
150 | // Person类的原型对象的属性
151 | // 需要注意原型对象具有动态性
152 | // 先创造实例对象后修改原型对象也能够立即生效
153 | Person.prototype.age = 28
154 | // 获取原型对象的属性 28
155 | console.log(person.age)
156 | // 检测属性是否存在于实例对象中 false
157 | console.log(person.hasOwnProperty('age'))
158 | // 给实例对象赋值属性
159 | person.age = 11
160 | // 获取了实例对象的属性 11
161 | console.log(person.age)
162 | // 检测属性是否存在于实例对象中 true
163 | console.log(person.hasOwnProperty('age'))
164 | // 删除实例对象的属性
165 | delete person.age
166 | // 仍然可以重新获取原型对象的属性 28
167 | console.log(person.age)
168 | // 检测属性是否存在于实例对象中 false
169 | console.log(person.hasOwnProperty('age'))
170 |
171 |
172 | // 判断属性存在于实例对象还是原型对象(也可能属性不存在)
173 | function hasPrototypePrpperty(obj,property){
174 | // in可以检测到实例对象和原型对象中的属性
175 | return !obj.hasOwnProperty(property) && (property in obj)
176 | }
177 |
178 | // for...in循环
179 | // 返回所有能够通过对象访问、可枚举的(enumerated)属性,包括实例和原型对象的中的属性
180 | for(let key in person) {
181 | // name、age
182 | // name是实例对象的属性
183 | // age是原型对象的属性
184 | console.log(key)
185 | }
186 |
187 | // Object.keys()
188 | // 返回所有可枚举的实例对象的属性
189 | // ['name']
190 | console.log(Object.keys(person))
191 | ```
192 |
193 | 创建类的时候默认会给类创建一个prototype属性,是类的原型对象的引用,也可以重写改类的原型对象
194 |
195 | ``` javascript
196 | function Person(name) {
197 | this.name = name
198 | }
199 |
200 | Person.prototype = {
201 | getName: function() {
202 | return this.name
203 | }
204 | }
205 |
206 | let person = new Person('ziyi2')
207 | // 此时Person类原型对象是一个新的对象
208 | // 注意和Person.prototype.getName的区别
209 | // 这个新的对象的constructor属性对应Object类
210 | console.log(Person.prototype.constructor === Object)
211 |
212 |
213 | Person.prototype = {
214 | // 使新的原型对象的constructor属性对应Person类
215 | constructor: Person,
216 | getName: function() {
217 | return this.name
218 | }
219 | }
220 |
221 | // true
222 | console.log(Person.prototype.constructor === Person)
223 |
224 | // 注意未重写原型对象之前的实例仍然指向未重写前的原型对象
225 | for(let key in person) {
226 | // name,getName
227 | console.log(key)
228 | }
229 |
230 | let person1 = new Person('ziyi1')
231 |
232 | // 重写的原型对象的constructor属性变成了可枚举属性
233 | for(let key in person1) {
234 | // name,constructor,getName
235 | console.log(key)
236 | }
237 |
238 | // 将constructor属性配置成不可枚举属性
239 | Object.defineProperty(Person.prototype,"constructor",{
240 | enumerable:false,
241 | value:Person
242 | })
243 |
244 | for(let key in person1) {
245 | // name,getName
246 | console.log(key)
247 | }
248 | ```
249 |
250 | ##### 原型的弊端
251 |
252 |
253 | 原型对象的基本类型数据的属性(存放的是具体的值,因此每个实例对象的该属性值的改变互不影响)的共享对于实例对象而言非常便捷有效,但是原型对象的引用类型属性不同,原型对象的引用类型的属性存放的是一个指针(C语言中的指针的意思,指针存放的是一个地址,并不是存放一个具体的值,因为类似数组等值在一个32bit的物理块中是放不下的,肯定是放在一个连续的物理块中,因此需要一个地址去读取这些连续的物理块),指针最终指向的是一个连续的物理块,因此通过原型对象的引用类型修改的值都是改变这个物理块中的值,因此所有实例对象的该指向都会发生变化。
254 |
255 |
256 |
257 | ``` javascript
258 | function Person(name) {
259 | this.name = name
260 | }
261 |
262 | Person.prototype = {
263 | constructor: Person,
264 | getName: function() {
265 | return this.name
266 | },
267 | age: 28,
268 | names: ['ziyi', 'ziyi1', 'ziyi2']
269 | }
270 |
271 | var person = new Person()
272 | person.names[0] = 'ziyi_modify'
273 | person.age = 11
274 | var person1 = new Person()
275 | // ["ziyi_modify", "ziyi1", "ziyi2"]
276 | console.log(person1.names)
277 | // 28
278 | console.log(person1.age)
279 | ```
280 |
281 | ##### 组合构造函数和原型模式
282 |
283 | 构造函数定义实例属性,原型对象定义共享的方法和基本数据类型的属性
284 |
285 | ``` javascript
286 | function Person(name) {
287 | this.name = name
288 | this.names = ['ziyi', 'ziyi1', 'ziyi2']
289 | }
290 |
291 | Person.prototype = {
292 | constructor: Person,
293 | getName: function() {
294 | return this.name
295 | }
296 | }
297 |
298 | var person = new Person()
299 | person.names[0] = 'ziyi_modify'
300 | var person1 = new Person()
301 | // ['ziyi', 'ziyi1', 'ziyi2']
302 | console.log(person1.names)
303 | ```
304 |
305 | #### 继承
306 |
307 | 继承分为接口继承和实现继承,ECMAScript只支持实现继承,实现继承主要依靠原型链。
308 |
309 | ##### 原型链
310 |
311 | 假设有两个类(Son类和Father类),Son类对应一个原型对象,通过Son类创建的实例对象都包含一个指向Son类原型对象的内部指针[[Prototype]](大部分浏览器支持实例对象的__proto__属性访问原型对象)。假如让Son类的原型对象引用Father类的实例对象,则Son类的原型对象将包含一个指向Father类的原型对象的内部指针[[Prototype]],从而使Son类的原型对象可以共享Father类原型对象和实例对象(注意这里也包括实例对象)的方法和属性,从而又使Son类的实例对象可以共享Father类原型对象的方法和属性,这就是原型链。
312 |
313 | 原型链实现了方法和属性的继承,此时Son类是子类,继承了Father类这个父类。
314 |
315 | ``` javascript
316 | function Father() {
317 | this.names = ['ziyi','ziyi1', 'ziyi2']
318 | }
319 | function Son() {}
320 |
321 | Father.prototype.getName = function() {
322 | return Father.name
323 | }
324 |
325 | // Son类的原型对象包含了Father类原型对象的方法和属性
326 | // 同时也包含了Father类实例对象的实例方法和实例属性
327 | Son.prototype = new Father()
328 |
329 | // 重写了Son类的原型对象
330 | // Son.prototype.constructor !== Son
331 | // true
332 | console.log(Son.prototype.constructor === Father)
333 |
334 | let son = new Son()
335 |
336 | // 继承Father类实例对象的引用类型属性
337 | // ["ziyi", "ziyi1", "ziyi2"]
338 | console.log(son.names)
339 |
340 | // 继承Father类原型对象的方法
341 | console.log(son.getName())
342 | ```
343 |
344 | > 读取对象的属性和方法时,会执行搜索,首先搜索实例对象本身有没有同名的属性和方法,有则返回, 如果没有找到,那么继续搜索原型对象,在原型对象中查找具有给定名字的属性和方法。
345 |
346 | 实现原型链,本质上就是扩展了原型搜索机制
347 |
348 | - 搜索实例
349 | - 搜索实例的原型(该原型同时也是另一个类的实例)
350 | - 搜索实例的原型的原型
351 | - ...
352 |
353 | 一直向上搜索,直到第一次搜索到属性或者方法为止,搜索不到,到原型链的末端停止。
354 |
355 |
356 | 该继承方法有一个缺陷,具体可以查看原型的弊端,在原型中使用引用类型的属性,在所有的实例对象中的该属性都引用了同一个物理空间,一旦空间的值发生了变化,那么所有实例对象的该属性值就发生了变化。
357 |
358 | ``` javascript
359 | // son实例对象修改Father类实例对象(Son类的原型对象)的引用类型属性
360 | son.names.push('ziyi3')
361 | let son1 = new Son()
362 | // son1中仍然引用的是Father类实例对象(Son类的原型对象)的引用类型属性
363 | // ["ziyi", "ziyi1", "ziyi2", "ziyi3"]
364 | console.log(son1.names)
365 | ```
366 |
367 | ##### 借用构造函数(伪造对象或经典继承)
368 |
369 | 为了避开原型中有引用类型数据的问题,做到子类继承(这里的继承只是创建和父类相同的实例对象的属性和方法)父类的实例对象的实例方法和实例属性,在子类的构造函数中调用父类的构造函数,从而使子类的this对象在父类构造函数中执行,并最终返回的是子类的this对象(我们知道子类的this对象在构造函数的执行过程中都是开辟新的对象空间,因此引用类型的实例属性都是不同的指针地址)。
370 |
371 | ``` javascript
372 | function Father() {
373 | this.names = ['ziyi','ziyi1', 'ziyi2']
374 | }
375 | function Son() {
376 | // 使用new关键字执行的构造函数this指向实例对象
377 | // 注意如果不用new关键字执行this指向全局对象window
378 | // 这里的Father类当做一个普通的执行函数
379 | // 此时只是让Son类的实例对象创建了和Father类实例对象一样的实例属性和实例方法
380 | Father.call(this)
381 |
382 | // Father.call(this)类似于在Son构造函数中执行
383 | // this.names = ['ziyi','ziyi1', 'ziyi2']
384 | }
385 |
386 | let son = new Son()
387 | son.names.push('ziyi3')
388 | // ["ziyi", "ziyi1", "ziyi2", "ziyi3"]
389 | console.log(son.names)
390 |
391 | let son1 = new Son()
392 | // ['ziyi','ziyi1', 'ziyi2']
393 | console.log(son1.names)
394 | ```
395 |
396 | 如果此时父类有自己的实例属性,而子类也有自己的实例属性
397 |
398 | ``` javascript
399 | function Father(name,age) {
400 | this.name = name
401 | this.age = age
402 | }
403 | function Son() {
404 | Father.apply(this, arguments)
405 | this.job = arguments[2]
406 | }
407 |
408 | let son = new Son('ziyi2', 28, 'web')
409 | // {name: "ziyi2", age: 28, job: "web"}
410 | console.log(son)
411 | ```
412 |
413 | ##### 组合继承
414 |
415 | 为了防止原型对象引用类型在实例对象中是同一个指针的问题,在原型链的实现中可以混合借用构造函数实现组合继承
416 |
417 | ``` javascript
418 | function Father(name,age) {
419 | this.name = name
420 | this.age = age
421 | // 引用类型的实例属性
422 | this.names = []
423 | }
424 |
425 | Father.prototype.getNames = function() {
426 | return this.names
427 | }
428 |
429 | function Son(name, age, job) {
430 | // 借用构造函数
431 | // this执行的过程中也会创建this.names实例属性
432 | Father.call(this, name, age)
433 | this.job = job
434 | }
435 |
436 | // 创建原型链
437 | // 需要注意此时Son类的原型对象中还是有Father类的实例对象的属性和方法
438 | Son.prototype = new Father()
439 | // 调整Son原型对象的constructor指向
440 | Son.prototype.constructor = Son
441 |
442 | let son = new Son('ziyi2', 28, 'web')
443 | son.names.push('ziyi2')
444 | // { age:28 job:"web" name:"ziyi2" names:["ziyi2"] }
445 | console.log(son)
446 | let son1 = new Son('ziyi2', 28, 'web')
447 | // [] son.name和son1.names是不同的指针,指向不同的物理空间
448 | console.log(son1.names)
449 | ```
450 |
451 | > 需要注意的是Son类的实例对象中有name、age和names属性,Son类的原型对象中也有这些属性,只是根绝原型链的搜索机制,在使用实例对象访问这些属性时,首先搜索了实例对象中的该同名属性,因此原型对象中的该属性被屏蔽。
452 |
453 | ``` javascript
454 | // undefined
455 | console.log(Son.prototype.age)
456 | // undefined
457 | console.log(Son.prototype.name)
458 | // []
459 | console.log(Son.prototype.names)
460 | ```
461 |
462 | ##### 寄生组合式继承
463 |
464 | | 类型 | 优缺点 |
465 | | :-------- | :--------|
466 | | 构造函数模式 | 可以创建不同实例属性的副本,包括引用类型的实例属性,但是不能共享方法 |
467 | | 原型模式 | 引用类型的属性对于实例对象而言共享同一个物理空间,因此可以共享方法 |
468 | | 原型链 | 对父类实现方法和属性继承的过程中,父类实例对象的引用类型属性在子类的实例中共享同一个物理空间,因为父类的实例对象指向了子类的原型对象 |
469 | | 借用构造函数 | 解决了继承中的引用值类型共享物理空间的问题,但是没法实现方法的共享 |
470 | | 组合继承 | 属性的继承使用借用构造函数方法,方法的继承使用原型链技术,即解决了引用值类型共享的问题,又实现了方法的共享,但是子类的原型对象中还存在父类实例对象的实例属性 |
471 |
472 | 目前而言,组合继承已经可以解决大部分问题,但是也有缺陷,就是会调用两次父类的构造函数,一次是实现原型时使子类的原型等于父类的实例对象调用了父类构造函数(同时在子类的原型对象中还存在了父类实例对象的实例属性),一次是使用子类构造函数时调用了一次父类构造函数。
473 |
474 | 寄生组合式继承可以解决在继承的过程中子类的原型对象中还存在父类实例对象的实例属性的问题。
475 |
476 | ``` javascript
477 | // 继承o原型对象的方法和属性
478 | // 需要注意o的引用类型属性在F实例对象中共享同一个物理空间(需要避免使用引用类型属性值)
479 | // 当然o的方法在F实例对象中共享
480 | function objectCreate(o) {
481 | // F类没有实例方法和实例属性
482 | function F() {}
483 | F.prototype = o
484 | // o是F类的原型对象
485 | // 返回F类的实例对象
486 | // new F().__proto__ = o
487 | // F类实例对象共享o的属性和方法
488 | // true
489 | console.log(new F().__proto__ === o)
490 | return new F()
491 | }
492 |
493 |
494 | // SubClass实现对于SuperClass原型对象的方法和属性的继承
495 | function inheritPrototype(SubClass, SuperClass) {
496 | // prototype是一个实例对象
497 | // prototype不是SuperClass的实例对象而是另一个类F类的实例对象
498 | // SuperClass类和objectCreate函数中的F类的原型对象都是SuperClass.prototype
499 | // 这里没有调用SuperClass构造函数
500 | // prototype继承了SuperClass类的原型对象的方法和属性
501 | var prototype = objectCreate(SuperClass.prototype)
502 | // 使prototype实例对象的constructor属性指向SubClass子类
503 | // 因为重写SuperClass的原型对象时会修改constructor属性
504 | // SubClass.prototype.constructor = SubClass
505 | prototype.constructor = SubClass
506 | // 使SubClass类的原型对象指向prototype实例对象
507 | // 类似于SubClass.prototype = new SuperClass()
508 | // 只是这里是SubClass.prototype = new F()
509 | // F类本身没有实例方法和实例属性
510 | SubClass.prototype = prototype
511 | }
512 |
513 |
514 |
515 | function Father(name, age) {
516 | this.name = name
517 | this.age = age
518 | }
519 |
520 | Father.prototype.getName = function() {
521 | return this.name
522 | }
523 |
524 | function Son1(name, age, job) {
525 | // 借用构造函数
526 | Father.apply(this, arguments)
527 | this.job = job
528 | }
529 |
530 | function Son2(name, age, job) {
531 | // 借用构造函数
532 | Father.apply(this, arguments)
533 | this.job = job
534 | }
535 |
536 |
537 |
538 | // 组合继承的写法
539 | Son1.prototype = new Father()
540 | Son1.prototype.constructor = Son1
541 | // age : undefined
542 | // constructor : ƒ Son1(name, age, job)
543 | // name : undefined
544 | // __proto__ : { getName:ƒ () constructor : ƒ Father(name, age) }
545 | console.log(Son1.prototype)
546 |
547 | // 寄生组合式继承的写法
548 | // 借用构造函数实现构造函数的方法和属性的继承
549 | // inheritPrototype函数实现原型对象的方法和属性的继承
550 | inheritPrototype(Son2, Father)
551 | // constructor : ƒ Son2(name, age, job)
552 | // __proto__ : { getName:ƒ () constructor : ƒ Father(name, age) }
553 | console.log(Son2.prototype)
554 |
555 |
556 | var son1 = new Son1('ziyi2', 28, 'web')
557 | var son2 = new Son2('ziyi3', 28, 'hardware')
558 | son1.getName()
559 | son2.getName()
560 | ```
561 |
562 | ### ES6中类的继承
563 |
564 | #### 类
565 |
566 | ES6中的类只是ES5封装后的语法糖而已
567 |
568 | ``` javascript
569 | // ES6中实现的Person类
570 | class Es6Person {
571 | constructor(name, age) {
572 | // 实例对象的属性
573 | this.name = name
574 | this.age = age
575 | // 实例对象的方法
576 | this.getName = () => {
577 | return this.name
578 | }
579 | }
580 |
581 | // Person类原型对象的方法
582 | getAge() {
583 | return this.age
584 | }
585 | }
586 |
587 |
588 | // ES5中实现的Person类
589 | function Es5Person (name, age) {
590 | // 实例对象的属性
591 | this.name = name
592 | this.age = age
593 | // 实例对象的方法
594 | this.getName = () => {
595 | return this.name
596 | }
597 | }
598 |
599 | // Person类原型对象的方法
600 | Es5Person.prototype.getAge = function() {
601 | return this.age
602 | }
603 | ```
604 |
605 | 在ES5中类的原型对象的方法是可枚举的,但是ES6中不可枚举
606 |
607 | ``` javascript
608 | // []
609 | console.log(Object.keys(Es6Person.prototype))
610 | // ["getAge"]
611 | console.log(Object.keys(Es5Person.prototype))
612 | ```
613 |
614 |
615 | 在ES5中如果不用new,this指向windows全局变量,在ES6如果不用new关键字则会报错处理
616 |
617 | ``` javascript
618 | // Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
619 | let person2 = Es6Person('ziyi2', 111)
620 | ```
621 |
622 |
623 | ES6中的类是不会声明提升的, ES5可以
624 |
625 | ``` javascript
626 | // Es5Person {}
627 | console.log(Es5Person)
628 | // Uncaught ReferenceError: Es6Person is not defined
629 | let es6 = new Es6Person()
630 | console.log(es6)
631 |
632 | class Es6Person {}
633 | function Es5Person {}
634 | ```
635 |
636 |
637 | 在ES6中如果不写构造方法
638 |
639 | ``` javascript
640 | class Es6Person {}
641 |
642 | // 等同于
643 | class Es6Person {
644 | constructor() {}
645 | }
646 | ```
647 |
648 |
649 | 在ES6中类的属性名可以采用表达式
650 |
651 | ``` javascript
652 | const getAge = Symbol('getAge')
653 |
654 | class Es6Person {
655 | constructor(name, age) {
656 | this.name = name
657 | this.age = age
658 | this.getName = () => {
659 | return this.name
660 | }
661 | }
662 |
663 | // 表达式
664 | [getAge]() {
665 | return this.age
666 | }
667 | }
668 |
669 | let es6Person = new Es6Person('ziyi2', 28)
670 | es6Person[getAge]()
671 | ```
672 |
673 | #### 类的私有方法和私有属性
674 |
675 | 在ES6的类中目前并没有私有方法和私有属性的标准语法,但是可以通过其他方式模拟实现,例如属性名采用表达式,而ES5中很难实现
676 |
677 | ``` javascript
678 | // 利用Symbol值的唯一性
679 | const getAge = Symbol('getAge')
680 | const job = Symbol('job')
681 |
682 | class Es6Person {
683 | constructor(name, age) {
684 | this.name = name
685 | this.age = age
686 | this.getName = () => {
687 | return this.name
688 | }
689 | // 私有属性
690 | this[job] = 'web'
691 | }
692 |
693 | // 私有方法
694 | [getAge]() {
695 | return this.age
696 | }
697 | }
698 |
699 | let es6Person = new Es6Person('ziyi2', 28)
700 | es6Person[getAge]()
701 | es6Person[job]
702 | ```
703 |
704 | > 此时如果Es6Person类处在一个文件中,那么getAge和job变量是当前文件的局部变量,外部文件无法访问,从而在外部文件调用Es6Person类的时候无法访问具有唯一性的getAge和job变量,从而使之成为私有方法和私有属性。目前私有属性有一个3阶段的[提案](https://github.com/tc39/proposal-private-methods)。
705 |
706 |
707 | #### 类的getter和setter
708 |
709 | ES6利用get和set关键字对某属性设置存值函数和取值函数拦截属性的存取行为和ES5一样。
710 |
711 | ``` javascript
712 | class Es6Person {
713 | constructor(name,age) {
714 | this.name = name
715 | this.age = age
716 | }
717 |
718 | get name() {
719 | return 'ziyi2'
720 | }
721 |
722 | set name(value) {
723 | console.log('setter: ', value)
724 | }
725 | }
726 |
727 | // setter: ziyi3
728 | let person = new Es6Person('ziyi3', 11)
729 | // ziyi2
730 | console.log(person.name)
731 | // setter: ziyi2
732 | person.name = 'ziyi2'
733 | ```
734 |
735 | #### 类的Generator方法
736 |
737 | 给类增加一个遍历器,从而使类可以进行for...of操作
738 |
739 | ``` javascript
740 | class Es6Person {
741 | constructor(...args) {
742 | this.args = args
743 | }
744 |
745 | * [Symbol.iterator]() {
746 | for(let arg of this.args) {
747 | yield arg
748 | }
749 | }
750 | }
751 |
752 | let person = new Es6Person('ziyi3', 11)
753 |
754 | for(let value of person) {
755 | // ziyi3
756 | // 11
757 | console.log(value)
758 | }
759 | ```
760 |
761 | #### 类的静态方法
762 |
763 | ``` javascript
764 | class Es6Person {
765 | // 类的静态方法
766 | static getClassName() {
767 | // this指向类,而不是实例对象
768 | return 'Es6Person'
769 | }
770 |
771 | // 类的静态方法
772 | static getName() {
773 | // this指向类,而不是实例对象
774 | return this.getClassName()
775 | }
776 | }
777 |
778 | // Es6Person
779 | console.log(Es6Person.getName())
780 | ```
781 |
782 |
783 | #### 类的静态属性提案
784 |
785 | ES6明确规定,Class内部只有静态方法,没有静态属性,目前有一个静态属性的提案,对静态属性规定了新的写法
786 |
787 | ``` javascript
788 | class Es6Person {
789 | static className = 'Es6Person'
790 | }
791 | ```
792 |
793 | #### new.target
794 |
795 | new.target返回new命令作用于的那个构造函数
796 |
797 |
798 | ``` javascript
799 | class Es6Person {
800 | constructor(name) {
801 | // new.target = class Es6Person {}
802 | if(new.target === Es6Person) {
803 | this.name = name
804 | } else {
805 | throw new Error('没有用new命令生成实例对象')
806 | }
807 | }
808 | }
809 | let person = new Es6Person('ziyi2')
810 | ```
811 |
812 | > new.target可以用于指定哪些子类可以继承父类。
813 |
814 | #### 类的继承
815 |
816 | ES5 的继承使用借助构造函数实现,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
817 |
818 |
819 | 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象
820 |
821 | ``` javascript
822 | class Es6Person {
823 | constructor(name) {
824 | this.name = name
825 | }
826 | }
827 |
828 |
829 | class Es6WebDeveloper extends Es6Person {
830 | constructor(name, age) {
831 | // 表示父类构造函数
832 | // 子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例
833 | super(name)
834 | this.age = age
835 | }
836 | }
837 | ```
838 |
839 | 例如以下写法会报错
840 |
841 | ``` javascript
842 | class Es6Person {
843 | constructor(name) {
844 | this.name = name
845 | }
846 | }
847 |
848 |
849 | class Es6WebDeveloper extends Es6Person {
850 | constructor(name, age) {
851 | // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
852 | console.log(this)
853 | super(name)
854 | }
855 | }
856 |
857 | let developer = new Es6WebDeveloper('ziyi2', 11)
858 |
859 | ```
860 |
861 | 此时的继承类似于ES5的寄生组合式继承,子类的原型对象中没有父类的实例对象和实例方法
862 |
863 | ``` javascript
864 | // Es6Person {constructor: ƒ}
865 | console.log(Es6WebDeveloper.prototype)
866 | ```
867 |
868 | ES6在继承的语法上显然也考虑的更加周到,不仅继承了类的原型对象,还继承了类的静态属性和静态方法
869 |
870 | ``` javascript
871 | class Es6Person {
872 |
873 | static getClassName() {
874 | return Es6Person.name
875 | }
876 |
877 | constructor(name) {
878 | this.name = name
879 | }
880 | }
881 |
882 |
883 | class Es6WebDeveloper extends Es6Person {
884 | constructor(name, age) {
885 | super(name)
886 | this.age = age
887 | }
888 | }
889 |
890 | // Es6Person
891 | Es6WebDeveloper.getClassName()
892 | ```
893 |
894 |
895 | #### super关键字
896 |
897 | super作为函数调用时,代表父类的构造函数,只能在子类的构造函数中使用
898 |
899 | ``` javascript
900 | class Es6Person {
901 | constructor(name) {
902 | // Es6WebDeveloper
903 | console.log(new.target.name)
904 | // Es6WebDeveloper {}
905 | console.log(this)
906 | this.name = name
907 | }
908 | }
909 |
910 | class Es6WebDeveloper extends Es6Person {
911 | constructor(name, age) {
912 | // 类似于A.prototype.constructor.call(this)
913 | // super内部的this指向Es6WebDeveloper
914 | super(name)
915 | this.age = age
916 | }
917 | }
918 |
919 | let developer = new Es6WebDeveloper('ziyi2')
920 | ```
921 |
922 | 除此之外,super当做一个对象使用,在子类普通方法(原型对象)中,指向父类的原型对象,因此可以调用父类的原型方法,需要注意的是执行父类的原型方法时,在方法中执行时this指向的是子类的实例对象而不是父类的实例对象
923 |
924 | ``` javascript
925 | class Es6Person {
926 | constructor(name) {
927 | this.name = name
928 | }
929 | getName() {
930 | // Es6WebDeveloper {name: "ziyi2", age: undefined}
931 | // 子类的whoIsSuper中调用的super.getName在这里执行的this指向子类实例对象
932 | console.log(this)
933 | return this.name
934 | }
935 | }
936 |
937 | Es6Person.prototype.className = Es6Person.name
938 |
939 | class Es6WebDeveloper extends Es6Person {
940 | constructor(name, age) {
941 | super(name)
942 | this.age = age
943 | }
944 | whoIsSuper() {
945 | // true
946 | console.log(super.getName === Es6Person.prototype.getName)
947 |
948 | // super无法获取父类的实例属性
949 | // 因为super指向的是父类的原型对象
950 |
951 | // undefined
952 | console.log(super.name)
953 | // Es6Person
954 | console.log(super.className)
955 |
956 | return super.getName()
957 | }
958 | }
959 |
960 | let developer = new Es6WebDeveloper('ziyi2')
961 | // ziyi2
962 | console.log(developer.whoIsSuper())
963 | ```
964 |
965 | 如果super用作对象且不在子类的原型对象中调用,而是在子类的静态方法中调用,那么super指代父类而不是父类的原型对象,同理调用父类静态方法时this指向子类而不是父类
966 |
967 | ``` javascript
968 | class Es6Person {
969 | static getClassName() {
970 | // class Es6WebDeveloper extends Es6Person {
971 | // ...
972 | // 在子类中使用super调用时,this指向子类
973 | console.log(this)
974 | return this.name
975 | }
976 |
977 | constructor(name) {
978 | this.name = name
979 | }
980 | }
981 |
982 | class Es6WebDeveloper extends Es6Person {
983 | constructor(name, age) {
984 | super(name)
985 | this.age = age
986 | }
987 |
988 | static getClassName() {
989 | return super.getClassName()
990 | }
991 | }
992 |
993 | // Es6WebDeveloper
994 | console.log(Es6WebDeveloper.getClassName())
995 | ```
996 |
997 | #### ES6的继承原理
998 |
999 | __proto__存在于实例与构造函数的原型对象之间
1000 |
1001 | ``` javascript
1002 | class Es6Person {
1003 | constructor(name) {
1004 | this.name = name
1005 | }
1006 | }
1007 |
1008 | let es6Person = new Es6Person('ziyi2')
1009 | // true
1010 | console.log(es6Person.__proto__ === Es6Person.prototype)
1011 | ```
1012 |
1013 |
1014 | ES6的继承原理
1015 |
1016 | ``` javascript
1017 | class Es6Person {}
1018 | class Es6WebDeveloper extends Es6Person {}
1019 |
1020 | // true
1021 | // Es6WebDeveloper看做一个实例对象
1022 | // Es6Person看做一个原型对象
1023 | // 因此Es6WebDeveloper继承了Es6Person的所有属性和方法
1024 | // 实现了类的静态属性和方法的继承
1025 | // 子类的原型是父类
1026 | console.log(Es6WebDeveloper.__proto__ === Es6Person)
1027 |
1028 | // true
1029 | // 这里类似于 Es6WebDeveloper.prototype = new Es6Person()
1030 | // 和ES5的原型链一样
1031 | // 子类的原型对象是父类的原型对象的实例
1032 | // 子类的实例继承父类的实例
1033 | console.log(Es6WebDeveloper.prototype.__proto__ === Es6Person.prototype)
1034 | ```
1035 |
1036 | 因此以下继承都是可理解的
1037 |
1038 | ``` javascript
1039 | class A extends Object {
1040 | }
1041 |
1042 | A.__proto__ === Object // true
1043 | A.prototype.__proto__ === Object.prototype // true
1044 |
1045 |
1046 | class A {
1047 | }
1048 |
1049 | A.__proto__ === Function.prototype // true
1050 | A.prototype.__proto__ === Object.prototype // true
1051 | ```
1052 |
1053 |
1054 |
1055 |
1056 |
1057 |
1058 |
--------------------------------------------------------------------------------
/JS设计模式.md:
--------------------------------------------------------------------------------
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 | JavaScript中的反模式示例如下
38 |
39 | - 在全局上下文中定义大量的变量污染全局命名空间
40 | - 向setTimeout或setInterval传递字符串,而不是函数
41 | - 修改Object类的原型
42 | - 以内联形式使用JavaScript
43 | - 使用document.write
44 |
45 | ### 设计模式的类别
46 |
47 | #### 创建型
48 |
49 | 专注于处理对象创建机制,以适合给定情况的方式来创建对象。创建对象的基本方法可能导致项目复杂性增加,而创建型模式旨在通过控制创建过程来解决这种问题。
50 |
51 | | 设计模式 | 描述 |
52 | | :--------| :------: |
53 | | Constructor(构造器) | |
54 | | Factory(工厂) | |
55 | | Abstract(抽象) | |
56 | | Prototype(原型) | |
57 | | Singleton(单例) | |
58 | | Singleton(单例) | |
59 | | Builder(生成器) | |
60 |
61 | #### 结构型
62 |
63 | 结构型与对象组合有关,通常可以用于找出在不同对象之间建立关系的简单方法。这种模式有助于确保在系统某一部分发生变化时,系统的整个结构不需要同时改变,同时对于不适合某一特定目的而改变的系统部分,也能够完成重组。
64 |
65 | | 设计模式 | 描述 |
66 | | :--------| :------: |
67 | | Iterator(装饰者) | |
68 | | Facade(外观) | |
69 | | Flyweight(享元) | |
70 | | Adapter(适配器) | |
71 | | Proxy(代理) | |
72 |
73 | #### 行为
74 |
75 | 行为设计模式专注于改善或简化系统中不同对象之间的通信。
76 |
77 | | 设计模式 | 描述 |
78 | | :--------| :------: |
79 | | Iterator(迭代器) | |
80 | | Mediator(中介者) | |
81 | |Observer(观察者) | |
82 | |Visitor(访问者) | |
83 |
84 | ## Constructor(构造器)模式
85 |
86 | ``` javascript
87 | function Person() {}
88 | var person = new Person()
89 |
90 | // 带原型的Constructor
91 | Person.prototype.getName = {}
92 | ```
93 |
94 | ## Module(模块)模式
95 |
96 | - 对象字面量表示法
97 | - Module模式
98 | - AMD模式
99 | - CommonJS模块
100 | - ECMAScript Harmony模块
101 |
102 | ### 对象字面量表示法
103 |
104 | ``` javascript
105 | let person = {
106 | name: '',
107 | age: 0,
108 | getName: function() {},
109 | getAge: function() {}
110 | }
111 | ```
112 |
113 | ### Module模式
114 |
115 | Module模式可以为类提供私有和公有的方法,Module模式会封装一个作用域,从而屏蔽来自全局作用域的特殊部分,使一个单独的对象拥有公有/私有的方法和变量。
116 |
117 | Module模式使用闭包封装私有状态和组织,提供一种包装混合公有/私有方法和变量的方式,防止其泄露至全局作用域,防止与全局作用域中的方法和变量发生冲突。通过该模式只需返回一个公有API,而其他的一切则都维持在私有闭包里。
118 |
119 | Module模式可以屏蔽处理底层事件逻辑,只暴露供应用程序调用的公有API,该模式返回的是一个对象而不是函数。
120 |
121 | ``` javascript
122 | // 一个立即执行的匿名函数创建了一个作用域
123 | // 全局作用域无法获取私有变量_counter
124 | var moduleMode = (function() {
125 | // 私有变量
126 | var _counter = 0
127 |
128 | // 返回一个公有对象
129 | return {
130 | // 公有API
131 | increment: function() {
132 | return ++_counter
133 | },
134 | // 公有API
135 | reset: function() {
136 | _counter = 0
137 | return _counter
138 | }
139 | }
140 | })()
141 |
142 | let counter = moduleMode.increment()
143 | console.log(counter)
144 | counter = moduleMode.increment()
145 | console.log(counter)
146 | counter = moduleMode.reset()
147 | console.log(counter)
148 | // _counter is not defined
149 | console.log(_counter)
150 | ```
151 |
152 | > Module模式的本质是使用函数作用域来模拟私有变量,在模式内,由于闭包的存在,声明的变量和方法旨在改模式内部可用,但在返回对象上定义的变量和方法,则对外部使用者可用。
153 |
154 | Module模式也可用于命名空间
155 |
156 | ``` javascript
157 | var Namespace = (function() {
158 | // 私有变量
159 | var _counter = 0
160 |
161 | // 私有方法
162 | var _sayCounter = function() {
163 | console.log(_counter)
164 | }
165 |
166 | // 返回一个公有对象
167 | return {
168 | // 公有变量
169 | counter: 10,
170 |
171 | // 公有API
172 | increment: function() {
173 | ++_counter
174 | // 调用私有变量
175 | _sayCounter()
176 | return _counter
177 | },
178 | // 公有API
179 | reset: function() {
180 | _counter = 0
181 | _sayCounter()
182 | return _counter
183 | }
184 | }
185 | })()
186 |
187 | Namespace.increment()
188 | Namespace.increment()
189 | Namespace.reset()
190 | ```
191 |
192 | ### Module模式的变化
193 |
194 | #### 引入
195 |
196 | 可以使jQuery、Underscore作为参数引入模块
197 |
198 | ``` javascript
199 | var moduleMode = (function($, _) {
200 | function _method() {
201 | $('.container').html('test')
202 | }
203 |
204 | return {
205 | method: function() {
206 | _method()
207 | }
208 | }
209 | })($,_)
210 |
211 | moduleMode.method()
212 | ```
213 |
214 | #### 引出
215 |
216 | ``` javascript
217 |
218 | let moduleMode = (function() {
219 | var public = {},
220 | _private = 'hello'
221 |
222 | function _method() {
223 | console.log(_private)
224 | }
225 |
226 | public.name = 'public'
227 |
228 | public.method = function () {
229 | _method()
230 | }
231 |
232 | return public
233 | })()
234 |
235 | console.log(moduleMode.name)
236 | ```
237 |
238 | ### Module模式的优缺点
239 |
240 | - 优点:整洁、支持私有数据。
241 | - 缺陷:私有数据难以维护(想改变可见性需要修改每一个使用该私有数据的地方),无法为私有成员创建自动化单元测试,开发人员无法轻易扩展私有方法。
242 |
243 | ## Singleton(单例/单体)模式
244 |
245 | 单例模式作为一个静态的实例实现时,可以延迟创建实例,从而在没有使用该静态实例之前,无需占用浏览器的资源或内存。同时当在系统中确实需要一个对象来协调其他对象时,Singleton模式非常有用。单例模式可以推迟初始化,通常是因为它需要一些信息,这些信息在初始化期间可能无法获得。
246 |
247 | ``` javascript
248 | var Singleton = (function() {
249 | function Single(options) {
250 | options = options || {}
251 | this.name = options.name || 'Single'
252 | this.age = options.age || 1
253 | }
254 |
255 | var _instance
256 |
257 | // 返回一个闭包,_instance不会被销毁
258 | return {
259 | name: 'Singleton',
260 | getInstance: function(options) {
261 | if(!_instance) {
262 | _instance = new Single(options)
263 | }
264 | return _instance
265 | }
266 | }
267 | })()
268 |
269 |
270 | var single = Singleton.getInstance({
271 | name: 'ziyi2',
272 | age: 28
273 | })
274 |
275 | // Single {name: "ziyi2", age: 28}
276 | console.log(single)
277 | ```
278 |
279 | ## Observer(观察者)模式
280 |
281 | 观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变更自动通知给这一系列观察者对象。当subject目标对象需要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。
282 | > 一个或多个观察者对目标对象的状态感兴趣时,可以将自己依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每个观察者的更新方法。如果观察者对目标对象的状态不感兴趣,也可以将自己从中分离。
283 |
284 |
285 | | 对象 | 描述 |
286 | | :--------| :------ |
287 | | Subject(目标) | 维护一系列的观察者,方便添加、删除和通知观察者 |
288 | | Observer(观察者) | 为那些目标状态发生改变时需要通知的对象提供一个更新接口 |
289 | | subject(目标)实例对象 | 状态发生变化时通知观察者实例对象们更新状态 |
290 | |observer(观察者)实例对象 | 实现更新接口用于更新状态 |
291 |
292 |
293 | 
294 |
295 |
296 |
297 | ### 观察者列表对象
298 |
299 | 观察者列表对象用于维护一系列的观察者实例对象
300 |
301 | ``` javascript
302 | // 观察者列表对象
303 | function ObserverList() {
304 | this.observerList = []
305 | }
306 |
307 | // 增加观察者实例对象
308 | ObserverList.prototype.add = function(observer) {
309 | return this.observerList.push(observer)
310 | }
311 |
312 | // 查看观察者实例对象的数量
313 | ObserverList.prototype.getCount = function() {
314 | return this.observerList.length
315 | }
316 |
317 | // 获取某个观察者实例对象
318 | ObserverList.prototype.get = function(index) {
319 | if(index < -1 || index > this.observerList.length) return
320 | return this.observerList[index]
321 | }
322 |
323 | // 删改观察者实例对象的列表 省略...
324 | ```
325 |
326 | ### Subject(目标)对象
327 |
328 | 使用观察者列表对象维护观察者实例对象,并可以通知观察者实例对象更新状态
329 |
330 | ``` javascript
331 | // 目标对象(在观察者列表上增、删观察者实例对象)
332 | function Subject() {
333 | this.observers = new ObserverList()
334 | }
335 |
336 | // 增加观察者实例对象
337 | Subject.prototype.add = function(observer) {
338 | this.observers.add(observer)
339 | }
340 |
341 | // 通知观察者列表更新
342 | Subject.prototype.notify = function(context) {
343 | var count = this.observers.getCount()
344 | for(var i=0; i 观察者实例对象或目标实例对象
367 | // extension -> 需要被扩展的对象
368 | function extend(obj, extension) {
369 | for(var key in obj) {
370 | extension[key] = obj[key]
371 | }
372 | }
373 | ```
374 |
375 | ### 具体的DOM元素用于创建观察者实例对象和目标实例对象
376 |
377 | 创建DOM元素,用于扩展观察者实例对象和目标实例对象
378 |
379 | ``` html
380 |
381 |
382 |
383 |
384 |
385 | ```
386 |
387 | 创建目标实例对象
388 |
389 | ``` javascript
390 | // 获取checkbox元素
391 | var checkbox = document.getElementById('checkbox')
392 |
393 | // 创建具体目标实例,并绑定到checkbox元素上
394 | extend(new Subject(), checkbox)
395 |
396 | // 点击checkbox会触发目标实例对象的通知方法
397 | // 广播到所有观察者实例对象促使它们调用更新状态方法
398 | checkbox.onclick = function() {
399 | checkbox.notify(checkbox.checked)
400 | }
401 | ```
402 |
403 | 创建观察者实例对象
404 |
405 | ``` javascript
406 | // 获取btn和div元素
407 | var btn = document.getElementById('btn'),
408 | div = document.getElementById('div')
409 |
410 | btn.onclick = handlerClick
411 |
412 | function handlerClick() {
413 | // 创建checkbox元素(注意和目标实例对象不同)
414 | var input = document.createElement("input")
415 | input.type = "checkbox"
416 |
417 | // 创建具体的观察者实例,并绑定到checkbox元素上
418 | extend(new Observer(), input)
419 |
420 | // 重写观察者更新行为
421 | input.update = function(value) {
422 | this.checked = value
423 | }
424 |
425 | // 通过目标实例对象新增需要被广播的观察者实例对象
426 | checkbox.add(input)
427 |
428 | // 将观察者附到div元素上
429 | div.appendChild(input)
430 | }
431 | ```
432 |
433 | > 至此,通过按钮新增观察者实例对象,点击目标checkbox实例对象时,checkbox的状态会广播给所有新增的观察者实例对象checkbox,从而使目标实例对象的值和观察者实例对象的值保持一致,实现了观察者模式。
434 |
435 |
436 | ## Publish/Subscribe(发布/订阅)模式
437 |
438 |
439 | 
440 |
441 |
442 | 需要注意token是每一次订阅的唯一标识,通过token可以取消特定的频道订阅。
443 |
444 |
445 |
446 | ``` javascript
447 |
448 | // 发布/订阅模式
449 | var pubsub = (function() {
450 |
451 | // 订阅和发布的事件频道集(桥梁、中间带)
452 | var _channels = [],
453 | _subUid = -1
454 |
455 | return {
456 | // 订阅频道
457 | subscribe: function(channel, handler) {
458 | if(!_channels[channel]) _channels[channel] = []
459 | var token = (++_subUid).toString()
460 | _channels[channel].push({
461 | token: token,
462 | handler: handler
463 | })
464 | return token
465 | },
466 |
467 | // 广播频道
468 | publish: function(channel, data) {
469 | if(!_channels[channel]) return false
470 | // 获取频道订阅者
471 | var subscribers = _channels[channel]
472 | var len = subscribers.length
473 | // 后订阅先触发
474 | while(len--) {
475 | subscribers[len].handler(data, channel, subscribers[len].token)
476 | }
477 | return this
478 | },
479 |
480 | // 移除订阅
481 | unsubscribe: function(token) {
482 | for(var channel in _channels) {
483 | var len = _channels[channel].length
484 | for(var index=0; index 通过外观模式对接口的二次封装隐藏其复杂性,可以简化用户的使用,外观模式也可以结合Module(模块)模式使用,需要注意的是外观模式会产生隐性成本,在设计时需要衡量是否需要使用外观模式抽象和封装某些结构。
756 |
757 |
758 | ## Factory(工厂)模式
759 |
760 | ### 简单工厂模式
761 |
762 | 简单工厂模式(静态工厂方法)主要用于创建同一类对象。该模式通过创建一个新对象然后包装增强其属性和功能实现,需要注意的是使用该模式它们的方法不能共享,不能像原型继承那样共享原型方法。
763 |
764 | ``` javascript
765 | function createPerson(type, name) {
766 | var person
767 | var person = new Object()
768 | person.name = name
769 | person.getName = function() {
770 | return this.name
771 | }
772 | return person
773 | }
774 |
775 | let person1 = createPerson('father', 'ziyi1')
776 | let person2 = createPerson('children', 'ziyi2')
777 | ```
778 |
779 | 也可以增强简单工厂模式, 使其可以创建不同类的对象
780 |
781 | ``` javascript
782 | function createPerson(type, name) {
783 |
784 | var person
785 |
786 | switch(type) {
787 | case 'father':
788 | // 父亲差异部分
789 | // 例如 person = new Father(name)
790 | break
791 | case 'mother':
792 | // 母亲差异部分
793 | // 例如 person = new Mother(name)
794 | break
795 | case 'children':
796 | // 孩子差异部分
797 | // 例如 person = new Children(name)
798 | break
799 | default:
800 | break
801 | }
802 | return person
803 | }
804 | ```
805 |
806 | ### 工厂方法模式
807 |
808 | 简单工厂模式创建多类对象相对不够灵活,工厂方法模式可以轻松创建多个类的实例对象
809 |
810 | ``` javascript
811 | function Person(type, name, age, job) {
812 | // 安全模式,外部可以不使用new关键字
813 | if(this instanceof Person) {
814 | return new this[type](name, age, job)
815 | } else {
816 | return new Person(type, name, age, job)
817 | }
818 | }
819 |
820 | Person.prototype = {
821 | constructor: Person,
822 | // Father类
823 | Father: function(name, age, job) {
824 | this.name = name
825 | this.age = age
826 | this.job = job
827 | console.log(`Father: ${name},${age},${job}`)
828 | },
829 |
830 | // Mother类
831 | Mother: function(name, age, job) {
832 | this.name = name
833 | this.age = age
834 | this.job = job
835 | console.log(`Mother: ${name},${age},${job}`)
836 | },
837 |
838 | // Children类
839 | Children: function(name, age, job) {
840 | this.name = name
841 | this.age = age
842 | this.job = job
843 | console.log(`Children: ${name},${age},${job}`)
844 | }
845 | }
846 |
847 |
848 | let data = [{
849 | type: 'Father',
850 | name: 'ziyi2',
851 | age: 44,
852 | job: 'web'
853 | }, {
854 | type: 'Mother',
855 | name: 'ziyi2',
856 | age: 280,
857 | job: 'web'
858 | }, {
859 | type: 'Children',
860 | name: 'ziyi2',
861 | job: 'web'
862 | }]
863 |
864 | for(let person of data) {
865 | Person(person.type, person.name, person.age, person.job)
866 | }
867 | ```
868 |
869 | > 在项目中通过对产品类的抽象使其创建业务主要负责用于创建多类产品的实例。
870 |
871 | ### 抽象工厂模式
872 |
873 |
874 | 抽象类是一种声明但是不能使用的类,JavaScript中有一个保留的关键字abstract对应抽象概念,抽象类在实例化时应该抛出错误。抽象类主要用于定义产品簇,并声明一些必备的方法,如果子类没有重写这些方法,那么子类实例化后调用这些方法时应该抛出错误。
875 |
876 | 抽象类中定义的方法只是显性的定义一些功能,但没有具体的实现,不能使用抽象类创建真实的使用实例对象。
877 |
878 | ``` javascript
879 |
880 | function objectCreate(o) {
881 | function F() {}
882 | F.prototype = o
883 | return new F()
884 | }
885 |
886 | /**
887 | * @Author: zhuxiankang
888 | * @Date: 2018-06-14 09:17:50
889 | * @Desc: 抽象工厂方法
890 | * @Parm: subClass -> 子类
891 | * abstractClass -> 抽象类
892 | */
893 | function AbstractFactory (subClass, abstractClass) {
894 | if(typeof AbstractFactory[abstractClass] == 'function') {
895 | // 注意和寄生组合式继承中var prototype = objectCreate(AbstractFactory[abstractClass].prototype)的区别
896 | // 这里是不仅继承了抽象类的原型对象的方法
897 | // 还继承了抽象类的实例对象的方法和属性
898 | // 继承构造函数中没有继承抽象类的实例对象的方法和属性
899 | var prototype = objectCreate(new AbstractFactory[abstractClass]())
900 | prototype.constructor = subClass
901 | subClass.prototype = prototype
902 | } else {
903 | throw new Error(abstractClass + ' undefined!')
904 | }
905 | }
906 |
907 | // Person抽象类
908 | AbstractFactory.Person = function() {
909 | // 实例属性会被子类继承
910 | this.type = 'person'
911 | }
912 |
913 | // Person抽象类声明的抽象方法(抽象方法不能被Person抽象类的实例使用)
914 | AbstractFactory.Person.prototype = {
915 | // constructor: AbstractFactory.Person,
916 |
917 | getType: function() {
918 | return new Error('abstract method "getType" can not be called!')
919 | },
920 |
921 | getName: function() {
922 | return new Error('abstract method "getName" can not be called!')
923 | }
924 | }
925 |
926 | // 创建其他抽象类
927 | // AbstractFactory.?.prototype
928 |
929 |
930 | // 创建继承抽象类的子类
931 | function Father(name) {
932 | this.name = name
933 | }
934 |
935 |
936 | function Mother(name) {
937 | this.name = name
938 | }
939 |
940 | // 抽象工厂方法实现对抽象类的继承
941 | AbstractFactory(Father, 'Person')
942 | AbstractFactory(Mother, 'Person')
943 |
944 |
945 | // 需要注意放在AbstractFactory工厂方法之后
946 | // 因为工厂方法里重写了子类的prototype原型对象
947 | Mother.prototype.getType = function() {
948 | return this.type
949 | }
950 |
951 |
952 | var father = new Father('ziyi2')
953 | // person
954 | console.log(father.type)
955 | // Error: abstract method "getType" can not be called!
956 | console.log(father.getType())
957 |
958 | var mother = new Mother('ziyi2')
959 | // person 根据原型链,子类的该方法重写了从父类继承的方法
960 | console.log(mother.getType())
961 | ```
962 |
963 |
964 | > 关于寄生组合式继承请查看[js类和继承](https://ziyi2.github.io/2018/06/05/js%E7%B1%BB%E5%92%8C%E7%BB%A7%E6%89%BF.html#more)。抽象工厂模式中的抽象类创建的不是一个真实的对象实例,而是一个类簇,抽象类指定了类的结构,区别于简单工厂模式创建单一对象,工厂方法模式创建多类对象。不过这种模式应用的并不广泛,因为JavaScript中不支持抽象化创建于虚拟方法。
965 |
966 |
967 | ## Mixin(混入)模式
968 |
969 | Mixin是可以轻松被一个子类或一组子类轻松继承功能的类,目的是函数复用。
970 |
971 | ### 继承Mixin
972 |
973 | 在JavaScript中,可以使用原型链轻松实现继承Mixin,具体可参考[寄生组合式继承](https://ziyi2.github.io/2018/06/05/js%E7%B1%BB%E5%92%8C%E7%BB%A7%E6%89%BF.html#more),Mixin类的方法和属性可以轻松被子类继承。
974 |
975 | ### Mixin(混入)
976 |
977 | Mixin允许对象通过较低的复杂性借用(继承)功能。继承Mixin可以实现父类的属性和方法被多个子类继承,但是在复杂的业务场景中可能存在一个子类需要继承多个父类的情况。
978 |
979 | Mixin对象可以被视为具有可以在很多其他对象原型中轻松共享属性和方法的对象。
980 |
981 |
982 | ``` javascript
983 |
984 |
985 | // mixin 混入对象
986 | // extend 被混入的对象
987 | var mixins = function(extend, mixin) {
988 | // 指定特定的混入属性
989 | if(arguments[2]) {
990 | for(var i=2,len=arguments.length; i Mixin有助于减少系统中的重复功能及增加函数复用,但是也存在一些缺陷,将Mixin导入对象原型会导致函数起源方面的不确定性以及原型污染。
1051 |
1052 |
1053 | ## 装饰者模式
1054 |
1055 | 装饰者模式旨在促进代码复用,提供了将行为添加至系统现有的类的功能,相对于类原有的基本功能来说不是必要的,如果是必要的,可以被合并到父类。装饰者在不改变原有对象的基本功能的基础上,对其功能进行扩展。
1056 |
1057 |
1058 | ```javascript
1059 | function MacBook() {
1060 | this.cost = function() {
1061 | return 997
1062 | }
1063 | this.screenSize = function() {
1064 | return 11.6
1065 | }
1066 | }
1067 |
1068 |
1069 | function Memory(macbook) {
1070 | var v = macbook.cost()
1071 | macbook.cost = function() {
1072 | return v + 75
1073 | }
1074 | }
1075 |
1076 | function Engraving(macbook) {
1077 | var v = macbook.cost()
1078 | macbook.cost = function() {
1079 | return v +200
1080 | }
1081 | }
1082 |
1083 |
1084 | var macbook = new MacBook()
1085 | Memory(macbook)
1086 | Engraving(macbook)
1087 | //1272
1088 | console.log(macbook.cost())
1089 | ```
1090 |
1091 | > 如果使用ES6语法,具体可以查看[ES6中的装饰者](http://es6.ruanyifeng.com/#docs/decorator)。装饰者模式如果管理不当,会极大的复杂化应用程序架构,因为向命名空间中引入了很多小型但类似的对象。
1092 |
1093 |
1094 |
1095 | ``` javascript
1096 | var decorator = function(dom, fn) {
1097 | if(typeof dom.onclick === 'function') {
1098 | // 获取原有的点击事件
1099 | var origin = dom.onclick
1100 | // 装饰dom的点击事件
1101 | dom.onclick = function(event) {
1102 | // 保留原有事件
1103 | origin.call(dom, event)
1104 | // 装饰新事件
1105 | fn.call(dom, event)
1106 | }
1107 | } else {
1108 | dom.onclick = fn()
1109 | }
1110 | }
1111 |
1112 |
1113 | var btn = document.getElementById('btn')
1114 | // 原有的点击事件
1115 | btn.onclick = function(event) {
1116 | console.log('origin btn click')
1117 | }
1118 |
1119 | function fn(event) {
1120 | console.log('decorator btn click')
1121 | }
1122 |
1123 | // 装饰btn
1124 | decorator(btn, fn)
1125 | ```
1126 |
1127 | > 按钮原有的点击事件功能不变,在此基础上对按钮的点击功能进行了装饰。
1128 |
1129 |
1130 |
1131 | ## Flyweight(享元)模式
1132 |
1133 |
1134 | ``` javascript
1135 | // 享元接口
1136 | var Flyweight = {
1137 | serverName: function() {},
1138 | getName: function() {}
1139 | }
1140 |
1141 | // 享元具体实现
1142 | function ConcreteFlyweight(newName) {
1143 | var _name = newName
1144 |
1145 | // 如果已经为某一功能定义接口,则实现该功能
1146 | if(typeof this.getName === "function") {
1147 | this.getName = function() {
1148 | return _name
1149 | }
1150 | }
1151 |
1152 | if(typeof this.serverName === "function") {
1153 | this.serverName = function(context) {
1154 | console.log('Server name ' + _name + ' to table number ' + context.getTable())
1155 | }
1156 | }
1157 | }
1158 |
1159 |
1160 | // 实现接口方法
1161 | Function.prototype.implementsFor = function(Interface) {
1162 | if(Interface.constructor === Function) {
1163 | this.prototype = new Interface()
1164 | this.prototype.parent = Interface.prototype
1165 | } else {
1166 | this.prototype = Interface
1167 | this.prototype.parent = Interface
1168 | }
1169 | this.prototype.constructor = this
1170 | }
1171 |
1172 | // 实现接口
1173 | ConcreteFlyweight.implementsFor(Flyweight)
1174 |
1175 |
1176 | function TableContext(tableNumber) {
1177 | return {
1178 | getTable: function() {
1179 | return tableNumber
1180 | }
1181 | }
1182 | }
1183 |
1184 |
1185 | // 享元工厂
1186 | function FlyweightFactory() {
1187 | var _names = []
1188 |
1189 | return {
1190 | getName: function(name) {
1191 | _name = new ConcreteFlyweight(name)
1192 | _names.push(_name)
1193 | return _name
1194 | },
1195 |
1196 | getTotal: function() {
1197 | return _names.length
1198 | }
1199 | }
1200 | }
1201 |
1202 |
1203 | // 示例
1204 | var names = new ConcreteFlyweight()
1205 | var tables = new TableContext()
1206 |
1207 | var orders = 0
1208 | var flyweightFactory
1209 | function takeOrder(name, table) {
1210 | names[orders] = flyweightFactory.getName(name)
1211 | tables[orders ++] = new TableContext(table)
1212 | }
1213 |
1214 | flyweightFactory = new FlyweightFactory()
1215 |
1216 | takeOrder('ziyi2', 1)
1217 | takeOrder('ziyi2', 2)
1218 |
1219 | takeOrder('ply', 1)
1220 | takeOrder('ply', 4)
1221 |
1222 | for(var i=0; i 创建的对象更复杂,是一个复合对象。
1526 |
1527 |
1528 |
1529 | ## 适配器模式
1530 |
1531 | 将一个类(对象)的接口(方法或属性)转化成另外一个接口,从而满足用户的需求。
1532 |
1533 | 例如将之前的外观模式适配jQuery库
1534 |
1535 | ``` javascript
1536 | var Browser = {
1537 | event: {
1538 | add: function(dom, type, fn) {
1539 | $(dom).on(type, fn)
1540 | },
1541 |
1542 | remove: function(dom, type, fn) {
1543 | $(dom).off(type, fn)
1544 | }
1545 |
1546 | // ....
1547 | },
1548 |
1549 | id: function(dom, id) {
1550 | return $(id).get(0)
1551 | }
1552 | }
1553 | ```
1554 |
1555 | > 需要注意适配的时候为了做到兼容参数此时不能变。
1556 |
1557 |
1558 | 除了代码适配,还可以进行参数适配。当一个函数传入参数较多时,很难记住参数的顺序,因此可以通过传入对象来进行参数适配
1559 |
1560 | ``` javascript
1561 | function fn(name, age, job, desc) {}
1562 |
1563 | function adapter(obj) {
1564 | var _adapter = {
1565 | name: '',
1566 | age: 0,
1567 | job: 'web',
1568 | desc: ''
1569 | }
1570 |
1571 | // 适配传入的参数
1572 | for(var key in _adapter) {
1573 | _adapter[key] = obj[key] || _adapter[key]
1574 | }
1575 | }
1576 | ```
1577 |
1578 |
1579 |
1580 | ## MV*
1581 |
1582 | ### MVC
1583 |
1584 |
1585 |
1586 | MVC(模型-视图-控制器)是一种架构设计模式,通过关注点分离鼓励改进引用程序组织。它强制将业务数据(Model)、用户界面(View)隔离,并将控制器(Controller)用于管理逻辑和用户输入。在JavaScript中的MVC框架包括Backbone、Ember.js和AngularJS。
1587 |
1588 | 
1589 |
1590 |
1591 | MVC有利于简化应用程序功能的模块化。
1592 | - 整体维护更容易。
1593 | - 解耦Model和View。
1594 | - 消除Model和Controlle的代码重复。
1595 |
1596 |
1597 | #### Model
1598 |
1599 | 代表特定的数据,当Model改变时,会通过观察者模式发布信息。一般要求数据可持久化,保存在内存中、用户的localStorage数据存储中或者数据库中。Model主要和业务数据相关。
1600 |
1601 | #### View
1602 | 描绘Model当前状态,会通过观察者模式订阅Model更新或修改的信息,从而做出View的更新。一个View通常订阅一个Model,用户与View进行交互,包括读取和编辑Model。
1603 |
1604 | #### Controller
1605 | 处理用户交互,为View做决定,是Model和View的中介。当用户操作View时,通常用于更新Model。
1606 |
1607 |
1608 | #### 优点
1609 |
1610 | - MVC有利于简化应用程序功能的模块化。
1611 | - 解耦Model和View。
1612 | - 消除Model和Controlle的代码重复。
1613 |
1614 | #### 缺点
1615 | - Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,Controller业务逻辑的正确性是无法验证的:Controller更新Model的时候,无法对View的更新操作进行断言。
1616 | - View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的
1617 |
1618 |
1619 |
1620 | ### MVC Model 2
1621 |
1622 | 在Web服务端开发的时候也会接触到MVC模式,而这种MVC模式不能严格称为MVC模式。经典的MVC模式只是解决客户端图形界面应用程序的问题,而对服务端无效。服务端的MVC模式又自己特定的名字:MVC Model 2,或者叫JSP Model 2,或者直接就是Model 2 。Model 2客户端服务端的交互模式如下:
1623 |
1624 | 
1625 |
1626 |
1627 | 服务端接收到来自客户端的请求,服务端通过路由规则把这个请求交由给特定的Controller进行处理,Controller执行相应的业务逻辑,对数据库数据(Model)进行操作,然后用数据去渲染特定的模版,返回给客户端。
1628 |
1629 | 因为HTTP协议是单工协议并且是无状态的,服务器无法直接给客户端推送数据。除非客户端再次发起请求,否则服务器端的Model的变更就无法告知客户端。所以可以看到经典的Smalltalk-80 MVC中Model通过观察者模式告知View更新这一环被无情地打破,不能称为严格的MVC。
1630 |
1631 | Model 2模式最早在1998年应用在JSP应用程序当中,JSP Model 1应用管理的混乱诱发了JSP参考了客户端MVC模式,催生了Model 2。
1632 |
1633 |
1634 | 
1635 |
1636 | 后来这种模式几乎被应用在所有语言的Web开发框架当中。PHP的ThinkPHP,Python的Dijango、Flask,NodeJS的Express,Ruby的RoR,基本都采纳了这种模式。平常所讲的MVC基本是这种服务端的MVC。
1637 |
1638 |
1639 | ### MVP
1640 |
1641 | MVP(模型-视图-表示器)是MVC设计模式的一种衍生模式,专注于改进表示逻辑。MVP打破了View原来对于Model的依赖,其余的依赖关系和MVC模式一致。
1642 |
1643 |
1644 | 
1645 |
1646 |
1647 | 和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter同样的会执行相应的业务逻辑,并且对Model进行相应的操作;而这时候Model也是通过观察者模式把自己变更的消息传递出去,但是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面。
1648 |
1649 |
1650 | 对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。
1651 |
1652 |
1653 |
1654 | #### 优点
1655 |
1656 | - 便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter业务逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例。
1657 |
1658 | - View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务逻辑完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做高度可复用的View组件。
1659 |
1660 |
1661 | #### 缺点
1662 |
1663 | - Presenter中除了业务逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。
1664 |
1665 | ### MVVM
1666 |
1667 | MVVM可以看作是一种特殊的MVP(Passive View)模式,或者说是对MVP模式的一种改良。
1668 |
1669 | MVVM代表的是Model-View-ViewModel,这里需要解释一下什么是ViewModel。ViewModel的含义就是 "Model of View",视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。 在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型。还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是Domain Model所不包含的,但也是需要显示的信息。
1670 |
1671 | 可以简单把ViewModel理解为页面上所显示内容的数据抽象,和Domain Model不一样,ViewModel更适合用来描述View。
1672 |
1673 |
1674 | 
1675 |
1676 |
1677 | MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。以前全部由Presenter负责的View和Model之间数据同步操作交由给Binder处理。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。
1678 |
1679 |
1680 | 也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。
1681 |
1682 |
1683 | #### 优点
1684 |
1685 | - 提高可维护性。解决了MVP大量的手动View和Model同步的问题,提供双向绑定机制。提高了代码的可维护性。
1686 | - 简化测试。因为同步逻辑是交由Binder做的,View跟着Model同时变更,所以只需要保证Model的正确性,View就正确。大大减少了对View同步更新的测试。
1687 |
1688 |
1689 | #### 缺点
1690 |
1691 | - 过于简单的图形界面不适用,或说牛刀杀鸡。
1692 | - 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。
1693 | - 数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。
1694 |
--------------------------------------------------------------------------------