第一章:面向对象的 JavaScript
1.1 动态类型语言和鸭子类型
静态类型语言的优点首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。 其次,如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。
静态类型语言的缺点:
-
首先是迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底只是辅助我们编写可靠性高程序的一种手段,而不是编写程序的目的,毕竟大部分人编写程序的目的是为了完成需求交付生产。
-
其次,类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。
动态类型语言的优点:
- 编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序是越有帮助的。
动态类型语言的缺点:
- 无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。
鸭子类型的通俗说法是:“如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。”
鸭子类型指导我们只关注对象的行为,而不关注对象本身,也就是关注 HAS-A,而不是 IS-A。
在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。
利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程”。
例如,一个对象若有 push 和 pop 方法,并且这些方法提供了正确的实现,它就可以被当作栈来使用。
一个对象如果有 length 属性,也可以依照下标来存取属性(最好还要拥有 slice 和 splice 等方法),这个对象就可以被当作数组来使用。
“面向接口编程”是设计模式中最重要的思想。
在 JavaScript 语言中,“面向接口编程”的过程跟主流的静态类型语言不一样。 因此,在 JavaScript 中实现设计模式的过程与在一些我们熟悉的语言中实现的过程会大相径庭。
1.2 多态
“多态”一词源于希腊文 polymorphism,拆开来看是 poly(复数)+ morph(形态)+ ism,从字面上我们可以理解为复数形态。
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
多态背后的思想是将 “做什么” 和 “谁去做,以及具体怎么做” 分离开来。
动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。
JavaScript 是一门不必进行类型检查的动态类型语言,为了真正了解多态的目的,我们需要转一个弯,从一门静态类型语言说起。
某些时候,在享受静态语言类型检查带来的安全性的同时,我们亦会感觉被束缚住了手脚。
为了解决这一问题,静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。
多态性的表现正是实现众多设计模式的目标。
使用继承来得到多态效果,是让对象表现出多态性的最常用手段。
继承通常包括实现继承和接口继承。
多态的思想实际上是把 “做什么” 和 “谁去做” 分离开来,要实现这一点,归根结底先要消除类型之间的耦合关系。
一个 JavaScript 对象,既可以表示 Duck 类型的对象,又可以表示 Chicken 类型的对象,这意味着 JavaScript 对象的多态性是与生俱来的。
某一种动物能否发出叫声,只取决于它有没有 makeSound 方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的“类型耦合”。
在 JavaScript 中,并不需要诸如向上转型之类的技术来取得多态的效果。
多态的最根本好处在于,你 不必再向对象询问 “你是什么类型” 而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。
多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
每个对象应该做什么,已经成为了该对象的一个方法,被安装在对象的内部,每个对象负责它们自己的行为。 所以这些对象可以根据同一个消息,有条不紊地分别进行各自的工作。
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
在 JavaScript 这种将函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够被四处传递。
当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这是“多态性”的一种体现,也是很多设计模式在 JavaScript 中可以用高阶函数来代替实现的原因。
1.3 封装
封装的目的是将信息隐藏。
这一节将讨论更广义的封装,不仅包括封装数据和封装实现,还包括封装类型和封装变化。
在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private、public、protected 等关键字来提供不同的访问权限。
除了 ECMAScript 6 中提供的 let 之外,一般我们通过函数来创建作用域。
在 ECMAScript 6 中,还可以通过 Symbol 创建私有属性。
有时候我们喜欢把封装等同于封装数据,但这是一种比较狭义的定义。
封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。 当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。
迭代器的作用是在不暴露一个聚合对象的内部表示的前提下,提供一种方式来顺序访问这个聚合对象。
一般而言,封装类型是通过抽象类和接口来进行的。
把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。
在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。 比如工厂方法模式、组合模式等。
对于 JavaScript 的设计模式实现来说,不区分类型是一种失色,也可以说是一种解脱。
从设计模式的角度出发,封装在更重要的层面体现为封装变化。
“找到变化并封装之”。
这 23 种设计模式分别被划分为 创建型模式、结构型模式 和 行为型模式。
- 创建型模式的目的就是 封装创建对象的变化。
- 结构型模式封装的是 对象之间的组合关系。
- 行为型模式封装的是 对象的行为变化。