├── 1. 基础知识 ├── 1.1 动态类型语言和鸭子类型.md ├── 1.2 多态.md ├── 1.3 封装.md ├── 1.4 prototype.md ├── 2.1 this.md ├── 2.2 call和apply.md ├── 3.1 闭包.md └── 3.2 高阶函数.md ├── 2. 设计模式 ├── 4.0 单例模式.md ├── 4.1 实现单例模式.md ├── 4.2 透明的单例模式.md ├── 4.3 用代理实现单例模式.md ├── 4.4 JavaScript中的单例模式.md ├── 4.6 惰性单例.md ├── 5.0 策略模式.md ├── 5.1 使用策略模式计算奖金.md ├── 5.2 JavaScript版本的策略模式.md ├── 5.3 策略模式的思考.md ├── 6.0 代理模式.md ├── 6.1 图片预加载.md ├── 6.2 代理的意义.md ├── 7.0 迭代器模式.md ├── 7.1 内部迭代器和外部迭代器.md └── 7.2 其它迭代器.md └── README.md /1. 基础知识/1.1 动态类型语言和鸭子类型.md: -------------------------------------------------------------------------------- 1 | # 动态类型语言和鸭子类型 2 | 3 | 编程语言按照*数据类型*可以分为两类 4 | 5 | 1. 静态类型语言 6 | 2. 动态类型语言 7 | 8 | 静态类型语言载入内存执行之前需要编译为可执行的二进制文件,在编译时段可以确定类型;动态类型语言执行之前不需要编译,一边解释一边执行,变量类型需要在程序运行时变量被赋值后才能确定。 9 | 10 | 动态类型语言并不是没有数据类型,而是数据类型直到变量被赋值后才能确定,被赋值的数据类型就是变量的类型。 11 | 12 | ## 优缺点 13 | 14 | ### 静态类型语言优点 15 | 16 | 1. 编译时就能发现类型不匹配错误,编译器可以帮我们在程序运行之前发现一些错误 17 | 18 | 2. 在程序中明确规定了数据类型,编译器可以针对这些信息做一些优化工作,提高程序运行速度 19 | 20 | ### 静态类型语言缺点 21 | 22 | 1. 迫使程序员依照强契约来编写程序,为每个变量规定数据类型归根到底只是辅助我们编写可靠性高程序的手段,而不是编写程序目的 23 | 24 | 2. 类型的声明会增加代码量和逻辑复杂度,会使程序员精力从业务逻辑上分散 25 | 26 | 27 | ### 动态类型语言优点 28 | 29 | 1. 编写代码数量少,看起来更简洁,程序员精力集中在业务逻辑 30 | 31 | 2. 没有类型约束,使用灵活 32 | 33 | ### 动态类型语言缺点 34 | 35 | 在程序运行之前无法保证变量的类型,从而在程序运行期间有可能发生和类型相关的错误。就好像在商店买了一包没有说明配料的牛肉辣条,只有吃到嘴里才知道是不是牛肉味 36 | 37 | 38 | ## duck typing 39 | 40 | 在JavaScript中当我们对一个变量赋值时,显然不需要考虑类型问题,JavaScript是一门典型的动态类型语言 41 | 42 | 动态类型语言对变量的宽裕给实际编码带来很大的灵活性。由于无需检测代码类型,我们可以尝试调用任何对象的任何方法,而无需考虑它原本是否被设计拥有该方法。 43 | 44 | **面向接口编程**是设计模式的重要思想,但在JavaScript中由于弱类型的缘故,没有办法通过类型来约束对象行为,ES6之前甚至没有`Class`的概念。因此在JavaScript中实现设计模式的过程与我们熟悉的静态类型预览中的实现过程有很大差别。 45 | 46 | 首先要解决的就是类型识别问题,在静态类型语言中我们可以通过`interface`或者`class`来判读一个对象**IS-A** 某种type,而在JavaScript中我们更关注`HAS-A`,也就是关注对象的行为,而不是关注对象本身,这就是经典的 duck typing 47 | 48 | > duck typing is a style of dynamic typing in which an object's current set of methods and properties determines the valid semantics, rather than its inheritance from a particular class or implementation of a specific interface. 49 | 50 | duck typing 的通俗说法是:当它走起路来像鸭子,叫起来也是鸭子,那么就认为他是鸭子 51 | 52 | 53 | 有个小故事帮助我们理解duck typing 54 | 55 | 从前在JavaScript王国里,有一个国王,他认为世界上最美妙的声音就是鸭子的叫声,于是国王召集大臣,要组建一个1000只鸭子的合唱团。大臣们找遍了全国,终于找到了999只鸭子了,但是始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫声和鸭子一模一样,于是这只鸡成了合唱团的最后一员 56 | 57 | 这个故事告诉我们,国王要听的只是鸭子的叫声,这个声音的主人是鸡还是鸭并不重要。 58 | 59 | var duck = { 60 | name: 'Frank', 61 | 62 | sing: function(){ 63 | console.log('sing'); 64 | } 65 | }; 66 | 67 | var chicken = { 68 | name: 'Jack', 69 | 70 | sing: function(){ 71 | console.log('sing'); 72 | } 73 | }; 74 | 75 | var choir = []; 76 | 77 | function joinChoir(animal){ 78 | if(typeof animal.sing === 'function'){ 79 | choir.push(animal); 80 | console.log('Choir length: ' + choir.length); 81 | } 82 | } 83 | 84 | joinChoir(duck); 85 | joinChoir(chicken); 86 | 87 | 我们可以看到,对于加入合唱团的动物,大臣无需检测它的类型,只需要保证它们拥有`sing`方法。在程序中对应的理解就是,当我们在某处需要某种类型的对象时,我们并不关心供消费的对象本身类型,实际在JavaScript中也没有类型,我们关系的时这个对象有没有我们需要的属性和方法,有的话,它就是我们需要的“鸭子”! 88 | 89 | 在静态类型语言中,要实现**面向接口编程**并不容易,往往需要借助抽象类或者这接口等将对象*向上转型*。当对象的真正类型被隐藏在起背后的它的超类型身后,这些对象才能被类型系统识别、相互替换使用,这样才能体现出对象多态的价值。 90 | 91 | 在动态语言的面向对象设计过程中,duck typing的概念至关重要,我们可以不必借助超类的帮助,轻松的在动态类型语言中实现一个原则:**面向接口编程**,而不是**面向实现编程**。 92 | 93 | 例如,一个对象如果正确实现了`pop`和`push`方法,我们可以不在乎起类型,就当做栈来使用 94 | -------------------------------------------------------------------------------- /1. 基础知识/1.2 多态.md: -------------------------------------------------------------------------------- 1 | # 多态 2 | 3 | **多态**一词来源于希腊文*polymorphism*,拆开来看是*poly*(复数) + *morph*(形态) + *ism*,从字面上可以理解为复数形态。 4 | 5 | 多态的实际含义是:同一个操作作用于不同的对象,可以产生不同的解释和执行结果。换句话说,给不同的对象发送同一条消息的时候,这些对象会根据这个消息分别给出不同的反馈。 6 | 7 | 举个例子:主人家亮了两只动物,分别是一只鸭和一只鸡,当主人发出“**叫**”的命令时,鸭会*呱呱呱*地叫,而鸡会*咯咯咯*地叫。它们同样是动物,并且可以发出叫声,但根据“*叫*”指令,发出的声音并不相同。 8 | 9 | 这其中蕴含了多态的思想 10 | 11 | ## 一段多态的Javascript代码 12 | 13 | 把上面故事用JavaScript代码实现一下 14 | 15 | function makeAnimalSound(animal){ 16 | if(animal instanceof Duck){ 17 | console.log('呱呱呱'); 18 | }else if(animal instanceof Chicken){ 19 | console.log('咯咯咯'); 20 | } 21 | } 22 | 23 | function Duck(){ 24 | 25 | } 26 | 27 | fuction Chicken(){ 28 | 29 | } 30 | 31 | makeAnimalSound(new Duck()); 32 | makeAnimalSound(new Chicken()); 33 | 34 | 这段代码体现了**多态性**,但是这样的多态性有很多问题,如果增加一条狗,狗的叫声是*汪汪汪*,这时候我们需要修改`makeAnimalSound`函数,但修改代码是危险地,修改的地方越多,程序越可能出错,而且动物种类增多的时候,这就可能变成一个巨大的函数。这也是`开放—封闭原则`要解决的一个重要问题。 35 | 36 | 多态背后的思想将**做什么**和**谁去做以及怎么做分离开**,也就是讲**不变的事物**和**可能改变的事物**分离。 37 | 38 | 在这个场景下,动物都会叫是不会变化的,但不同类型的动物的怎么叫是变化的。 39 | 40 | 把不变的部分隔离出来,把可变的部分封装起来,这给予我们拓展程序的能力,程序看起来是可生长的,也是符合`开放-封闭原则`的,相对于修改代码,仅仅增加代码就能完成相同的功能,这显然要优雅和安全很多 41 | 42 | ## 对象的多态性 43 | 44 | function Duck(){} 45 | 46 | Duck.prototype.makeSound = function(){ 47 | console.log('呱呱呱'); 48 | }; 49 | 50 | function Chicken(){} 51 | 52 | Chicken.prototype.makeSound = function(){ 53 | console.log('咯咯咯'); 54 | }; 55 | 56 | function makeAnimalSound (animal){ 57 | animal.makeSound(); 58 | } 59 | 60 | makeAnimalSound(new Duck()); 61 | makeAnimalSound(new Chicken()); 62 | 63 | 这段代码和上面代码功能相同,鸭和鸡街道指令后发出不同叫声,这时候再要求添加狗,只要简单地追加一些代码就可以了,而不用修改以前的代码 64 | 65 | function Dog(){} 66 | 67 | Dog.prototype.makeSound = function(){ 68 | console.log('汪汪汪'); 69 | }; 70 | 71 | makeAnimalSound(new Dog()); 72 | 73 | ## 类型检查和多态 74 | 75 | 类型检查是多态不得不说的一个话题,但是JavaScript没有必要进行类型检查,为了了解多态的真正目的,把上面的代码翻译为静态类型语言Java 76 | 77 | 静态类型语言在编译的时候要进行严格的类型检查,所以不能给变量赋予不同类型的值,这种类型检查会让代码变僵硬 78 | 79 | public class Duck{ 80 | public void makeSound(){ 81 | System.out.print('呱呱呱'); 82 | } 83 | } 84 | 85 | public class Chicken{ 86 | public void makeSound(){ 87 | System.out.print('咯咯咯'); 88 | } 89 | } 90 | 91 | public class AnimalSound{ 92 | public void makeSound(Duck duck){ 93 | duck.makeSound(); 94 | } 95 | } 96 | 97 | public class Test{ 98 | public static void main(){ 99 | AnimalSund animalSound = new AnimalSound(); 100 | Duck duck = new Duck(); 101 | animalSound.makeSound(duck); 102 | } 103 | } 104 | 105 | 这样就能让鸭子叫了,但是如果也想让鸡叫是件麻烦的事情,因为**AnimalSound**类的`makeSound`方法只接受`Duck`类型的参数,我们传入`Chicken`类型的实例在编译阶段类型检查的时候会报错 106 | 107 | 某些时候在享受静态类型语言类型检查的安全性的时候,我们也会感受到被束缚住了手脚,为了解决这个问题,静态语言通常被设计为可以`向上转型`:当给iygelei变量赋值的时候,这个变量的类型既可以是这个类本身,也可以使用这个类的超类。 108 | 109 | 这就像我们描述*天上有只麻雀在飞*的时候,也可以说成*天上有只鸟*在飞,同理,当我们设计一个类`Animal`座位*Duck*和*CHicken*的超类的时候,我们可以让**AnimalSound**类的`makeSound`方法接受`Animal`类型的参数,这是让对象表现出多态性的必经之路,而多态性的表现正是实现众多设计模式的目标 110 | 111 | ## 使用继承得到多态效果 112 | 113 | 使用继承得到多态效果是让对象表现出多态性的最常用手段。继承通常包括 114 | 115 | 1. 实现继承 116 | 117 | 2. 接口继承 118 | 119 | 下面的例子使用实现继承,先创建一个`Animal`抽象类,分别让`Duck`和`Chicken`继承 120 | 121 | public abstract class Animal{ 122 | abstract void makeSound(); //抽象方法,作为子类类的约束 123 | } 124 | 125 | 126 | 127 | public class Duck extends Animal{ 128 | public void makeSound(){ 129 | System.out.print('呱呱呱'); 130 | } 131 | } 132 | 133 | public class Chicken extends Animal{ 134 | public void makeSound(){ 135 | System.out.print('咯咯咯'); 136 | } 137 | } 138 | 139 | *AnimalSound*的`makeSound`方法参数类型改为`Animal`,而不是具体的`Duck`或者`Chicken` 140 | 141 | public class AnimalSound{ 142 | public void makeSound(Animal animal){ 143 | animal.makeSound(); 144 | } 145 | } 146 | 147 | public class Test{ 148 | public static void main(){ 149 | AnimalSund animalSound = new AnimalSound(); 150 | 151 | Duck duck = new Duck(); 152 | animalSound.makeSound(duck); 153 | 154 | Chicken chicken = new Chicken(); 155 | animalSound.makeSound(chicken); 156 | } 157 | } 158 | 159 | ## JavaScript的多态性 160 | 161 | 从前面内容可以看出多态的思想实际是把*做什么*和*谁去做*分离开,要实现这一点,归根结底先要消除类型之间的耦合关系。如果类型之间的耦合关系没有消除,那么我们在`makeSound`方法中指定了发出叫声的对象时某个类型,它就不能再被替换为另外一个类型,在Java中通过`向上转型`来制定一个模糊地类型实现多态。 162 | 163 | 而JavaScript的变量类型在运行期是可变的。一个JavaScript对象,既可以表示`Duck`类型的对象,也可以表示`Chicken`类型的对象,这意味着JavaScript对象的多态性是与生俱有的 164 | 165 | 这也很好理解,JavaScript是一门动态类型语言,在编译时没有类型检查过程,既没有检查创建的对象的类型,也没有检查传递参数的类型,在前面JavaScript的例子中,我们既可以传入`duck`对象,也可以传入`chicken`对象做参数 166 | 167 | 由此可见,在JavaScript中某一种动物能够否发出叫声,只取决于它有没有`makeSound`方法,而不取决于它是否是某种类型的对象。这里不存在任何程度的类型耦合,这正是duck type中关注的`HAS-A`而不是`IS-A`,不需要通过向上转型来取得多态效果 168 | 169 | ## 多态在面向对象程序设计中的作用 170 | 171 | Martin Fowler在《重构:改善既有代码的设计》中写道: 172 | 173 | > 多态的最根本好处在于,你不必再想对象询问“你是什么类型”,而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其它的一切多态机制会为你安排妥当。 174 | 175 | 换句话说,堕胎的最根本好处就是通过把过程的条件分支语句转换为对象的多态性,从而消除这些条件分支语句 176 | 177 | ## 设计模式和多态 178 | 179 | GoF所著的《设计模式》一书的副书名是*可复用面向对象软件的基础*。该书完全完全是从面向对象的角度出发的,通过对`封装`、`继承`、`多态`、`组合`的反复使用,提来出一些可重复使用的面向对象设计技巧。而多态在其中又是重中之重,绝大部分设计模式的实现都离不开多态性的思想。 180 | 181 | 在命令模式中,请求被封装在一些命令对象里,这使得命令的调用者和命令的接收者完全解耦,当调用命令的`execute`方法时,不同的命令会做不同的事情,从而产生不同的执行结果。而做这些事情的过程是早已被封装在命令对象内部的,作为调用命令的客户,根本没必要关系命令的执行过程。 182 | 183 | 在策略模式中,Context并没有执行算法的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被封装在对象内部,当我们对这些策略对象发出*计算*的消息时,它们会返回各自不同的计算结果。 184 | 185 | 在JavaScript这种函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够传递。当我们对一些函数发出*调用*的消息时,这些函数会返回不同的执行结果,这是多态性的一种体现,也是很多设计模式在JavaScript中可以用高阶函数来替代实现的原因。 186 | -------------------------------------------------------------------------------- /1. 基础知识/1.3 封装.md: -------------------------------------------------------------------------------- 1 | # 封装 2 | 3 | 封装的目的是将信息隐藏,一般有四种 4 | 5 | 1. 封装shuju 6 | 7 | 2. 封装实现 8 | 9 | 3. 封装类型 10 | 11 | 4. 封装变化 12 | 13 | ## 封装数据 14 | 15 | 在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了`private`、`public`、`protected`等关键字来提供不同的访问权限。 16 | 17 | 但JavaScript并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只恩呢该模拟出`public`和`private`这两种封装性。 18 | 19 | 除了ES6中提供的`let`之外,一般我们通过函数来创建作用域 20 | 21 | var myObj = (function(){ 22 | var __name = 'sven'; // 私有(private)变量 23 | return { 24 | getName: function(){ // 公有(public)方法 25 | return __name; 26 | } 27 | }; 28 | })(); 29 | 30 | console.log(myObj.getName()); // sven 31 | console.log(myObj.__name); // undefined 32 | 33 | ## 封装实现 34 | 35 | 有时候我们喜欢把封装等同于数据封装,但这是狭义的定义,封装的目的是将信息隐藏,封装应该被视为*任何形式的封装*,也就是说封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。 36 | 37 | 从封装实现细节来讲,封装是的对象内部的变化对其它对象时透明的,对象对它自己行为负责,其它对象或用户都不关系它的内部实现。封装是的对象之间的耦合变松散,对象之间只通过暴露API来通信。我们可以随意修改对象的内部实现,只要对外的借口没有变化,就不会影响到程序的其它功能。 38 | 39 | 封装实现细节的例子非常多,拿迭代器来讲,迭代器的作用是在不暴露一个聚合对象内部表示的前提下,提供一种方式来顺序访问这个聚合对象。我们编写一个`each`函数,它的作用就是便利一个聚合对象,使用这个`each`函数的人不用关心它的内部是怎么实现的,只要提供的功能正确就可以。即使`each`函数修改了内部源代码,只要对外的接口或者调用方式没有变化,用户就不用关心它的内部实现的改变。 40 | 41 | ## 封装类型 42 | 43 | 静态类型是静态语言中一种非常重要的封装方式,一般而言封装类型是通过接口和抽象类进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心的是对象的行为。在许多静态语言的设计模式中,想方设法去隐藏对象的类型,也是促使这些模式诞生的原因之一,比如工厂模式、组合模式等。 44 | 45 | 当然在JavaScript中并没有对抽象类和接口的支持,JavaScript也是一种类型模糊的语言。在封装类型方面JavaScript没有能力,也没有必要做的更多。对于JavaScript的设计模式来说,不区分类型是一种失色,也可以说是一种解脱。 46 | 47 | ## 封装变化 48 | 49 | 从设计模式的角度出发,封装更重要的层面体现为*封装变化* 50 | 51 | > 考虑你的设计中哪些地方可能发生变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候破迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是很多设计模式的主题。 —— 《设计模式》 52 | 53 | 《设计模式》中归纳的23中设计模式,从意图上区分有三类 54 | 55 | 1. 创建型模式 56 | 2. 结构型模式 57 | 3. 行为型模式 58 | 59 | 拿创建型模式来说,要创建一个对象是一种抽象行为,而具体创建什么对象时可变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系,行为型模式封装的是对象的行为变化。 60 | 61 | 通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度的保证程序的稳定性和可拓展性。 62 | -------------------------------------------------------------------------------- /1. 基础知识/1.4 prototype.md: -------------------------------------------------------------------------------- 1 | # 原型模式和基于原型的对象系统 2 | 3 | 在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。而在原型编程思想中,类并不是必需的,对象未必需要从类中创建而来,一个对象时通过clone另外一个对象所得到的。 4 | 5 | 原型模式不单是一种设计模式,也被称为一种编程泛型。 6 | 7 | ## 使用clone的原型模式 8 | 9 | 从设计模式的角度讲,原型模式是用于创建对象的一种模式,如果我们想创建一个对象,一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,不再关心对象的具体类型,而是找到一个对象,然后通过clone来创建一个一模一样的对象。 10 | 11 | 既然原型模式是通过clone来创建对象的,那么很自然地想到,如果需要一个跟某个对象一模一样的对象,就可以使用原型模式。 12 | 13 | 假设编写一个飞机大战的网页游戏。某种飞机具有分身功技能,如果不适用原型模式,那么在创建分身之前必须先保存该飞机的血量、炮弹等级等信息,然后讲这些信息设置到新建的飞机上,这样才能得到一架一模一样的飞机。 14 | 15 | 如果使用原型模式,我们只需要调用负责clone的方法,便能完成同样功能。原型模式的关键在于语言本身是否提供了clone方法。ES5提供了`Object.create`方法,可以用来clone对象 16 | 17 | function Plane(){ 18 | this.blood = 100; 19 | this.attackLevel = 1; 20 | this.defenseLevel = 1; 21 | }; 22 | 23 | var plane = new Plane(); 24 | plane.blood = 70; 25 | plane.attackLevel = 10; 26 | plane.defenseLevel = 7; 27 | 28 | var clonePlane = Object.create(plane); 29 | 30 | 在不支持ES5的浏览器中可以使用以下代码 31 | 32 | Object.create = Object.create || function(obj){ 33 | var F = function(){}; 34 | F.prototype = obj; 35 | return new F(); 36 | } 37 | 38 | ## clone是创建对象的手段 39 | 40 | 原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,clone知识创建这个对象的过程和手段。 41 | 42 | 在用Java等静态语言编写程序的时候,类型之间的解耦非常重要。依赖导致原则提醒我们创建对象的时候要避免依赖具体类型,而用`new`创建对象很僵硬。工厂方法模式和抽象工厂模式可以帮我们解决这个问题,但这两个模式会带来很多跟产品类平行的工厂类层次,增加很多额外代码。 43 | 44 | 原型模式提供了另一种创建对象的方式,通过clone对象,不用关心对象具体类型。在JavaScript这种弱类型语言中,创建对象非常容易,不存在类型耦合问题。从设计模式角度讲,原型模式的意义并不大。但JavaScript本身是一门基于原型的面向对象语言,它的对象系统就是利用原型模式来搭建的,在这里称之为原型编程泛型更合适。 45 | 46 | ## JavaScript中的原型继承 47 | 48 | 原型编程泛型有以下几个规则 49 | 50 | 1. 所有数据都是对象 51 | 2. 要得到一个对象不是通过实例化一个类,而是找到一个对象作为原型并clone它 52 | 3. 对象会记住它的原型 53 | 4. 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型 54 | 55 | JavaScript同样遵守这几条规则,下面看看JavaScript如何在这些规则基础之上构建它的对象系统 56 | 57 | ### 所有数据都是对象 58 | 59 | JavaScript在设计的时候,模仿Java引入了两套类型机制 60 | 61 | 1. 基本类型:undefined、number、boolean、string、null 62 | 2. 对象类型:object 63 | 64 | 按照JavaScript设计者的本意,除了undefined之外,一切都应是对象。为了实现这一目标,`number`、`boolean`、`string`这几个基本类型数据也可以通过*包装类*的方式变成对象类型数据来处理。 65 | 66 | 我们不能说在JavaScript中所有的数据都是对象,但可以说绝大部分数据都是对象。JavaScript中有一个根对象,所有对象追根溯源都来自这个对象—— Object.prototype。这是一个空对象,我们遇到的每个对象都是从`Object.prototype` clone来的。 67 | 68 | var obj1 = new Object(); 69 | var obj2 = {}; 70 | 71 | ### 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并clone它 72 | 73 | 在JavaScript中并不需要关心clone的细节,引擎内部实现,我们要做的只是显式调用 74 | 75 | var obj1 = new Object(); 76 | var obj2 = {}; 77 | 78 | 引擎内部会从`Object.prototype`上面colne出一个对象出来,我们最终得到的就是这个对象。看看如果使用`new`运算符从构造器中得到一个对象 79 | 80 | function Person(name){ 81 | this.name = name; 82 | } 83 | 84 | Person.prototype.getName = function(){ 85 | return this.name; 86 | }; 87 | 88 | var person = new Person('Byron'); 89 | 90 | console.log(person.name); 91 | console.log(person.getName()); 92 | console.log(Object.getPrototypeOf(person) === Person.prototype); 93 | 94 | 在JavaScript中没有类的概念,但刚才却调用了`new Person()`,在这里Person并不是类,而是函数构造器,JavaScript函数即可以作为普通函数被调用,也可以作为构造器被调用。当使用`new`运算符来调用函数时,函数就是一个构造器,这个过程,实际上也是先clone `Object.prototype`对象,再进行一些其它额外操作的过程。 95 | 96 | 在Chrome和Firefox等对外暴露了对象`__proto__`属性的浏览器下,我们可以下面代码理解**new**的过程 97 | 98 | function Person(name){ 99 | this.name = name; 100 | } 101 | 102 | Person.prototype.getName = function(){ 103 | return this.name; 104 | }; 105 | 106 | var objectFactory = function(){ 107 | var obj = new Object(), // 从Object.prototype上clone一个空对象 108 | Constructor = [].shift.call(arguments); // 获取外部传入的构造器 109 | 110 | obj.__proto__ = Constructor.prototype; // 指向正确的原型 111 | var ret = Constructor.apply(obj, arguments); // 借用外部传入的构造器给obj添加属性 112 | return typeof ret === 'obj' ? ret : obj; 113 | }; 114 | 115 | var person = objectFactory(Person, 'Byron'); 116 | 117 | ### 对象会记住它的原型 118 | 119 | 如果请求可以在一个链条中依次往后传递,那么每个节点都必须知道它的下一个节点。同理要想完成JavaScript的原型链查找机制,每个对象至少应该先记住它的原型。 120 | 121 | 目前我们一直在讨论*对象的原型*,就JavaScript的真正实现来说,并不能说对象有原型,而只能说对象的构造器有原型。对于*对象把请求委托给它自己的原型*这句话,更好的说法是对象把请求委托给它的构造器的原型。 122 | 123 | JavaScript给对象提供了一个名为`__proto__`的隐藏属性,指向其构造器的原型对象,即`Constructor.prototype`。在一些浏览器中`__proto__`属性被公开出来,可以在Chrome或Firefox上验证 124 | 125 | var obj = new Object(); 126 | console.log(a.__proto__ === Object.prototype); 127 | 128 | `__proto__`是对象和对象构造器的原型联系起来的纽带。正因为对象要通过`__proto__`属性来记住它的构造器原型,所以上面例子`objectFactory`函数模拟new创建对象的时候,要收工给obj设置正确的`__proto__`属性 129 | 130 | ### 如果对象没法响应某个请求,会把请求委托给它的构造器原型 131 | 132 | 这条规则是原型继承的精髓所在。在JavaScript中,每个对象都是从`Object.prototype`对象clone而来,如果是这样的话,我们只能得到单一的继承关系,每个对象都继承自`Object.prototype`,这样的对象系统显然是非常受限的。 133 | 134 | 实际上JavaScript的对象最初都是由`Object.prototype`对象clone而来,但对象构造器的原型并不仅限于`Object.prototype`上,而是可以动态的指向其它对象。这样一来当对象a需要借用对象b的能力时,可以把对象a的构造器原型指向对象b,从而达到继承效果。 135 | 136 | var obj = {name: 'Byron'}; 137 | 138 | function A(){}; 139 | A.prototype = obj; 140 | 141 | var a = new A(); 142 | consloe.log(a.name); 143 | 144 | 我们看看执行这段代码的时候引擎做了那些事情 145 | 146 | 1. 尝试遍历对象**a**中的所有属性,但是没有找到*name* 147 | 2. 查找*name*属性这个请求被委托给a的构造器原型,它被`a.__proto_`属性记录,指向`A.prototype`,也就是对象**obj** 148 | 3. 在对象**obj**中找到*name*属性病返回 149 | 150 | 对于层次多的场景查找方式类似,当查找到`Object.prototype`仍然没有的时候就返回`undefined` 151 | 152 | ## 原型继承的未来 153 | 154 | 设计模式在很多时候其实体现了语言的不足, Peter Norvig曾说过,设计模式是对语言不足的补充,如果要费尽心思使用设计模式,不如使用一门更好的语言。 155 | 156 | 作为web前端开发者,相信在很长一段时间内JavaScript是唯一选择,虽然没有办法换一门语言,但语言本身也在发展,说不定哪天某个模式在JavaScript就已经是天然存在,不再需要拐弯抹角来实现。比如`Object.create`就是原型模式的天然实现,使用Object.create来完成原型继承看起来更能体现原型模式的精髓,目前大部分浏览器都提供了Object.create方法。 157 | 158 | 但在当前的引擎下,通过Object.create来创建对象的效率并不高,比通过构造函数创建对象要慢。此外还需要注意的是,通过设置构造器的*prototype*来实现原型继承的时候,除了根对象Object.prototype以外,任何对象都有一个原型,而通过`Object.create(null)`可以创建没有原型的对象。 159 | 160 | 另外ECMAScript6带来了新的Class语法,这让JavaScript看起来像是一门基于类的语言,但其背后仍是通过原型机制来创建对象 161 | 162 | class Animal{ 163 | constructor(name){ 164 | this.name = name; 165 | } 166 | 167 | getName(){ 168 | return this.name; 169 | } 170 | } 171 | 172 | class Dog extends Animal{ 173 | constructor(name){ 174 | super(name); 175 | } 176 | 177 | speak(){ 178 | return 'woof'; 179 | } 180 | } 181 | 182 | var dog = new Dog(); 183 | console.log(dog.getName()); 184 | console.log(dog.speak()); 185 | -------------------------------------------------------------------------------- /1. 基础知识/2.1 this.md: -------------------------------------------------------------------------------- 1 | # this 2 | 3 | 在JavaScript中this总是让初学者感到迷惑,和别的语言大相径庭的是,JavaScript的this总是指向一个对象,而具体是指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。 4 | 5 | 6 | 除去不常使用的`with`、`setTimeout`和`eval`的情况,具体到实际应用中,this的指向大致可以有四种 7 | 8 | 1. 作为对象的方法调用 9 | 2. 作为普通函数调用 10 | 3. 构造器调用 11 | 4. `Function.prototype.call`和`Function.prototype.apply`调用 12 | 5. DOM事件处理程序 13 | 14 | ## 作为对象方法被调用 15 | 16 | 当函数作为对象方法被调用的时候,this指向该对象 17 | 18 | var obj = { 19 | a: 1, 20 | getA: function(){ 21 | console.log(this === obj); 22 | console.log(this.a); 23 | } 24 | }; 25 | 26 | obj.getA(); 27 | 28 | ## 作为普通函数调用 29 | 30 | 当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时this指向全局对象,在浏览器环境中就是`window`对象。 31 | 32 | var a = 0; 33 | var obj = { 34 | a: 1, 35 | getA: function(){ 36 | console.log(this === obj); 37 | console.log(this.a); 38 | } 39 | }; 40 | 41 | obj.getA(); 42 | 43 | var getName = obj.getName; 44 | getName(); 45 | 46 | ## 构造器调用 47 | 48 | JavaScript中没有类,但是可以从构造器中创建对象,同事叶提供了new操作符,使得构造器看起来更像是一个类。 49 | 50 | 除了宿主提供的内置函数,大部分JavaScript函数都可以当做构造器使用,构造器和普通函数的区别仅在于调用方式。当用new操作符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象 51 | 52 | var MyClass = function(){ 53 | this.name = 'Byron'; 54 | }; 55 | 56 | var obj = new MyClass(); 57 | console.log(obj.name); 58 | 59 | 但用new调用构造器时,还要注意一个问题,如果构造器显式的返回一个object类型的对象,那么此次运算结果最终会返回这个对象,而不是我们预期的this 60 | 61 | var MyClass = function (){ 62 | this.name = 'Byron'; 63 | return { 64 | name: 'Casper' 65 | }; 66 | }; 67 | 68 | var obj = new MyClass(); 69 | console.log(obj.name); 70 | 71 | 如果构造器不显式返回任何数据或者返回一个飞对象类型数据,返回的仍然是this 72 | 73 | ## Function.prototype.call和Function.prototype.apply调用 74 | 75 | 和普通函数调用相比,用`Function.prototype.call`和`Function.prototype.apply`可以动态的改变函数的this 76 | 77 | var obj = { 78 | name: 'Byron', 79 | getName: function(){ 80 | return this.name; 81 | } 82 | }; 83 | 84 | var obj2 = { 85 | name: 'Casper' 86 | }; 87 | 88 | console.log(obj1.getName()); 89 | console.log(obj1.getName.call(obj2)); 90 | 91 | `call` 和 `apply` 方法能够很好的体现JavaScript的函数语言特征,在JavaScript中几乎每一次编写函数式语言风格代码,都离不开`call`和`apply`,在JavaScript诸多版本的设计模式中,也会用到。 92 | 93 | ## DOM事件处理程序 94 | 95 | var ele = document.querySelector('div'); 96 | 97 | ele.addEventListener('click', function(e){ 98 | console.log(this); 99 | }, false); 100 | 101 | DOM事件的处理程序this指向DOM本身,但在低版本IE指向window 102 | -------------------------------------------------------------------------------- /1. 基础知识/2.2 call和apply.md: -------------------------------------------------------------------------------- 1 | # call和apply 2 | 3 | ECMAScript3 给Function的原型定义了两个方法`Function.prtotype.call`和`Function.prototype.apply`。在实际开发中,特别是一些函数式风格的编码中,call和apply方法尤为有用。 4 | 5 | ## call和apply的区别 6 | 7 | call和apply作用一模一样,区别在于传入参数的形式不同。 8 | 9 | apply接受两个参数,第一个参数指定了函数体内`this`的指向,第二个参数是(类)数组结构,apply把这个集合内的元素作为参数传给被调用函数。 10 | 11 | function func(a, b, c){ 12 | console.log([a, b, c]); 13 | } 14 | 15 | func.apply(null, [1, 2, 3]); 16 | 17 | call传入的参数不固定,第一个参数也是指定函数体内`this`指向,从第二个参数以后每个参数以此被传入函数 18 | 19 | func.call(null, 1, 2, 3); 20 | 21 | 当调用一个函数时,JavaScript解释器并不会计较形参和实参在数量、类型以及顺序的区别,JavaScript的参数在内部就是用一个数组表示的。apply的使用比call更频繁,我们不必关心具体有多少个参数被传入函数,只要用apply传入即可。 22 | 23 | call是包装在apply上的一个语法糖,如果明确知道函数接受多少个参数,而且想一目了然地表达形参和实参对应关系,可以使用call 24 | 25 | 当使用call和apply的时候,如果我们传入的第一个参数为`null`,函数体内this会指向默认的宿主对象,在浏览器中就是`window`,但是在严格模式下,this还是null 26 | 27 | ## call和apply用途 28 | 29 | ### 改变this指向 30 | 31 | call和apply最常见用处就是改变this指向,看个例子 32 | 33 | var obj1 = { 34 | name: 'Byron' 35 | }; 36 | 37 | var obj2 = { 38 | name: 'Casper' 39 | }; 40 | 41 | window.name = 'window'; 42 | 43 | function getName(){ 44 | console.log(this.name); 45 | } 46 | 47 | getName(); // window 48 | getName.call(obj1); // Byron 49 | getName.call(obj2); // Casper 50 | 51 | 在实际开发中,经常会遇到this指向被不经意改变的场景,比如有一个div节点 52 | 53 | document.getElementById('div1').onclick = function(){ 54 | console.log(this.id); // div1 55 | }; 56 | 57 | 如果事件处理程序中有一个内部函数`func`,在事件内部调用`func`函数时,func函数体内的this就指向了`window` 58 | 59 | document.getElementById('div1').onclick = function(){ 60 | console.log(this.id); // div1 61 | 62 | function func(){ 63 | console.log(this.id); 64 | } 65 | 66 | func(); // undefined 67 | }; 68 | 69 | 这时候可以使用call来修正this指向 70 | 71 | document.getElementById('div1').onclick = function(){ 72 | console.log(this.id); // div1 73 | 74 | function func(){ 75 | console.log(this.id); 76 | } 77 | 78 | func.call(this); // div1 79 | }; 80 | 81 | ### Function.prototype.bind 82 | 83 | 现代浏览器内置了`Function.prototype.bind`来指定函数内部this指向,在低版本浏览器上可以模拟实现 84 | 85 | Function.prototype.bind = function(context){ 86 | var self = this; // 保存原函数 87 | return function(){ 88 | return self.apply(context, arguments); 89 | }; 90 | }; 91 | 92 | var obj = { 93 | name: 'Byron' 94 | }; 95 | 96 | var func = (function(){ 97 | console.log(this.name); 98 | }).bind(obj); 99 | 100 | func(); // Byron 101 | 102 | 在`Function.prototype.bind`的内部实现中,先把func保存起来,然后返回一个新函数。在函数内部`self.apply(context, arguments)`这句代码才是原来的func函数,并且this指向context。 103 | 104 | 这是一个简化版的实现,通常我们会实现的稍微复杂一些,可以往func函数中预填一些参数 105 | 106 | Function.prototype.bind = function(){ 107 | var self = this, 108 | context = [].shift.call(arguments), // 第一个参数是上下文,this指向 109 | args = [].slice.call(arguments); // 剩余参数转为数组 110 | 111 | return function(){ 112 | var newArgs = [].concat.call(agrs, [].slice.call(arguments)); // 允许新函数添加参数 113 | self.apply(context, newArgs); 114 | } 115 | }; 116 | 117 | var obj = { 118 | name: 'Byron' 119 | }; 120 | 121 | function func(a, b, c, d){ 122 | console.log(this.name); 123 | console.log([a, b, c, d]); 124 | } 125 | 126 | func = func.bind(obj, 1, 2); 127 | 128 | func(3, 4); 129 | 130 | ### 借用其它对象方法 131 | 132 | 借用方法的第一种场景是*借用构造函数*,通过这种技术,可以实现一些类似继承效果 133 | 134 | var A = function(name){ 135 | this.name = name; 136 | }; 137 | 138 | var B = function(){ 139 | A.apply(this, arguments); 140 | }; 141 | 142 | B.prototype.getName = function(){ 143 | return this.name; 144 | }; 145 | 146 | var b = new B('Byron'); 147 | b.getName(); // Byron 148 | 149 | 借用方法的第二种场景更常见 150 | 151 | 函数的参数列表`arguments`是一个类数组对象,虽然也有下标,但并非数组,不能使用数组的方法。这种情况下我们经常会借用`Array.prototype`对象的方法 152 | 153 | (function(){ 154 | Array.prototype.push.call(arguments, 3); 155 | console.log(agguments) 156 | })(1, 2); 157 | 158 | 在操作arguments的时候经常需要借用Array.prototype的方法 159 | 160 | 想把arguments转换为数组的时候,可以借用`Array.prototype.slice`方法;想截去arrguments列表中头一个元素时,可以借用`Array.prototype.shift`方法。看看V8源代码怎么实现的,以`Array.prototype.push` 161 | 162 | function ArrayPush(){ 163 | var n = TO_UINT32(this.length); // 被push对象的长度 164 | var m = %_ArgumentsLength(); // push的参数个数 165 | for(var i = 0; i < m; i++){ 166 | this[i+n] = %_Arguments(i); // 复制元素 167 | } 168 | this.length = n + m; 169 | return this.length; 170 | } 171 | 172 | 通过这段代码可以看到,Array.prototype.push实际上是一个属性复制过程,把参数按照下标=依次添加到被push的对象上面,顺便修改*length*值。至于被修改的对象是谁,到底是数组还是类数组对象,根本不关心,所以我们可以把任意对象传入'Array.prototype.push' 173 | -------------------------------------------------------------------------------- /1. 基础知识/3.1 闭包.md: -------------------------------------------------------------------------------- 1 | # 闭包 2 | 3 | 虽然JavaScript是一门面向对象的编程语言,但这门语言同事也拥有许多函数式语言的特征。 4 | 5 | 函数式语言的鼻祖是LISP,JavaScript在设计之初参考了LISP的方言**Scheme**,引入*Lambda*表达式、闭包、高阶函数等特性,使用这些特性可以用一些灵活而巧妙的方式来编写JavaScript代码。 6 | 7 | ## 变量作用域 8 | 9 | 变量作用域是指变量有效范围。当在函数内部使用`var`关键字声明变量,这时候变量是局部变量,只在函数内部有效,在函数外部不能访问。有趣的是JavaScript和大多数语言不同,使用`var`声明变量`{}`并不会形成块作用域,仍然是函数作用域。不实用`var`的时候,编程全局变量。 10 | 11 | function func(){ 12 | var a = 1; 13 | if(a > 0){ 14 | var b = 2; 15 | } 16 | 17 | console.log(a); // 1 18 | console.log(b); // 2 19 | } 20 | 21 | console.log(a); // a is not defined 22 | 23 | ## 变量的生命周期 24 | 25 | 除了变量的作用域外,另一个和闭包有关的概念就是变量的生命周期。对于全局变量,生命周期除非我们主动销毁会一直存在。对于局部变量,当退出函数时,局部变量不能再被访问,它们会随着函数调用的结束而销毁。 26 | 27 | var func = function(){ 28 | var a = 1; // 每次函数执行完后销毁 29 | console.log(a); 30 | }; 31 | 32 | func(); // 1 33 | func(); // 1 34 | 35 | 现在看看下面这段代码 36 | 37 | var func = function(){ 38 | var a = 1; 39 | 40 | return function(){ 41 | a++; 42 | console.log(a); 43 | }; 44 | }; 45 | 46 | var fn = func(); 47 | 48 | fn(); // 2 49 | fn(); // 3 50 | fn(); // 4 51 | 52 | 和我们之前的推论相反,函数退出后,局部变量`a`并没有被销毁,而是一直存在。这是因为当执行`var fn = func();`时,返回了一个匿名函数的引用,它可以访问到`func()`被调用时产生的环境,而局部变量`a`就在这个环境中。既然局部变量所在的环境还能呗外界访问,这个局部变量就有了不被销毁的理由。在这里产生一个闭包结构,局部变量的生命周期被延续。 53 | 54 | 闭包有一个经典应用。假设页面上有5个idv节点,我们通过循环给每个div绑定*click*事件,点击的时候弹出相应索引 55 | 56 | var nodes = doccument.querySelecotrAll('div'); 57 | for(var i = 0; i < nodes.length; i++){ 58 | nodes[i].onclick = function(){ 59 | alert(i); 60 | }; 61 | } 62 | 63 | 测试的时候会发现无论点击哪个div都弹出数字5.这是因为div节点的click是被异步触发的,当事件触发的时候,for循环早已结束,此时变量`i`已经是5,所以在div的click事件函数中顺着作用域链从内到外查找变量i时,查到的都是5。 64 | 65 | 可以利用闭包,把每次循环时的i保存起来 66 | 67 | for(var i = 0; i < nodes.length; i++){ 68 | (function(i){ 69 | nodes[i].onclick = function(){ 70 | alert(i); 71 | }; 72 | })(i); 73 | } 74 | 75 | ## 闭包的更多作用 76 | 77 | ### 封装变量 78 | 79 | 闭包可以帮助把一些不需要暴露在全局的变量封装成*私有变量*。假设有一个计算乘积的简单函数 80 | 81 | var mul = function(){ 82 | var a = 1; 83 | for(var i = 0, l = arguments.length; i < l; i++){ 84 | a = a * arguments[i]; 85 | } 86 | 87 | return a; 88 | }; 89 | 90 | mult函数接受一些number类型参数,并返回这些参数的乘积。对于哪些相同的参数来说,每次计算是一种浪费,我们可以加入缓存机制来提高函数性能 91 | 92 | var cache = {}; 93 | var mult = function(){ 94 | var args = Array.prototype.join.call(arguments, ','); 95 | if(cacge[args]){ 96 | return cache[args]; 97 | } 98 | var a = 1; 99 | for(var i = 0, l = arguments.length; i < l; i++){ 100 | a = a * arguments[i]; 101 | } 102 | 103 | return a; 104 | } 105 | 106 | 我们看到cache变量仅仅在mult函数中被使用,与其让cache变量和mult函数平行暴露在全局变量下,不如把它封装在mult函数内部,这样可以避免cache被意外修改 107 | 108 | var mult = (function(){ 109 | var cache = {}; 110 | return function(){ 111 | var args = Array.prototype.join.call(arguments, ','); 112 | if(cacge[args]){ 113 | return cache[args]; 114 | } 115 | var a = 1; 116 | for(var i = 0, l = arguments.length; i < l; i++){ 117 | a = a * arguments[i]; 118 | } 119 | 120 | return a; 121 | }; 122 | })(); 123 | 124 | 提炼函数是代码重构的一个重要技巧,如果在一个大函数中有一些代码能够独立出来,我们常常把这些代码封装在独立的小函数中,独立出来的小函数有利于代码复用,命名良好还有注释作用。如果这些小函数不需要在程序的其它地方用,最好把他们用闭包封装起来 125 | 126 | var mult = (function(){ 127 | var cache = {}; 128 | var calculate = function(){ 129 | var a = 1; 130 | for(var i = 0, l = arguments.length; i < l; i++){ 131 | a = a * arguments[i]; 132 | } 133 | return a; 134 | }; 135 | 136 | return function(){ 137 | var args = Array.prototype.join.call(arguments, ','); 138 | if(args in cache){ 139 | return cache[args]; 140 | } 141 | return cache[args] = calculate.apply(null, arguments); 142 | }; 143 | })(); 144 | 145 | ### 延续局部变量生命周期 146 | 147 | img对象经常用于数据上报 148 | 149 | var report = function(src){ 150 | var img = new Image(); 151 | img.src= src; 152 | }; 153 | 154 | report('//xxx.com/getUserInfo'); 155 | 156 | 因为一些低版本浏览器bug,report函数会丢失30%左右数据,也就是说并不是每次report函数都成功发起了HTTP请求。丢失数据的原因是img是report函数的局部变量,当report函数的调用结束后,img局部变量随即被销毁,此时还没来得及发送HTTP请求 157 | 158 | 现在我们可以把img变量用闭包封闭起来,便可以解决请求丢失问题 159 | 160 | var report = (function(){ 161 | var imgs = []; 162 | return function(src){ 163 | var img = new Image(); 164 | imgs.push(img); 165 | img.src = src; 166 | }; 167 | })(); 168 | 169 | ## 闭包和面向对象设计 170 | 171 | 过程和数据的结合是形容面向对象中的*对象*时常用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的方式包含了数据。同常用面向对象能实现的功能,用闭包也能实现。在JavaScript语言的祖先Scheme语言中,甚至没有提供面向对象的原生设计,但可以使用闭包来实现一个完整的面向对象系统。 172 | 173 | var extent = function(){ 174 | var value = 0; 175 | return { 176 | call: function(){ 177 | value++; 178 | console.log(value); 179 | } 180 | }; 181 | }; 182 | 183 | var extent = extent(); 184 | 185 | extent.call(); 186 | extent.call(); 187 | extent.call(); 188 | 189 | 如果换成面向对象的写法就是 190 | 191 | var extent = { 192 | value: 0, 193 | call: function(){ 194 | this.value++; 195 | console.log(this.value); 196 | } 197 | }; 198 | 199 | extent.call(); 200 | extent.call(); 201 | extent.call(); 202 | 203 | ## 用闭包实现命令模式 204 | 205 | 在JavaScript版本的各种设计模式中,闭包的应用非常广泛,在完成闭包实现命令模式之前,我们先用面向对象的方式来编写一段命令模式的代码 206 | 207 | 208 |
209 | 210 | 211 | 244 | 245 | 246 | 247 | 命令模式的意图是把请求封装为对象,从而分离请求的发起者和接收者之间的耦合关系。在命令被执行之前,可以预先旺命令对象中植入命令的接收者。 248 | 249 | 但在JavaScript中,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求闲的更加简单和自然。如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作。在面向对象版本的命令模式中,余弦值入的命令接收者被当做属性保存起来;而在闭包版本的命令模式中,命令接收者会被封装在闭包形成的环境中 250 | 251 | var Tv = { 252 | open: function(){ 253 | console.log('打开电视机'); 254 | }, 255 | close: function(){ 256 | console.log('关闭电视机'); 257 | } 258 | }; 259 | 260 | var createCommand = function(receiver){ 261 | var execute = function(){ 262 | return receiver.open(); 263 | }; 264 | 265 | var undo = function(){ 266 | return receiver.close(); 267 | }; 268 | 269 | return { 270 | execute: execute, 271 | undo: undo 272 | }; 273 | }; 274 | 275 | var setCommand = function(command){ 276 | document.getElementById('execute').onClick = function(){ 277 | command.execute(); 278 | }; 279 | document.getElementById('undo').onClick = function(){ 280 | command.undo(); 281 | }; 282 | }; 283 | 284 | setCommand(createCommand(Tv)); 285 | 286 | ## 闭包与内存管理 287 | 288 | 闭包是一个非常强大的特性,但人们对它也有很多误解,一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。 289 | 290 | 局部变量本应该在函数被退出的时候解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能够一直存在下去。从这个角度看闭包确实会使一些数据无法被即时销毁。 291 | 292 | 使用闭包一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存的影响是一致的,这里并不能说形成内存泄露。 293 | 294 | 闭包喝内存泄露有关系的地方是,使用闭包的同时很容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候有可能会内存泄露。这并非闭包问题,也不是JavaScript问题,在低版本IE浏览器中,用于BOM和DOM中的对象是使用C++以COM对象的方式实现,而COM对象的垃圾回收机制采用的引用计数策略,在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成循环引用,那么两个对象都无法被回收,循环引用造成的内训泄露在本质上也不是闭包引造成的。 295 | 296 | 要想解决循环引用带来的内训泄露问题,我们只需要把循环引用中的变量设置为`null`即可。将变量设置为null意味着切断变量与之前引用值之间的连接,当垃圾回收器下次运行时,就可以删除这些值并回收它们占用的内存。 297 | -------------------------------------------------------------------------------- /1. 基础知识/3.2 高阶函数.md: -------------------------------------------------------------------------------- 1 | # 高阶函数 2 | 3 | 高阶函数是指至少满足下列条件之一的函数 4 | 5 | 1. 函数可以作为参数被传递 6 | 7 | 2. 函数可以作为返回值输出 8 | 9 | JavaScript中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数作为参数传递,还是让函数的执行结果返回另外一个函数,这两种情形都有很多应用场景。 10 | 11 | ## 函数作为参数传递 12 | 13 | 把函数当做参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离叶无道马忠变化与不变的部分。一个重要的应用场景就是回调函数。 14 | 15 | ### 异步回调 16 | 17 | 在ajax异步请求中,回调函数的使用非常频繁。当我们想在ajax请求返回之后做一些事情,但又并不知道请求返回的确切时间时,最常见的方案就是把callback函数当做参数传入发起ajax请求的方法中,待请求完成之后执行callback函数: 18 | 19 | function getUserInfo(userId, callback){ 20 | $.ajax({ 21 | url: '//xxx.com/getUserInfo', 22 | data: { 23 | id: userId 24 | } 25 | }).done(function(result){ 26 | callback(result); 27 | }); 28 | } 29 | 30 | getUserInfo(12345, function(data){ 31 | console.log(data.name); 32 | }); 33 | 34 | 回调函数不仅只在异步请求中,当一个函数不适合执行一些请求时,我们可以把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,委托给另外一个函数执行。 35 | 36 | 比如希望在页面中创建100个*div*节点,然后把这些节点隐藏,下面是一种代码编写方式: 37 | 38 | function appendDiv(){ 39 | for(var i = 0; i < 100; i++){ 40 | var div = document.createElement('DIV'); 41 | div.innerHTML = i; 42 | document.body.appendChild(div); 43 | div.style.display = 'none'; 44 | } 45 | } 46 | 47 | appendDiv(); 48 | 49 | 这样我们把创建和隐藏逻辑杂糅到了一起,`appendDiv`函数过于僵硬,很难被复用。我们可以把影藏div代码抽离出来,用回调函数的形式传入 50 | 51 | function appendDiv(callback){ 52 | for(var i = 0; i < 100; i++){ 53 | var div = document.createElement('DIV'); 54 | div.innerHTML = i; 55 | document.body.appendChild(div); 56 | if(typeof callback === 'function'){ 57 | callback(div); 58 | } 59 | } 60 | } 61 | 62 | appendDiv(function(node){ 63 | node.style.display = 'none'; 64 | }); 65 | 66 | 可以看到,隐藏节点的请求实际上是由客户端发起的,但客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放到回调函数中,委托给`appendDiv`方法。当节点创建好的时候,`appendDiv`会执行之前客户传入的回调函数。 67 | 68 | ### Array.prototype.sort 69 | 70 | `Array.prototype.sort`接受一个函数做参数,这个函数封装了数组元素的排序规则,从其使用规则可以看到,我们的目的是对数组排序,这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装到函数里动态传入,使`Array.prototype.sort` 71 | 72 | // 从小到大排序 73 | [1, 4, 2, 9, 3].sort(function(x, y){ 74 | return x - y; 75 | }); 76 | 77 | // 从大到小排序 78 | [1, 4, 2, 9, 3].sort(function(x, y){ 79 | return y - x; 80 | }); 81 | 82 | ## 函数作为返回值传入 83 | 84 | 相比把函数作为参数传递,函数当做返回值的应用场景更多,也更能体现函数式编程的巧妙,让函数继续返回一个可执行函数,意味着运算过程是可延续的。 85 | 86 | ### 判断数据的类型 87 | 88 | 看个例子,判断数据类型的函数 89 | 90 | function isString(obj){ 91 | return Object.prototype.toString.call(obj) === '[object String]'; 92 | } 93 | 94 | function isArray(obj){ 95 | return Object.prototype.toString.call(obj) === '[object Array]'; 96 | } 97 | 98 | function isNumber(obj){ 99 | return Object.prototype.toString.call(obj) === '[object Number]'; 100 | } 101 | 102 | 可以看出这些函数大部分实现都是相同的,不同的只是`Object.prototype.toString.call`返回的字符串。为了避免重复,我们可以尝试把这些字符串作为参数提前植入`isType`函数: 103 | 104 | function isType(type){ 105 | return function(obj){ 106 | return Object.prototype.toString.call(obj) === '[object ' + type +']'; 107 | }; 108 | } 109 | 110 | ### getSingle 111 | 112 | 下面是一个单例模式的例子 113 | 114 | function getSingle(fn){ 115 | vae ret; 116 | return function(){ 117 | return ret || (ret = fn.apply(this, arguments)); 118 | }; 119 | } 120 | 121 | 这个高阶函数的例子,即把函数当做参数传递,又让函数执行后反悔了另外一个函数 122 | 123 | ## AOP 124 | 125 | AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,通常是日志统计、安全控制、异常处理等。把这些功能处理出来之后,再通过动态植入的方式进入业务逻辑模块中。这样做好处首先是可以保持业务逻辑模块的高内聚,其次可以方便的复用日志统计等功能模块。 126 | 127 | 在Java中可以通过反射和动态代理等技术实现AOP,而在JavaScript中AOP的实现更加简单,下面通过`Function.prototype`来实现这一点: 128 | 129 | Function.prototype.before = function(beforeFn){ 130 | var _self = this; 131 | return function(){ 132 | beforeFn.apply(this, arguments); 133 | return _self.apply(this, arguments); 134 | }; 135 | }; 136 | 137 | Function.prototype.after = function(afterFn){ 138 | var _self = this; 139 | return function(){ 140 | var ret = _self.apply(this, arguments); 141 | afterFn.apply(this, arguments); 142 | return ret; 143 | }; 144 | }; 145 | 146 | var func = function(){ 147 | console.log(2); 148 | }; 149 | 150 | func = func.before(function(){ 151 | console.log(1); 152 | }).after(function(){ 153 | console.log(3); 154 | }); 155 | 156 | func(); // 1 2 3 157 | 158 | ## curring 159 | 160 | 函数柯里化又成部分求值,一个currying的函数首先会接受一些参数,接受这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,之前传入的参数在函数形成的闭包中被保存起来。等函数整整需要求值的时候,之前传入的所有参数会一次性用于求值。 161 | 162 | 从字面上理解currying不太容易,看个例子。假设要编写一个计算每月开销的函数,在每天结束之前,记录今天花了多少钱 163 | 164 | var monthlyCost = 0; 165 | 166 | function cost(money){ 167 | monthlyCost += money; 168 | } 169 | 170 | cost(100); // 第1天开销 171 | cost(200); // 第2天开销 172 | cost(300); // 第3天开销 173 | cost(700); // 第30天开销 174 | 175 | 每天结束后都会记录并计算到今天为止花掉的钱。但我们其实并不关心每天花掉多少钱,而只想知道月底的时候花了多少钱,也就是说只需要在月底计算一次。 176 | 177 | 如果在每个月的前29天,我们都知识保存好当天的开销,知道第三十天才进行求值运算,这样就达到了我们的要求。 178 | 179 | var cost = (function(){ 180 | var args = []; 181 | 182 | return function(){ 183 | if(arguments.length === 0){ 184 | var money = 0; 185 | for(var i = 0, l = args.length; i < l; i++){ 186 | money += args[i]; 187 | } 188 | return noney; 189 | }esle{ 190 | [].push.apply(args, arguments); 191 | } 192 | }; 193 | })(); 194 | 195 | cost(100); // 未真正求值 196 | cost(200); // 未真正求值 197 | cost(300); // 未真正求值 198 | 199 | cost(); // 600 200 | 201 | 接下来编写一个通用的`currying`函数,函数接受一个将要被currying的函数作为参数 202 | 203 | var currting = function(fn){ 204 | var args = []; 205 | 206 | return function(){ 207 | if(arguments.length === 0){ 208 | return fn.apply(this, args); 209 | }else{ 210 | [].push.apply(args, arguments); 211 | return arguments.callee; 212 | } 213 | }; 214 | }; 215 | 216 | var cost = (function(){ 217 | var money = 0; 218 | 219 | return function(){ 220 | for(var i = 0, l = arguments.length; i < l; i++){ 221 | money += arguments[i]; 222 | } 223 | 224 | return money; 225 | }; 226 | })(): 227 | 228 | var cost = currying(cost); 229 | 230 | cost(100); // 未真正求值 231 | cost(200); // 未真正求值 232 | cost(300); // 未真正求值 233 | 234 | cost(); // 600 235 | 236 | 至此,我们完成了一个currying函数的编写,当调用`cost`时,如果明确的带了一些参数,表示此时并不进行真正的求值运算,而是把这些参数保存起来,此时让`cost`函数返回另外一个函数。只有当我们以不带参数的形式执行`cost`的时候,才利用前面保存的所有参数,真正开始进行求值运算。 237 | 238 | ## uncurrying 239 | 240 | 在JavaScript中,当我们调用对象的某个方法的时候,其实不用去关心该对象是否被设计为拥有这个方法,这是动态语言的特点。一个对象可以调用自身方法以外的方法,使用`call`和`apply`可以完成这个需求。 241 | 242 | (function(){ 243 | Array.prototype.push.call(arguments, 4); 244 | console.log(arguments); 245 | })(1, 2, 3); 246 | 247 | Array.prototype上的方法原本只能用来操作Array对象,但用call和apply可以把任意对象当做this传入某个方法,这样一来,方法中用到this的地方就不局限于原来规定的对象,而是加以泛化并得到更广的适用性。 248 | 249 | uncurrying用来把泛化this的过程提取出来,JavaScript之父Brendan Eich在2011年发表的一篇Twitter 250 | 251 | Function.prototype.uncurrying = function(){ 252 | var self = this; 253 | return function(){ 254 | var obj = Array.prototype.shift.call(arguments); 255 | return self.apply(obj, arguments); 256 | }; 257 | }; 258 | 259 | var push = Array.prototype.push.uncurrying(); 260 | 261 | (function(){ 262 | push(arguments, 4); 263 | console.log(agruments); 264 | })(1, 2, 3); 265 | 266 | 还有一种实现方式 267 | 268 | Function.prototype.uncurrying = function(){ 269 | var self = this; 270 | return function(){ 271 | Function.prototype.call.apply(self, arguments); 272 | }; 273 | }; 274 | 275 | ## 函数节流 276 | 277 | JavaScript中函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则我们不会遇到和性能相关的问题。但在一些少数情况下,函数的触发不是由用户直接控制的,在这些场景下,函数有可能被非常频繁的调用,而造成性能问题。 278 | 279 | ### 函数被频繁调用的场景 280 | 281 | 1. window.resize 事件 282 | 2. mousemove 事件 283 | 3. scroll事件 284 | 285 | ### 函数节流原理 286 | 287 | 通过上面三个场景可以看出,它们面临的共同问题是函数被触发的频率过高。 288 | 289 | 比如我们在window.onresize事件中要打印当前浏览器窗口大小,在我们通过拖拽来改变窗口大小的时候,打印窗口大小的工作1秒钟进行了10次,而我们实际上只需要2次或者3次。 290 | 291 | 这就需要我们按时间忽略掉一些事件请求,比如确保在500ms内只打印一次,很显然我们可以借助`setTimeout`来做此事 292 | 293 | 把即将执行的函数用 `setTimeout`延迟一段时间执行,在这次请求执行之前忽略接下来调用该函数的请求 294 | 295 | ### 代码实现 296 | 297 | var throttle = function(fn, interval){ 298 | 299 | var _self = fn, // 保存需要被演示函数的引用 300 | timer, // 定时器 301 | firstTime = true; // 是否第一次调用 302 | 303 | return function(){ 304 | var args = arguments, 305 | _me = this; 306 | 307 | if(firstTime){ // 第一次调用不要延时 308 | _self.apply(_me, args); 309 | return firstTime = false; 310 | } 311 | 312 | if(timer){ // 定时器还在说明上次还没执行,取消本次请求 313 | return false; 314 | } 315 | 316 | timer = setTimeout(function(){ // 延时一段时间执行 317 | 318 | clearTimeout(timer); 319 | timer = null; // 重置定时器,准备下次调用 320 | _self.apply(_me, args); 321 | 322 | }, interval || 500); 323 | }; 324 | }; 325 | 326 | window.onresize = throttle(function(){ 327 | console.log(1); 328 | }, 500); 329 | 330 | ## 分时函数 331 | 332 | 前面的函数节流我们提供了一种限制函数被频繁调用的解决方案,我们还会遇到另外一个问题,某些函数确实是用户触发调用的,但因为一些客观运营,这些函数会严重影响页面性能。 333 | 334 | 一个例子是创建WebQQ的好友列表,列表通常会有成百上千个好友,如果一个好友用一个节点表示,当我们页面渲染这个列表的时候,可能要一次性王忒慢创建成百上千个节点,在短时间内向页面中大量添加DOM节点显然浏览器吃不消,我们看到的结果就是浏览器会卡顿甚至假死 335 | 336 | var ary = []; 337 | 338 | for(var i = 0; i < 1000; i++){ 339 | ary.push(i); // 假设ary装在了1000个好友的数据 340 | } 341 | 342 | var renderFriendList = function(data){ 343 | for(var i = 0; l = data.length, i < l; i++){ 344 | var div = document.createElement('div'); 345 | div.innerHTML = i; 346 | docuemnt.body.appendChild(div); 347 | } 348 | }; 349 | 350 | renderFriendList(ary); 351 | 352 | 这个问题的解决方案之一是下面的`timeChunk`函数,该函数让创建节点工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。 353 | 354 | timeChunk 函数接受三个参数 355 | 356 | 1. 创建节点时需要用到的数据 357 | 2. 封装了创建节点逻辑的函数 358 | 3. 第一批创建节点的数量 359 | 360 | 361 | 362 | var timeChunk = function(ary, fn, count){ 363 | 364 | var obj, 365 | t; 366 | 367 | var len = ary.length; 368 | 369 | var start = function(){ 370 | for(var i = 0; i < Math.min(count || 1, arr.length); i++){ 371 | var obj = ary.shift(); 372 | } 373 | }; 374 | 375 | return function(){ 376 | t = setInterval(function(){ 377 | if(ary.length === 0){ 378 | return clearInterval(t); 379 | } 380 | 381 | start(); 382 | }, 200); 383 | }; 384 | }; 385 | 386 | ## 惰性加载函数 387 | 388 | 在web开发中因为浏览器之间实现的差异,一些嗅味工作总是不可避免的。比如我们需要一个在各个浏览器中能通用的事件绑定函数 `addEvent`,常见写法 389 | 390 | var addEvent = function(elem, type, handler){ 391 | 392 | if(window.addEventListener){ 393 | return elem.addEventListener(type, handler, false); 394 | } 395 | 396 | if(window.attachEvent){ 397 | return elem.attachEvent('on'+type, handler); 398 | } 399 | 400 | }; 401 | 402 | 这个函数的缺点是吗,当它每次被调用的时候都会执行里面的if分支,虽然开销并不大,但有些方法可以让程序避免这些重复的执行过程。 403 | 404 | 第二种方案是这样,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让addEvent返回一个包裹了正确逻辑的函数。 405 | 406 | var addEvent = (function(){ 407 | 408 | if(window.addEventListener){ 409 | return function(elem, type, handler){ 410 | return elem.addEventListener(type, handler, false); 411 | }; 412 | } 413 | 414 | if(window.attachEvent){ 415 | return function(elem, type, handler){ 416 | elem.attachEvent('on'+type, handler); 417 | }; 418 | } 419 | 420 | })(); 421 | 422 | 这种写法也有中缺点,如果我们没有使用addEvent函数,那么嗅探工作就多余了,稍微改进一下,在第一次进入if分支后重写函数,以后就可以直接使用了 423 | 424 | var addEvent = function(elem, type, handler){ 425 | 426 | if(window.addEventListener){ 427 | addEvent = function(elem, type, handler){ 428 | return elem.addEventListener(type, handler, false); 429 | }; 430 | } 431 | 432 | if(window.attachEvent){ 433 | addEvent = function(elem, type, handler){ 434 | elem.attachEvent('on'+type, handler); 435 | }; 436 | } 437 | 438 | addEvent(elem, type, handler); 439 | 440 | }; 441 | -------------------------------------------------------------------------------- /2. 设计模式/4.0 单例模式.md: -------------------------------------------------------------------------------- 1 | # 单例模式 2 | 3 | 单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 4 | 5 | 单例模式是一种常见的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的`window`对象等。 6 | 7 | 在JavaScript开发中单例模式的应用也极为广泛,试想一下,当我们单机登录按钮的时候,页面会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗只会被创建一次,那么这个登录浮窗就适合用单例模式创建。 8 | -------------------------------------------------------------------------------- /2. 设计模式/4.1 实现单例模式.md: -------------------------------------------------------------------------------- 1 | # 实现单例模式 2 | 3 | 要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是则在下一次获取该类的实例时,直接返回之前创建的对象。 4 | 5 | var Singleton = function(name){ 6 | this.name = name; 7 | this.instance = null; 8 | }; 9 | 10 | Singleton.prototype.getName = function(){ 11 | console.log(this.name); 12 | }; 13 | 14 | Singleton.getInstance = function(name){ 15 | if(!this.instance){ 16 | this.instance = new Singleton(name); 17 | } 18 | return this.instance; 19 | }; 20 | 21 | var a = Singleton.getInstance('test'); 22 | var b = Singleton.getInstance('test'); 23 | console.log(a === b); //true 24 | 25 | 我们通过`Singleton.getInstance`来获取Singleton类的唯一实例对象,这种方法相对简单,但是有一个问题,就是增加了这个类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,和以往通过`new XXX`等方式获取对象方式不同,这里要用`Singleton.getInstance`来获取对象。 26 | -------------------------------------------------------------------------------- /2. 设计模式/4.2 透明的单例模式.md: -------------------------------------------------------------------------------- 1 | # 透明的单例模式 2 | 3 | 我们现在的目标是实现一个透明的单例类,用户从这个类中创建对象的时候,可以像使用其它任何普通类一样。 4 | 5 | 在下面例子中使用,我们使用`createDiv`单例类,它的作用是负责在页面中创建唯一的div节点 6 | 7 | var CreateDiv = (funcrion(){ 8 | 9 | var instance = null; 10 | 11 | var CreateDiv = function(html){ 12 | if(instance){ 13 | return instance; 14 | } 15 | 16 | this.html = html; 17 | this.init(); 18 | 19 | return instance = this; 20 | }; 21 | 22 | CreateDiv.prototype.init = function(){ 23 | var div = document.createElement('div'); 24 | div.innerHTML = this.html; 25 | document.body.appendChild(div); 26 | }; 27 | 28 | return CreateDiv; 29 | 30 | })(); 31 | 32 | 在这段代码中`CreateDiv`的构造函数实际上负责了两件事情 33 | 34 | 1. 创建对象和执行初始化`init`方法 35 | 2. 保证只有一个对象 36 | 37 | 了解“单一职责原则”的同学可以轻易看出问题,构造函数有两个职责。 38 | 39 | 假设某天要利用这个类,在页面中创建多个div,也就是让这个类从单例类变成普通可以生产多个实例的类,那我们必须得重写构造函数,把控制创建唯一对象的那一段去掉,这种修改可能给我们带来意外的麻烦。 40 | -------------------------------------------------------------------------------- /2. 设计模式/4.3 用代理实现单例模式.md: -------------------------------------------------------------------------------- 1 | # 用代理实现单例模式 2 | 3 | 现在我们通过引入代理类的方式来解决构造函数职责不单一的问题 4 | 5 | 首先在`CreateDiv`构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类 6 | 7 | var CreateDiv = function(html){ 8 | this.html = html; 9 | this.init(); 10 | }; 11 | 12 | CreateDiv.prototype.init = function(){ 13 | var div = document.createElement('div'); 14 | div.innerHTML = this.html; 15 | document.body.appendChild(div); 16 | }; 17 | 18 | 接下来引入代理类`ProxySingletonCreateDiv` 19 | 20 | var ProxySingletonCreateDiv = ( 21 | var instance; 22 | 23 | return function(html){ 24 | if(!instance){ 25 | instance = new CreateDiv(html); 26 | } 27 | 28 | return instance; 29 | }; 30 | )(); 31 | 32 | var a = new ProxySingletonCreateDiv('test'); 33 | var b = new ProxySingletonCreateDiv('test'); 34 | console.log(a === b); //true 35 | 36 | 通过引入代理类的方式,同样完成了单例模式的编写,跟之前不同的是,现在把负责管理单例的逻辑移到了代理类`ProxySingletonCreateDiv`中。 37 | 38 | 这样一来`CreateDiv`就变成了一个普通的类,和`ProxySingletonCreateDiv`组合起来可以达到单例模式的效果。 39 | -------------------------------------------------------------------------------- /2. 设计模式/4.4 JavaScript中的单例模式.md: -------------------------------------------------------------------------------- 1 | # JavaScript中的单例模式 2 | 3 | 前面提到的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在Java中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来。 4 | 5 | 但JavaScript其实是一门无类(class-free)语言,正是因为此,生硬的移植单例模式的概念意义不大。在JavaScript中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要先为它创建一个类呢? 6 | 7 | 单例模式的核心是*确保只有一个实例,并且提供全局访问* 8 | 9 | 全局变量不是单例模式,但在JavaScript开发中,我们经常把全局变量当做单例开始用 10 | 11 | var a = {}; 12 | 13 | 当用这种方式创建对象`a`时,对象是独一无二的。如果变量被声明在全局作用域下,我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的,这样就满足了单例模式的两个条件。 14 | 15 | 但是全局变量存在很多问题,他很容易造成命名空间污染,在大型项目中如果不加以限制和管理,程序中可能存在很多这样的变量。JavaScript的变量叶很容易被不小心覆盖。 16 | 17 | Douglas Crockford多次把全局变量称为JavaScript最糟糕的特性。作为普通开发者我们有必要减少全局变量的使用,即时需要也要把他的污染降到最低。 18 | 19 | ## 使用命名空间 20 | 21 | 适当的使用命名空间并不会杜绝全局变量,但可以减少全局变量的数量。 22 | 23 | 最简单的方法依然是用对象字面量方式 24 | 25 | var namespace1 = { 26 | a: function(){}, 27 | b: function(){} 28 | }; 29 | 30 | ## 使用闭包封存私有变量 31 | 32 | 这种方法把一些变量封装在闭包内部,只暴露一些接口和外界通信 33 | 34 | var users = (function(){ 35 | var _name = 'Test', 36 | _age = 20; 37 | 38 | return { 39 | getUserInfo: function(){ 40 | return _name + '-' + _age; 41 | } 42 | }; 43 | })(); 44 | -------------------------------------------------------------------------------- /2. 设计模式/4.6 惰性单例.md: -------------------------------------------------------------------------------- 1 | # 惰性单例 2 | 3 | 惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实际开发中非常有用,前面我们已经使用过 4 | 5 | Singleton.getInstance = (function(){ 6 | var instance = null; 7 | 8 | return function(name){ 9 | if(!instance){ 10 | instance = new Singleton(name); 11 | } 12 | return instance; 13 | }; 14 | })(); 15 | 16 | 不过这是基于“类”的单例模式,前面说过基于类的单例模式在JavaScript中并不适用,我们以一个登录浮窗为例,介绍与全局变量结合实现的惰性单例。 17 | 18 | ## 方案一 19 | 20 | 在页面加载完成时创建好浮窗的DOM,然后设置隐藏状态,当用户点击登录的时候出现 21 | 22 | 这种方式有一个问题,用户如果没有点击登录按钮,这些操作就多余了 23 | 24 | ## 方案二 25 | 26 | 用户点击的时候判断浮窗是否存在,存在则创建,否则直接显示,我们很容易写成这样的代码 27 | 28 | var CreateDialog = (function(){ 29 | var dialog; 30 | 31 | return function(){ 32 | if(!dialog){ 33 | // do something to create dialog DOM 34 | } 35 | 36 | return dialog; 37 | }; 38 | })(); 39 | 40 | ## 通用方案 41 | 42 | 上面代码看起来可以工作,但是有两个问题 43 | 44 | 1. 违反单一职责原则,创建对象和管理单例逻辑杂糅 45 | 2. 稍微修改需求,整个函数都要动,当然这也是违反单一职责原则的副作用 46 | 47 | 我们需要把不变的部分——管理单例逻辑分离出来,用一个变量来标志是否创建过对象,如果是则在下次直接返回这个已经创建好了的对象 48 | 49 | var obj; 50 | if(!obj){ 51 | obj = xxx; 52 | } 53 | 54 | 现在我们我们就把如果管理单例的逻辑从原来的代码中抽离出来,这些逻辑封装在`getSingle`函数内部,创建对象的方法`fn`被当做参数动态传入 55 | 56 | var getSingle = function(fn)( 57 | var result; 58 | 59 | return function(){ 60 | if(!result){ 61 | result = fn.apply(this, arguments); 62 | } 63 | 64 | return result; 65 | }; 66 | ); 67 | 68 | 这样我们写一个创建浮窗的方法`CreateDialog` 69 | 70 | var CreateDialog = function(){ 71 | // do something to create dialog DOM 72 | }; 73 | 74 | var CreateSingletonDialog = getSingle(CreateDialog); 75 | var dialog = CreateSingletonDialog(); 76 | 77 | 在这个例子中我们把创建实例对象的职责和管理单例的职责分别放置在两个方法中,这两个方法可以独立互不影响,当它们组合使用的时候,就完成了单例模式。 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /2. 设计模式/5.0 策略模式.md: -------------------------------------------------------------------------------- 1 | # 策略模式 2 | 3 | 在程序设计中我们经常遇到要实现某一种功能有多中方案的选择,比如一个压缩文件的程序,即可以使用gzip算法,也可以选择zip算法。 4 | 5 | 这些算法灵活多样,而且可以随意替换,这种问题的解决方案就是策略模式。 6 | 7 | 策略模式的定义是:定义一系列的算法,把它们封装起来,并且使它们可以互相替换。 8 | -------------------------------------------------------------------------------- /2. 设计模式/5.1 使用策略模式计算奖金.md: -------------------------------------------------------------------------------- 1 | # 使用策略模式计算奖金 2 | 3 | 策略模式有着广泛的应用,本节以年终奖计算为例进行介绍 4 | 5 | 很多公司的年终奖是根据员工的工资系数和年底绩效情况来发放的。例如绩效为S的员工有4倍工资,绩效为A的员工有3倍工资,而绩效为B的员工有2倍工资。 6 | 7 | ## 最初的代码实现 8 | 9 | 我们可以编写一个`calculateBonus`的函数计算每个人的奖金数额。函数需要两个参数 10 | 11 | 1. 员工的工资数额 12 | 2. 绩效考核等级 13 | 14 | 15 | 16 | var calculateBonus = function(salary, performanceLevel){ 17 | 18 | if(performanceLevel === 'S'){ 19 | return salary * 4; 20 | } 21 | 22 | if(performanceLevel === 'A'){ 23 | return salary * 3; 24 | } 25 | 26 | if(performanceLevel === 'B'){ 27 | return salary * 2; 28 | } 29 | 30 | }; 31 | 32 | 这段代码非常简单,但是有明显缺陷 33 | 1. 函数体积庞大,包含了很多`if-else`语句,这些语句需要覆盖所有逻辑分支 34 | 2. 函数缺乏弹性,如果增加了一种等级或者等级对应工资计算方式发生变化,需要修改函数内部实现,违反**开放-封闭**原则 35 | 3. 工资算法复用性差,只能靠复制粘贴 36 | 37 | ## 使用组合函数重构代码 38 | 39 | 一般很容易想到的方式是使用组合函数来重构代码,我们把各种算法封装到对应函数里面,函数保持良好命名习惯,可以复用 40 | 41 | var performanceS =function(salary){ 42 | return salary * 4; 43 | }; 44 | 45 | var performanceA =function(salary){ 46 | return salary * 3; 47 | }; 48 | 49 | var performanceB =function(salary){ 50 | return salary * 2; 51 | }; 52 | 53 | var calculateBonus = function(salary, performanceLevel){ 54 | 55 | if(performanceLevel === 'S'){ 56 | return performanceS(salary); 57 | } 58 | 59 | if(performanceLevel === 'A'){ 60 | return performanceA(salary); 61 | } 62 | 63 | if(performanceLevel === 'B'){ 64 | return performanceB(salary); 65 | } 66 | 67 | }; 68 | 69 | 目前程序得到了一定程度上的改善,但这种改善十分有限,已然没有解决最重要的问题:函数体积可能无限膨胀,而且在系统变化的时候缺乏弹性 70 | 71 | ## 使用策略模式 72 | 73 | 策略模式是指定义一系列的算法,把它们封装起来。将不变的部分和变化的部分隔离是每个设计模式的主题,策略模式目的是将算法的使用和算法的实现隔离开。 74 | 75 | 在这个例子中,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而没中等级都有不同的计算规则。 76 | 77 | 一个基于策略模式嗯程序至少有两部分组成 78 | 1. 一组策略类:策略类封装了具体的算法,并负责具体的计算过程 79 | 2. 环境类Context:Context类接受客户请求,随后把请求委托给某个策略类 80 | 81 | 82 | 83 | var performanceS = function(){}; 84 | 85 | performanceS.prototype.calculate = function(salary){ 86 | return salary * 4; 87 | }; 88 | 89 | var performanceA = function(){}; 90 | 91 | performanceA.prototype.calculate = function(salary){ 92 | return salary * 3; 93 | }; 94 | 95 | var performanceB = function(){}; 96 | 97 | performanceB.prototype.calculate = function(salary){ 98 | return salary * 2; 99 | }; 100 | 101 | var Bonus = function(){ 102 | this.salary = null; 103 | this.strategy = null; 104 | }; 105 | 106 | Bonus.prototype.setSalary = function(salary){ 107 | this.salary = salary; 108 | }; 109 | 110 | Bonus.prototype.setStrategy = function(strategy){ 111 | this.strategy = strategy; 112 | }; 113 | 114 | Bonus.prototype.getBonus = function(){ 115 | return this.strategy.calculate(this.salary); 116 | }; 117 | 118 | 在完成最终的代码之前,我们再来回顾一下策略模式的思想 119 | 120 | >定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换 121 | 122 | 这句话如果说的更详细一些:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法中。客户端对Context发起请求的时候,Context总是把这些请求这些策略对象中的一个处理。 123 | 124 | 现在完成剩下的代码 125 | 126 | var bonus = new Bonus(); 127 | bonus.setSalary(10000); 128 | 129 | bonus.setStrategy(new PerformanceS()); 130 | console.log(bouns.getBonus()); // 40000 131 | 132 | bonus.setStrategy(new PerformanceA()); 133 | console.log(bouns.getBonus()); // 30000 134 | 135 | 通过策略模式代码变得更加清晰,各个类嗯职责也更鲜明。 136 | 137 | 138 | -------------------------------------------------------------------------------- /2. 设计模式/5.2 JavaScript版本的策略模式.md: -------------------------------------------------------------------------------- 1 | # JavaScript 版本的策略模式 2 | 3 | 之前strategy对象从各个策略类中创建而来,这是模拟一些传统面向对象语言的实现,在JavaScript中函数也是对象,所以更加简单和直白的做法是把strategy直接定义为函数 4 | 5 | var strategies = { 6 | "S": function(salary){ 7 | return salary * 4; 8 | }, 9 | "A": function(salary){ 10 | return salary * 3; 11 | }, 12 | "B": function(salary){ 13 | return salary * 2; 14 | } 15 | }; 16 | 17 | 同样Context也没必要用Bonus类来表示,我们依然使用`calculateBonus`函数充当Context来接受用户的请求。 18 | 19 | var calculateBonus = function(level, salary){ 20 | return strategies[level](salary); 21 | }; 22 | 23 | console.log(calculateBonus('S', 20000)); 24 | console.log(calculateBonus('A', 10000)); 25 | -------------------------------------------------------------------------------- /2. 设计模式/5.3 策略模式的思考.md: -------------------------------------------------------------------------------- 1 | # 策略模式的思考 2 | 3 | ## 多态在策略模式中的体现 4 | 5 | 通过使用策略模式重构代码,我们消除了程序中大片的天剑分支语句,所有和计算奖金有关的逻辑不再放在Context中,而是分布在哥哥策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象,每个策略对象负责的算法已被各自封装在对象内部。 6 | 7 | 当我们对策略对象发出*计算奖金*的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以互相替换”的目的。替换Context中当前保存的策略对象,便能执行不同的算法,来得到我们想要的结果。 8 | 9 | ## 策略模式的优缺点 10 | 11 | 策略模式是一种常用且有效的设计模式,有几个明显的优点 12 | 13 | 1. 策略模式李勇组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句 14 | 2. 策略模式提供了对*开放-封闭*原则的支持,将算法封装在独立的strategy中,使得它们方便切换、理解、拓展 15 | 3. 策略模式的算法可以复用在系统其它地方 16 | 4. 策略模式李勇组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案 17 | 18 | 当然策略模式也有缺点 19 | 20 | 1. 使用策略模式会在程序中增加很多策略类或者策略对象 21 | 2. 要使用策略类,必须向客户端暴露所有实现,违反最小知识原则 22 | 23 | ## 一等函数对象与策略模式 24 | 25 | 在以类为中心的传统面向对象语言中,不同的算法或行为被封装在各个策略类中,Context将请求委托给这些策略对象,这些策略对象会根据请求返回不同的执行结果,这样能表现出对象的多态性 26 | 27 | 但在函数作为一等对象的语言中,策略模式是隐形的,strategy就是值为函数的变量。在JavaScript中除了使用类来封装算法和行为外,使用函数当然也是一种选择,这些算法可以封装到函数中并且四处传播,也就是我们常说的“高阶函数”。 28 | 29 | 实际上在JavaScript这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。当我们对这些函数发出*调用*的消息时,不同的函数会返回不同的结果。在JavaScript中“函数对象的多态性”来的更加简单。 30 | 31 | var S = function(salary){ 32 | return salary * 4; 33 | }; 34 | 35 | var A = function(salary){ 36 | return salary * 3; 37 | }; 38 | 39 | var B = function(salary){ 40 | return salary * 2; 41 | }; 42 | 43 | var calculateBonus = function(func, salary){ 44 | return func(salary); 45 | }; 46 | 47 | console.log(calculateBonus(S, 20000)); 48 | 49 | -------------------------------------------------------------------------------- /2. 设计模式/6.0 代理模式.md: -------------------------------------------------------------------------------- 1 | # 代理模式 2 | 3 | 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。 4 | 5 | 代理模式的定义:为其它对象提供一种代理以控制对这个对象的访问。在某些情况下一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。 6 | 7 | 代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。明星都有经纪人做代理,如果想请明星来办一场商业演出,只能联系他的经纪人,经纪人把细节和报酬谈好之后,再把合同交给明星签。 8 | 9 | 代理模式的关键是当客户端不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户端实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。 10 | 11 | ## 举个例子 12 | 13 | 从一个小例子了解代理模式的结构 14 | 15 | 小明喜欢上女孩A,决定送A一束花表白,刚好打听到A和他有个共同的好友B,于是内向的小明决定让B来代替自己完成送花这件事情 16 | 17 | var Flower = function(){} 18 | 19 | var A = { 20 | receiveFlower: function(flower){ 21 | console.log('收到花 ' + flower); 22 | } 23 | }; 24 | 25 | var B = { 26 | receiveFlower: function(flower){ 27 | A.receiveFlower(flower); 28 | } 29 | }; 30 | 31 | var XM = { 32 | sendFlower: function(target){ 33 | var flower = new Flower(); 34 | target.reveiveFlower(flower); 35 | } 36 | }; 37 | 38 | XM.sendFlower(B); 39 | 40 | 至此我们完成了一个最简单的代理模式,看起来代理模式只是把送花复杂化了,没什么用处。我们改变一下故事背景 41 | 42 | 假设当A在心情好的时候收到花,小明表白成功率有60%,当A心情差的时候收到花,成功率无限趋近0。小明和A只认识了两天,无法分辨A什么时候心情好,不合时宜的送出花可能被直接扔掉,但这是吃7天泡面换来的。但是B却非常了解A,所以小明只管把花给B,B来监听A心情变化,择机把花送出去。 43 | 44 | var Flower = function(){} 45 | 46 | var A = { 47 | listenGoodMood: function(fn){ 48 | setTimeout(fn, Math.rand()); 49 | }, 50 | receiveFlower: function(flower){ 51 | console.log('收到花 ' + flower); 52 | } 53 | }; 54 | 55 | var B = { 56 | receiveFlower: function(flower){ 57 | A.listenGoodMood(functin(){ // 监听A的好心情 58 | A.receiveFlower(flower); 59 | }); 60 | } 61 | }; 62 | 63 | var XM = { 64 | sendFlower: function(target){ 65 | var flower = new Flower(); 66 | target.reveiveFlower(flower); 67 | } 68 | }; 69 | 70 | XM.sendFlower(B); 71 | 72 | 这个例子中代理的作用就显而易见了,这也是绝大多数男孩追女孩的秘诀——搞定闺密 73 | 74 | ## 保护代理和虚拟代理 75 | 76 | 虽然上面只是个例子,但是我们可以从例子中找到两种代理的身影 77 | 78 | 代理B可以帮助A过滤掉一些请求,比如送花的人太猥琐,这种请求代理B可以直接拒绝,这种代理叫做**保护代理**。A和B一个充当白脸,一个充当黑脸。白脸A可以保持良好的女神形象,不直接拒掉任何人,于是找来黑脸B来控制对A的访问。 79 | 80 | 另外在花的价格不菲,导致`new Flower()`是一个非常昂贵的操作,那么我们可以把这个操作交给B去执行,代理人B会根据A心情好的时候再去执行,这是代理的另一种模式**虚拟代理**。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建 81 | 82 | var B = { 83 | receiveFlower: function(){ 84 | A.listenGoodMood(functin(){ // 监听A的好心情 85 | var flower = new Flower(); 86 | A.receiveFlower(flower); 87 | }); 88 | } 89 | }; 90 | 91 | 保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript中并不容易实现保护代理,因为我们无法判断谁访问不了对象。虚拟代理是最常见的一种代理模式 92 | -------------------------------------------------------------------------------- /2. 设计模式/6.1 图片预加载.md: -------------------------------------------------------------------------------- 1 | # 虚拟代理实现图片预加载 2 | 3 | 在web开发中图片预加载是一种常用技术,如果直接给每个img标签设置src属性,可能会由于图片太大或者网络不佳,图片会出现长时间空白。常见做法是先用一张loading的gif占位,然后一不加载图片,等图片加载好了再填进img中,这种场景就很适合虚拟代理 4 | 5 | 下面来实现一下这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的`setSrc`接口,外界调用这个接口可以给img标签设置src属性 6 | 7 | var myImage = (function(){ 8 | var imgNode = document.createElement('img'); 9 | document.body.appendChild(imgNode); 10 | 11 | return { 12 | setSrc: function(src){ 13 | imgNode.src = src; 14 | } 15 | }; 16 | })(); 17 | 18 | 现在引入代理对象`proxyImage`,通过这个代理对象,图片在未被加载好之前页面出现一个loading的gif,提示用户图片正在加载 19 | 20 | var proxyImage = (function(){ 21 | var img = new Image(); 22 | img.onload = function(){ 23 | myImage.setSrc(this.src); 24 | }; 25 | 26 | return { 27 | setSrc: function(src){ 28 | myImage.setSrc('loading.gif'); 29 | img.src = src; 30 | } 31 | }; 32 | })(); 33 | 34 | proxyImage.setSrc('background.jpg'); 35 | -------------------------------------------------------------------------------- /2. 设计模式/6.2 代理的意义.md: -------------------------------------------------------------------------------- 1 | # 代理的意义 2 | 3 | 也许大家会有疑问,不过实现一个小小的图片预加载功能,即使不引入任何模式也能办到,那么引入代理模式的好处究竟在哪里?我们写一个不用代理的函数实现 4 | 5 | var myInmage = (function(){ 6 | 7 | var imgNode = document.createElement('img'); 8 | document.body.appendChild(imgNode); 9 | 10 | var img = new Image(); 11 | img.onload = function(){ 12 | imgNode.src = img.src; 13 | }; 14 | 15 | return { 16 | setSrc: function(src){ 17 | imageNode.src = 'loading.gif'; 18 | img.src = src; 19 | } 20 | }; 21 | 22 | })(); 23 | 24 | 为了说明代理模式的意义,介绍一下一个面向对象设计的原则——单一职责原则 25 | 26 | 单一职责原则指的是,就一个类(通常叶包括对象和函数)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因将有多个。 27 | 28 | 面向对象设计鼓励将行为分布到细粒度的对象中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计,当变化发生时,设计可能会早到意外的破坏。 29 | 30 | 职责被定义为“引起变化的原因”,上段代码中myImage对象除了负责给image设置src,还要负责预加载图片。我们在处理其中一个职责的时候,很有可能因为其强耦合性影响另外一个职责实现。 31 | 32 | 另外在面向对象程序设计中,大多数情况下,若违反其它任何原则,同时也会违反开放-封闭原则。如果我们只是从网络上获取其它一些很小的图片,我们希望把预加载功能移除,这时候就不得不改动myImage对象了。 33 | 34 | 实际上我们需要的只是给img节点设置src属性,预加载图片只是一个锦上添花的功能,如果能把这个操作放在另外一个对象里,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载完成之后,把请求重新交给本体myImage。 35 | 36 | 纵观整个程序,我们并没有改变或者增加myimage接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放-封闭原则的。 37 | 38 | 给img设置src和图片预加载这两个重要功能被隔绝在两个对象里,它们可以相互变化而不影响对方。 39 | 40 | ## 代理和本体接口的一致性 41 | 42 | 上面提到如果我们不在需要预加载,那么就不在需要代理对象,可以选择直接请求本体,其中一个关键是代理和本体都对外提供了setSrc方法,在客户看来代理对象和本体对象是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处 43 | 44 | 1. 用户可以放心的请求代理,他只关心是否能得到想要的结果。 45 | 2. 在任何使用本体的地方都可以替换成使用代理 46 | 47 | 在Java中代理和本体都需要显示的实现同一个接口,一方面接口保证了它们会拥有同样的方法,另一方面面向接口编程迎合依赖导致原则,通过接口进行向上转型。 48 | 49 | 在JavaScript这类动态语言中,我们有时通过鸭子类型来检测代理和本体是否都实现了`setSrc`方法,另外大多是时候甚至不做检测,全部依赖自觉,这对程序健壮性是有影响的。如果代理对象和本体对象都是一个函数,函数必然能够被执行,则可以认为它们有一致的接口。 50 | -------------------------------------------------------------------------------- /2. 设计模式/7.0 迭代器模式.md: -------------------------------------------------------------------------------- 1 | # 迭代器模式 2 | 3 | 迭代器模式是指提供一种方法来顺序访问一个聚合对象中的各个元素,而又不需要暴露对象内部表示 4 | 5 | 迭代器模式可以把迭代的过程从业务逻辑中剥离出来,在使用迭代器模式之后,及时不关心对象的内部构造,叶可以按顺序访问其中的每个元素 6 | 7 | 目前只有些“古董级”的语言中才会为了实现迭代器模式二烦恼,现在流行的大部分语言如Java、Ruby等都已经有了内置的迭代器实现,JavaScript也有了`Array.prototype.forEach` 8 | 9 | ## jQuery中的迭代器 10 | 11 | 迭代器模式无非就是循环访问聚合对象的各个元素。jQuery中的`$.each`函数就是一个实现,回调函数有两个参数 12 | 13 | 1. i 当前索引 14 | 2. item 当前迭代的元素 15 | 16 | 17 | 18 | $.each([1, 2, 3], function(i, item){ 19 | console.log(i); 20 | console.log(item); 21 | }); 22 | 23 | ## 实现自己的迭代器 24 | 25 | 现在我们自己实现一个each函数,函数接受两个参数 26 | 27 | 1. 被循环的数组 28 | 2. 每次迭代的回调函数 29 | 30 | 31 | 32 | var each = function(ary, callback){ 33 | for(var i = 0, l = ary.length; i < l; i++){ 34 | callback.call(ary[i], i, ary[i]); 35 | } 36 | }; 37 | 38 | each([1, 2, 3], function(i, item){ 39 | console.log(i); 40 | console.log(item); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /2. 设计模式/7.1 内部迭代器和外部迭代器.md: -------------------------------------------------------------------------------- 1 | # 内部迭代器和外部迭代器 2 | 3 | 迭代器可以氛围内部迭代器和外部迭代器 4 | 5 | ## 内部迭代器 6 | 7 | 上面写的each函数是个内部迭代器,函数内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用,不用关心迭代器的内部实现。但这也是内部迭代器的缺点,由于内部迭代器的迭代规则已经定义好,上面的each函数就无法同时迭代两个数组了。 8 | 9 | 比如现在有个需求,要判断两个数组里元素的值是否完全相等,如果不改写each函数本身的代码,我们能够入手的地方似乎只剩下each函数的回调函数了 10 | 11 | var compare = function(ary1, ary2){ 12 | if(ary1.length !== ary2.length) return false; 13 | 14 | each(ary1, function(i, item){ 15 | if(ary2[i] !== item){ 16 | return false; 17 | } 18 | }); 19 | 20 | return true; 21 | }; 22 | 23 | 这个compare函数能够实现主要依赖了JavaScript里可以把函数当做参数传递的特性,在一些没有闭包的语言中,内部迭代器本身的实现叶非常复杂,C语言的内部迭代器是用函数指针实现的,循环处理所需要的数据都要以参数的形式明确地从外部传递进去。 24 | 25 | ## 外部迭代器 26 | 27 | 外部迭代器必须显式的请求下一个元素,增加了一些调用的复杂度,但也增强了迭代器的灵活性,可以控制迭代的过程或者顺序。 28 | 29 | var Iterator = function(obj){ 30 | var current = 0; 31 | 32 | var next = function(){ 33 | current += 1; 34 | }; 35 | 36 | var isDone = function(){ 37 | return current >= obj.length; 38 | }; 39 | 40 | var getCurrent = function(){ 41 | return obj[current]; 42 | }; 43 | 44 | return { 45 | next: next, 46 | isDone: isDone, 47 | getCurrent: getCurrent 48 | }; 49 | }; 50 | 51 | 看看如何实现compare函数 52 | 53 | var compare = function(iterator1, iterator2){ 54 | while(!iterator1.isDone() && !iterator2.isDone()){ 55 | if(iterator1.getCurrent() !== iterator2.getCurrent()){ 56 | return false; 57 | } 58 | 59 | iterator1.next(); 60 | iterator2.next(); 61 | } 62 | 63 | return iterator1.isDone() && iterator2.isDone(); 64 | }; 65 | 66 | var iterator1 = Iterator([1,2]); 67 | var iterator2 = Iterator([1,2,3]); 68 | 69 | console.log(compare(iterator1, iterator2)); 70 | 71 | 外部迭代器虽然调用方式相对复杂,但是适用面更广,也能满足更加多变的需求,内部迭代器和外部迭代器没有优劣之分,究竟使用哪个需要根据具体场景而定。 72 | -------------------------------------------------------------------------------- /2. 设计模式/7.2 其它迭代器.md: -------------------------------------------------------------------------------- 1 | # 其它迭代器 2 | 3 | ## 迭代类数组对象和字面量对象 4 | 5 | 迭代器不仅可以迭代数组,也可以迭代一些类数组对象。通过之前代码可以发现无论是内部迭代器还是外部迭代器,只要被迭代的聚合对象拥有length属性并且可以被下标访问,那它就可以被迭代。 6 | 7 | 在JavaScript中`for...in`语句可以用来迭代普通字面量对象的属性,jQuery中提供了`$.each`函数来封装各种迭代行为 8 | 9 | $.each = function(obj, callback){ 10 | var value, 11 | i = 0, 12 | length = obj.length, 13 | isArray = isArrayLike(obj); 14 | 15 | if(isArray){ // 迭代类数组 16 | for(; i < length; i++){ 17 | value = callback.call(obj[i], i, obj[i]); 18 | 19 | if(value === false) break; 20 | } 21 | }else{ 22 | for(i in obj){ // 迭代object对象 23 | value = callback.call(obj[i], i, obj[i]); 24 | 25 | if(value === false) break; 26 | } 27 | } 28 | 29 | return obj; 30 | }; 31 | 32 | ## 倒序迭代器 33 | 34 | 由于GoF对迭代器模式的定义非常松散,所以我们可以用那个多种多样的迭代器实现。总的来说,迭代器模式提供了循环访问一个聚合对象中每个元素的方法,但它没有规定我们以顺序、倒序还是中序来遍历聚合对象 35 | 36 | 我们实现一个倒序访问的迭代器 37 | 38 | var reverseEach = function(ary, callback){ 39 | for(var l = ary.length - 1; l >= 0; l--){ 40 | callback.call(ary[l], l, ary[l]); 41 | } 42 | }; 43 | 44 | ## 中止迭代器 45 | 46 | 迭代器可以像普通的for循环中的break一样,提供一种跳出循环的方法,jQuery的`$.each`中有一句 47 | 48 | if(value === false){ 49 | break; 50 | } 51 | 52 | 这句代码的意思是,约定如果迭代器回调函数返回`false`,则提前中止循环 53 | 54 | var each = function(ary, callback){ 55 | for(var i = 0, len = art.length; i < len; i++){ 56 | if(callback.call(ary[i], i, ary[i]) === false){ 57 | break; 58 | } 59 | } 60 | }; 61 | 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 设计模式学习总结 2 | 3 | 看了曾探的《[JavaScript设计模式与开发实践](http://book.douban.com/subject/26382780/)》,之前模模糊糊有些感觉的东西忽然清晰了很多,趁热打铁总结一下 4 | 5 | **主要包括三个部分** 6 | 7 | 1. 基础知识 8 | * 面向对象的JavaScript 9 | * this、call、apply 10 | * 闭包和高阶函数 11 | 12 | 2. 设计模式 13 | * 单例模式 14 | * 策略模式 15 | * 代理模式 16 | * 迭代器模式 17 | * 观察者模式 18 | * 命令模式 19 | * 组合模式 20 | * 模板方法模式 21 | * 享元模式 22 | * 职责链模式 23 | * 中介者模式 24 | * 装饰者模式 25 | * 状态模式 26 | * 适配器模式 27 | 28 | 3. 设计原则和编程技巧 29 | * 单一职责原则 30 | * 最少知识原则 31 | * 开放封闭原则 32 | * 接口和面向接口编程 33 | * 代码重构 34 | 35 | 主要都是对书中代码验证和知识理解,可以说就是笔记 36 | --------------------------------------------------------------------------------