动态类型语言
编程语言按数据类型大体可以分为两类:静态类型语言与动态类型语言。
而JavaScript是一门典型的动态类型语言。
动态类型语言对变量类型的宽容使得编程变得很灵活。由于不用进行类型检测,我们可以调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。而这是建立在鸭子类型的概念上。
鸭子类型
鸭子类型通俗的说法是:
鸭子类型指导我们只关注对象的行为,而不关注对象本身。
在动态类型语言的面向对象设计中,利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程”。例如:
一个对象若有push和pop方法,并且这些方法提供了正确的实现,它就可以被当作栈来使用;
一个对象若有length属性,且可以依照下标来存取属性,这个对象就可以被当作数组来使用。
多态
多态的含义
对象的多态性
我们说的多态性,其实就是对象的多态性,那么,对象的多态性是怎样的?如何让对象表现出多态性?
对象多态性的一个简单的例子:
// 让动物发声
var makeSound = function(animal){
animal.sound();
}
// 鸭子的叫声
var Duck = function(){};
Duck.prototype.sound = function(){
console.log('嘎嘎嘎');
};
// 小鸡的叫声
var Chicken = function(){};
Chicken.prototype.sound = function(){
console.log('咯咯咯');
}
// 让鸭子发声
makeSound(new Duck());
// 让小鸡发声
makeSound(new Chicken());
// 如果像让小狗发声,只需要简单地追加类似的代码
var Dog = function(){};
Dog.prototype.sound = function(){
console.log('汪汪汪');
}
makeSound(new Dog());
类型检查
静态类型语言(例如Java)在编译时会进行类型匹配检查,这种检查在带来安全性的同时,让代码显得僵硬。因此,静态类型语言通常被设计为可以向上转型:
就像我们在描述“一只麻雀在飞”,“一只喜鹊在飞”时,如果想忽略他们的具体类型,可以说成“一只鸟在飞”,这时“鸟”就是“麻雀”和“喜鹊”的超类。
而JavaScript是一门不必进行类型检查的动态类型语言。
多态的作用
多态是面向对象编程语言中最重要的技术。
多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。有一个例子可以很好地诠释:
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
多态与设计模式
从面向对象设计的角度出发,通过对封装、继承、多态、组合等技术的反复使用,提炼出一些可重复使用的面向对象设计技巧,我们将其归纳为设计模式。而多态在其中是重中之重,绝大多部分设计模式的实现都离不开多态性的思想。例如:
命令模式
组合模式
策略模式
...
Javascript将函数作为一等对象,所以函数本身也是对象,函数用来封装行为并且能够四处传递。当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这是多态性的一种体现。
封装
封装的目的是将信息隐藏。封装包括:
封装数据
封装实现
封装类型
封装变化
封装数据
在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言可能提供了private、public、protected等关键字来提供不同的访问权限。但JavaScript并没有提供对这些关键字的支持,只能依赖变量的作用域来实现封装特性,而且只能模拟出public和private这两种封装性。
一般我们通过函数来创建作用域:
var myObject = (function(){
var __name = 'sven'; //私有(private)变量
return {
getName:function(){ //公开(public)方法
return __name;
}
}
})();
console.log(myObject.getName()); //输出:sven
console.log(myObject.__name); //输出:undefined
封装实现
从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的(即不可见)。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。对象使得对象之间的耦合变得松散,对象之间只通过暴露的API接口来通信。
封装实现细节的例子非常多,例如迭代器。迭代器的作用是在不暴露一个聚合对象的内部表示的前提下,提供一种方式来顺序访问这个聚合对象。如一个each函数,它的作用就是遍历一个聚合对象,使用这个each函数的人不用关心它的内部代码是怎么实现的,只要它提供的功能正确便可以了。
封装类型
封装类型是静态类型语言的一种重要封装方式。封装类型是通过抽象类和接口来进行的。
在JavaScript中,并没有对抽象类和接口的支持。JavaScript本身也是一门类型模糊的语言。在封装类型方面,JavaScript没有能力,也没有必要做得更多。
封装变化
从设计模式的角度出发,封装在更重要的层面体现为封装变化。
通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换掉那些容易变化的部分,如果这些部分是已经封装好的,替换起来也想对容易。这可以最大程度地保证程序的稳定性和可扩展性。
原型模式
原型模式
原型模式是用于创建对象的一种模式。
原型模式的实现关键是语言本身是否提供了clone方法,ECMAScript5提供了Object.create方法,可以用来克隆对象。
原型模式的真正目的不在于需要得到一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。
在JavaScript这种类型模糊的语言中,创建对象非常容易,也不存在类型耦合的问题。从设计模式的角度来看,原型模式的意义并不算大。但JavaScript本身是一门基于原型的面向对象语言,它的对象系统就是使用原型模式来搭建的,在这里称为原型编程范型也许更合适。
原型编程范型
原型编程中有一个重要特性,即当对象无法响应某个请求时,会把该请求委托给它自己的原型。
而原型编程范型至少包括以下基本原则:
所有的数据都是对象
要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
对象会记住它的原型
如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
JavaScript中的原型继承
JavaScript在原型编程范型的规则的基础上来构建它的对象系统。
所有的数据都是对象
JavaScript在设计的时候,模仿Java引入了两套类型机制:基本类型和对象类型。
按照JavaScript设计者的本意,除了undefined之外,一切都应是对象。为了实现这一目标,number、boolean等几种基本类型数据可以通过“包装类”的方式变成对象类型数据。
JavaScript绝大部分数据都是对象。事实上,JavaScript中的根对象是Object.prototype对象。Object.prototype对象是一个空对象。JavaScript的每个对象,都是从Object.prototype对象克隆而来。
要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
JavaScript通过显式地调用 var obj1 = new Object()
, 或者 var obj2 = {}
。此时,引擎内部会从Object.prototype上面克隆一个对象出来。
这里用了new运算符从构造器中得到了一个对象。在JavaScript里,函数既可以作为普通的函数被调用,也可以作为构造器被调用。用new运算符来创建对象的过程,实际上也只是先克隆Object.prototype对象,再进行一些其他额外操作的过程。
对象会记住它的原型
就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。“对象把请求委托给它自己的原型”就是对象把请求委托给它的构造器的原型。
JavaScript给对象提供了一个名为__proto__的隐藏属性,某个对象的__proto__属性默认会指向它的构造器的原型对象,即{Constructor}.prototype。在一些浏览器中,__proto__被公开出来。
如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
这条规则是原型继承的精髓所在。当一个对象无法响应某个请求时,它会顺着原型链把请求传递下去,直到遇到一个可以处理请求的对象为止。
虽然JavaScript的对象最初都是由Object.prototype对象克隆而来,但对象构造器的原型并不仅限于Object.prototype上,而是可以动态地指向其他对象。例如,当对象A需要对象B的能力时,可以有选择地把对象A的构造器的原型指向对象B,从而达到继承的效果。
PS:本节内容为《JavaScript设计模式与开发实践》第一章 笔记。