JavaScript——面向对象设计部分总结
关于面向对象设计的相关总结
多态
同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
案例:
向不同动物发出‘叫声’的命令
var makeSound = function(animal) {
if (animal instanceof Duck) {
console.log('嘎嘎嘎');
} else if (animal instanceof Chicken) {
console.log('咯咯咯');
}
};
var Duck = function() {};
var Chicken = function() {};
makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯
多态背后的思想是将做什么和谁去做以及怎样去做分离开来,也就是将不变的事物与 可能改变的事物分离开来。在这个故事中,动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放—封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。
类型检查和多态
类型检查是在表现出对象多态性之前的一个绕不开的话题,但 JavaScript 是一门不必进行类型检查的动态类型语言。
静态类型语言的多态
静态类型语言,是需要指定变量的类型,并在编译时检测数据类型是否匹配,这样做有安全优势,但是也会被数据类型给束缚绑定。如下:
案例-JAVA版本
public class Duck { // 鸭子类
public void makeSound(){
System.out.println( "嘎嘎嘎" );
}
}
public class Chicken { // 鸡类
public void makeSound(){
System.out.println( "咯咯咯" );
}
}
public class AnimalSound {
// AnimalSound 已绑定Duck 鸭子类
public void makeSound( Duck duck ){ // (1)
duck.makeSound();
}
}
public class Test {
public static void main( String args[] ){
AnimalSound animalSound = new AnimalSound();
Duck duck = new Duck();
animalSound.makeSound( duck ); // 输出:嘎嘎嘎
}
}
案例中AnimalSound 已经绑定特定数据类型,无法实现多态。
为了解决这一问题,静态类型的面向对象语言通常被设计为可以向上转型(利用继承,抽象,和接口):当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。当 Duck 对象和 Chicken 对象的类型都被隐藏在超类型 Animal 身后, Duck 对象和 Chicken对象就能被交换使用,这是让对象表现出多态性的必经之路,而多态性的表现正是实现众多设计模式的目标。
使用继承来得到多态效果,是让对象表现出多态性的最常用手段。继承通常包括实现继承和接口继承。
JavaScript的多态
多态的思想实际上是把做什么和谁去做分离开来,要实现这一点,归根结底先要消除类型之间的耦合关系。如果类型之间的耦合关系没有被消除,那么我们在 makeSound 方法中指定了发出叫声的对象是某个类型,它就不可能再被替换为另外一个类型。在 Java 中,可以通过向上转型来实现多态。
而 JavaScript 的变量类型在运行期是可变的。一个 JavaScript 对象,既可以表示 Duck 类型的对象,又可以表示 Chicken 类型的对象,这意味着 JavaScript 对象的多态性是与生俱来的。
由此可见,某一种动物能否发出叫声,只取决于它有没有 makeSound 方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的“类型耦合”。这正是我们利用的鸭子类型的道理。JavaScript 中,并不需要诸如向上转型之类的技术来取得多态的效果。
鸭子类型的通俗说法是: “如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。 ”;不关心实际的数据类型,而是看该对象是否有相应的方法(会叫,走起来像鸭子),如果有该对象就是一个相应的数据类型(就是一个鸭子);
如:JavaScript函数中的argument对象,有length属性,可以依照下标来存取属性这个对象就可以被当作数组来使用。
在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程”。
在静态类型语言中,要实现“面向接口编程”并不是一件容易的事情,往往要通过抽象类或者接口等将对象进行向上转型。当对象的真正类型被隐藏在它的超类型身后,这些对象才能在类型检查系统的“监视”之下互相被替换使用。只有当对象能够被互相替换使用,才能体现出对象多态性的价值。
封装
封装的目的是将信息隐藏。一般而言,我们讨论的封装是封装数据和封装实现,这里将说明更广义的封装,不仅包括封装数据和封装实现,还包括封装类型和封装变化。
封装数据
在许多语言(如JAVA)的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private、public、 protected 等关键字来提供不同的访问权限。
但 JavaScript 并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 public 和 private 这两种封装性。
封装实现
封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。
封装类型
封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。比如工厂方法模式、组合模式等。
当然在 JavaScript 中,并没有对抽象类和接口的支持。 JavaScript 本身也是一门类型模糊的语言。在封装类型方面, JavaScript 没有能力,也没有必要做得更多。对于 JavaScript 的设计模式实现来说,不区分类型是一种失色,也可以说是一种解脱。
封装变化
从设计模式的角度出发,封装在更重要的层面体现为封装变化。
设计模式分别被划分为创建型模式、结构型模式和行为型模式。
拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。
通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。
闭包
在 JavaScript 版本的设计模式中,许多模式都可以用闭包和高阶函数来实现。
过程与数据的结合是形容面向对象中的**“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据**。通常用面向对象思想能实现的功能,用闭包也能实现。反之亦然。
闭包功能小结(不一定全面,后期会增补):
- 封装变量:及私有化变量,对外提供统一的访问接口
- 延长局部变量周期:被闭包引用的变量,会长期存在于变量环境,而一直存在(这也是闭包可能会造成内存溢出风险所在)
高阶函数
高阶函数是指至少满足下列条件之一的函数。
- 函数可以作为参数被传递
- 函数可以作为返回值输出
作为参数应用
把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。
栗子:
-
作为回调函数(委托执行):如在 ajax 异步请求的应用中,回调函数的使用非常频繁
注意:回调不光只有异步,也有同步,回调只是延迟了函数作用时间而已,封装了动态变化的业务逻辑部分; -
Array.prototype.sort 接受一个函数当作参数,这个函数里面封装了数组元素的排序规则。
我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分 。
函数作为返回值输出
相比把函数当作参数传递,函数当作返回值输出的应用场景也许更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。
高阶函数的应用
实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过**“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模的纯净和高内聚性**,其次是可以很方便地复用日志统计等功能模块。
在 Java 语言中,可以通过反射和动态代理机制来实现 AOP 技术。而在 JavaScript 这种动态语言中, AOP 的实现更加简单,这是 JavaScript 与生俱来的能力。
通常,在 JavaScript 中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多。
currying(函数柯里化)
currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
uncurrying(函数柯里化)
它解决的问题是让对象去借用一个原本不属于自己的方法。在JS的语言环境中,我们可以通过call和apply完成this的转化,同样,用uncurrying可以解决this的转化问题。
栗子:
(function(){
Array.prototype.push.call( arguments, 4 ); // arguments 借用 Array.prototype.push 方法
console.log( arguments ); // 输出: [1, 2, 3, 4]
})( 1, 2, 3 );
在我们的预期中, Array.prototype 上的方法原本只能用来操作 array 对象。 但用 call 和 apply可以把任意对象当作 this 传入某个方法,这样一来,方法中用到 this 的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。