面向对象的JavaScript
看本文之前需要有JavaScript的基础知识
JavaScript没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承,也没有在语言层面提供对抽象类和接口的支持
一、动态类型语言和鸭子模型
编程语言按照数据类型大体可分为两类:静态类型语言、动态类型语言
静态类型语言在编译时便已经确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值后,才会具有某种类型。
静态语言
优点:
在编译时就能发现类型不匹配的错误,如果在程序中明确规定了数据类型,编译器还可以针对这些信息对程序进行一些优化的工作,提高程序执行速度
缺点:
迫使程序员按照锲约来编写程序,类型的声明会增加更多代码
动态语言
优点:
编写的代码数量少,更加简洁
缺点:
无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误
在JavaScript中,当我们对一个变量赋值时,虽然不需要考虑他的类型,因此,JavaScript是一门典型的动态类型语言。
这一切都建立在鸭子类型的概念上,鸭子类型的通俗说法是:”如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子。”
我们可以通过一个小故事来更深刻的了解鸭子类型:
从前在JavaScript的王国里,有一个国王,他觉得世界上最美妙的声音就是鸭子叫声,于是国王召集大臣,要组建一个1000只鸭子组成的合唱团。大臣们找遍了全国,终于找到了999只鸭子,但始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫声跟鸭子一摸一样,于是这只鸡就成为了合唱团的最后一员。
这个故事告诉我们,国王要听的只是鸭子的叫声,这个声音的主人到底是鸡还是鸭并不重要,鸭子类型指导我们只关注对象的行为,而不关注对象本身。
二、多态
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果,换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
从字面意思理解多态不太容易,下面我们来举例说明一下。
主人家里养了两只动物,分别是一只鸡和一只鸭,当主人向他们发出”叫”的命令时,鸭会”嘎嘎嘎”地叫,而鸡会”咯咯咯”地叫。这两只动物都会以自己的方式来发出叫声。它们同样”都是动物,并且可以发出叫声”,但根据主人的指令,它们会各自发出不同的叫声。
一段”多态”的JavaScript代码
我们把上面的故事用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());这段代码确实体现了”多态性”,当我们分别向鸭和鸡发出”叫唤”的消息时,它们根据此消息做出了各自不同的反应,但是这样的”多态性”是无法令人满意的,如果后来增加一只动物,此时我们必须要改动
makeSound
函数,修改代码总是危险的,当动物的种类越来越多时,makeSound
有可能变成一个巨大的函数。多态背后的思想是将”做什么”和”谁去做以及怎样做”分离开来,也就是将”不变的事物”与”可能改变的事物”分离开来。在这个故事中,动物会叫,这个是不变的,但是不同类型的动物具体怎么叫是可变的,把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来可生长的,也是符合开放-封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。
对象的多态性
下面是改写后的代码,首先我们把不变的部分隔离出来,那就是所有的动物都会发出叫声:
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());检查类型和多态
public class Duck {
public void makeSound(){
System.out.println("嘎嘎嘎");
}
}
public class Chicken {
public void makeSound(){
System.out.println("咯咯咯");
}
}
public class AnimalSound {
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);
}
}已经顺利让鸭子可以发出叫声,但如果现在想要鸡也叫起来,我们发现这是一件不可能实现的事情,因为(1)处
AnimalSound
类的makeSound
方法,被我们规定为只能接受Duck类型的参数。某些时候,在享受静态语言类型检查带来安全性的同时,我们亦会感觉被束缚了手脚。
为了解决这一问题,静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。就像我们描述天上的一只麻雀或者一只喜鹊,通常可以说”一只麻雀在飞”或者”一只喜鹊在飞”。但如果想忽视他们的具体类型,那么也可以说”一只鸟在飞”。
同理,当Duck对象和Chicken对象的类型都被隐藏在超类型的Animal身后,Duck对象和Chicken对象就能被交换使用,这是让对象表现出多态性的必经之路,而多态性的表现正是实现众多设计模式的目标。
使用继承得到多态效果
使用继承来得到多态效果,是让对象表现出多态性的最常用手段。
我们先创建一个Animal抽象类,再分别让Duck和Chicken都继承自Animal抽象类,下面代码中(1)处和(2)处的赋值语句显然是成立的,因为鸭子和鸡也是动物
public abstract class Animal {
abstract void makeSound(); // 抽象方法
}
public class Chicken extend Animal {
public void makeSound() {
System.out.println("咯咯咯");
}
}
public class Duck extend Animal {
public void makeSound() {
System.out.println("嘎嘎嘎");
}
}
Animal duck = new Duck(); // (1)
Animal Chicken = new Chicken(); // (2)现在剩下的就是让
AnimalSound
类的makeSound
方法接受Animal类型的参数,而不是具体的Duck类型或者Chicken类型:public class AnimalSound {
public void makeSound(Animal animal){
animal.makeSound();
}
}
public class Test {
public static void main(String args[]) {
AnimalSound animalSound = new AnimalSound();
Animal duck = new Duck();
Animal Chicken = new Chicken();
animal.makeSound(duck);
animal.makeSound(chicken);
}
}JavaScript的多态
在java中可以通过向上转型来实现,而JavaScript的变量类型在运行期是可变的,一个JavaScript对象,既可以表示Duck类型的对象,又可以表示Chicken类型的对象,这意味着JavaScript对象的多态性是与生俱来的。
在JavaScript中,并不需要向上转型之类的技术来取得多态的效果
多态在面向对象程序设计中的作用
Martin Fowler在《重构:改善既有代码的设计》里写道:
多态的最根本好处在于,你不必再向对象询问”你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。
Martin Fowler的话可以用下面这个例子很好的诠释:
在电影的拍摄现场,当导演喊出”action”时,主角开始背台词,照明师负责打灯,后面的群众演员假装中枪倒地,道具师往镜头前撒上雪花。在得到同一消息时,每个对象都知道自己应该做什么。如果不利于对象的多态性,而是用面向对象的方式来编写这段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人面前,确认它们的职业分工(类型),然后告诉它们要做什么。如果映射到程序中,那么程序中将充斥着条件分支语句。
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象的程序设计
三、封装
封装的目的是将信息隐藏
封装数据
只能依赖变量的作用域来实现封装特性,而且只能模拟出public和private这两种封装性。
除了ES6中提供的let之外,一般我们通过函数来创建作用域。
var myObject = (function(){
var _name = 'seven'; // 创建private变量
return {
getName: function(){ // 公开public方法
return _name;
}
}
})在ES6中,还可以通过Symbol创建私有属性。
封装实现
封装实现细节的例子非常多,拿迭代器来说明,迭代器的作用是在一个不暴露一个聚合对象的内部表示的前提下,提供一种方式来顺序访问这个聚合对象。我们编写了一个each函数,它的作用就是遍历一个聚合对象,使用这个each函数的人不用关心它的内部代码是怎样实现的,只要它提供的功能正确便可以。即使each函数修改了内部源码,只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现的改变。
封装类型
封装类型是静态类型语言中一种重要的封装方式,一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为,在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。比如工厂方法模式、组合模式等。
四、原型模式基于原型继承的JavaScript对象系统
原型模式不单是一种设计模式,也称为一种编程泛型。
使用克隆的原型模式
原型模式是用于创建对象的一种模式,如果我们要创建一个对象,一种方法是先指定它的类型,然后通过类来创建对象。原型模式选择了另一种方式,我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一摸一样的对象。
原型模式的实现关键,是语言本身是否提供了clone方法。ES5提供了
Object.create
方法,可以用来克隆对象。克隆是创建对象的手段
原型模式提供了另一种创建对象的方式,通过克隆对象,我们就不用再关心对象的具体类型名字。这就像一个仙女要送给三岁小女孩生日礼物,虽然小女孩可能还不知道飞机或者船怎么说,但她可以指着商店橱柜里的飞机模型说”我要这个”。
当然在JavaScript这种类型模糊的语言中,创建对象非常容易,也不存在类型耦合的问题。从设计模式的角度来讲,原型模式的意义比不算大。但JavaScript本身是一门基于原型的面向对象语言,它的对象系统就是使用原型模式搭建的,在这里称之为原型编程范型也许更合适。
体验Io语言
在JavaScript语言中不存在类的概念,对象也并非从类中创建出来的,所有的JavaScript对象都是从某个对象上克隆而来的。
JavaScript基于原型的面向对象系统参考了Self语言和Smalltalk语言,为了搞清JavaScript中的原型,我们本该寻根溯源这两门语言,但由于这两门语言距离现在是在太遥远,我们转而了解一下另外一种轻巧又基于原型的语言——Io语言。
作为一门基于原型的语言,Io中同样没有类的概念,每一个对象都是基于另外一个对象的克隆。在Io中,根对象名为Object。
原型编程范型的一些规则
如果A对象是从B对象克隆而来,那么B对象就是A对象的原型。每个对象都有原型,这个原型还有属于自己的原型,最终形成了原型链,原型链最顶端是null。基于原型链的委托机制就是原型继承的本质
为什么设计原型:继承,让对象的属性和方法共享。
原型编程中的一个重要特征:当对象无法响应某个请求时,会把该请求对象委托给它自己的原型。
原型编程规范至少包括以下基本准则
所有的数据都是对象
要得到一个对象,不是通过实例化,而是找到一个对象作为原型并克隆它
对象会记住它的原型
如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
JavaScript中的原型继承
所有数据都是对象
按照JavaScript设计者的本意,除了underfined之外,一切都应是对象,事实上,JavaScript中的根对象是
Object.prototype
对象。Object.prototype
对象是一个空对象。我们在遇到的每个对象,实际上都是从Object.prototype
对象克隆而来的,Object.prototype
对象就是它们的原型。要得到一个对象,不是通过实例化,而是找到一个对象作为原型并克隆它
在JavaScript语言里,我们并不关心克隆的细节,因为这是引擎内部负责实现的,我们所需要做的只是显式地调用
var obj1 = new Object()
或者var obj2 = {}
。此时,引擎内部会从Object.prototype
上面克隆一个对象出来,我们最终得到地就是这个对象。JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new运算符来调用函数时,此时的函数就是一个构造器。用new运算符来创建对象的过程,实际上也只是先克隆
Object.prototype
对象,再进行一些其他额外操作的过程。JavaScript是通过克隆
Object.prototype
来得到新的对象,但实际上并不是每次都真正克隆了一个新的对象对象会记住它的原型
JavaScript给对象提供了一个名为
__proto__
的隐藏属性,某个对象的__proto__
属性会默认指向它的构造器的原型对象,即{Constructor}.prototype
。在一些浏览器中,__proto__
被公开出来。实际上,
__proto__
就是对象跟”对象构造器的原型”联系起来的纽带。因为对象要通过__proto__
属性来记住它的构造对象的原型。如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
这条规则即是原型继承的精髓所在。JavaScript的克隆跟Io语言还是有点不一样,Io中每个对象都可以作为原型被克隆,当Animal对象克隆自Object对象,Dog对象又克隆自Animal对象时,就形成了一条天然的原型链。
而在JavaScript中,每个对象都是从
Object.prototype
对象克隆而来的,如果是这样的话,我们只能得到单一的继承关系,即每个对象都继承自Object.prototype
对象,这样的对象系统显然是受限的。实际上,虽然JavaScript的对象最初都是由
Object.prototype
对象克隆而来的,但对象构造器的原型并不仅限于Object.prototype
上,而是可以动态指向其他对象。这样一来,当对象a需要借用对象b的能力时,可以有选择性的把对象a的构造器的原型指向对象b,从而达到继承的效果。下面的代码是我们最常用的原型继承方式:
var obj = { name: 'seven' };
var A = function(){};
A.prototype = obj;
var a = new A();
console.log(a.name); // seven当我们期望得到一个”类”继承自另外一个”类”的效果时:
var A = function(){};
A.prototype = { name: 'seven' };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log(b.name) // seven
小结
- JavaScript中一切引用类型都是对象,对象就是属性的集合
- Array类型、Function类型、Object类型、Date类型、RegExp类型都是引用类型
- 原型存在的意义就是组成原型链
- 原型链存在的意义就是继承
- 继承存在的意义就是属性共享
- 构造函数用来创建对象,同一构造函数创建的对象,其原型相同
- 对象有
__proto__
属性,函数有__proto__
属性,数组也有__proto__
属性,只要是引用类型,就有__proto__
属性,指向其原型 - 只有函数有
prototype
属性,指向new操作符加调用该函数创建的对象实例的原型对象 - instanceof运算符用于检查右边构造函数的
prototype
属性是否出现在左边对象的原型链中的任何位置,其他它则表示的是一种原型链的继承关系 - 继承意味着复制操作,然而JavaScript默认并不会复制对象的属性,相反,JavaScript只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性