原型模式(克隆模式)

152 阅读6分钟

简介

从设计模式的角度讲,原型模式是用于创建对象的一种模式,如果我们想要创建一个对象一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象

代码实现(js)

原型模式的实现关键,是语言本身是否提供了clone方法。ECMAScript5提供了Object.create方法,可以用来克隆对象

var Plane = function() {
    this.blod = 100;                
    this.attackLevel = 1;                   
}
var plane = new Plane();
plane.blod = 500;               
plane.attackLevel = 10;             
plane.defenselevel = 7;             
var clonePlane = Object.create(plane)               
console.log(clonePlane.attackLevel)
console.log(clonePlane.blod)                
console.log(clonePlane.defenselevel)

克隆是创建对象的手段

原型模式的真正目的并非在于需要一个一模一样的对象,而是提供了一种编辑的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。

原型模式提供了另外一种创建对象的方式,通过克隆对象,我们就不用再关心对象的具体类型名字。这就你要送给三岁小女孩生日礼物,虽然小女孩可能还不知道飞机或者船怎么说,但她可以指着商店里的飞机模型说:“我要这个”。

当然在JavaScript,类型模糊的语言中,创建对象非常容易,也不存在类型耦合的问题。从设计模式的角度来讲,原型模式的意义并不算大。但JavaScript本身是一门基于原型的面向对象语言,它的对象系统就是使用原型模式来搭建的,在这里称之为原型编程范型也许更合适

根对象

前面说过,原型模式不仅仅是一种设计模式,也是一中编程范型。JavaScript就是使用原型模式来搭建整个面向对象系统的。在JavaScrput语言中不存在类的概念,对象也并非从类中创建出来的,所有的 JavaScript对象都是从某个对象上克隆而来的。对于习惯以类为中心语言的人来说,也许一时不容易理解这种基于原型的语言,即使是对于 前端开发者而言,也可能会有一种“不识庐山真面目,只缘身在此山中”的感觉,事实上,使用原型模式来构建面向对象系统的语言远非仅有 JavaScript一家。

原型编程范型基本规则

  1. 所有数据都是对象
  2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  3. 对象会记住它的原型
  4. 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型

JavaScript中的原型继承

1. 所有数据都是对象

JavaScript在设计的时候,模仿Java引入了两条类型机制:基本类型和对象类型。基本类型包括 undefined、number、boolean、string、function、object。从现在看来,这并不是一个好的想法。我们不能说在 JavaScript中所有数据都是对象,但可以说绝大部分数据都是对象,那么相信在JavaScript中也一定有一个根对象存在,这些对象追根溯源都来源于这个根对象。

JavaScript中所有对象是 Object.prototype对象。Object.prototype对象是一个空对象。我们在 JavaScript遇到每个对象。实际上都是从 Object.prototype对象克隆而来的Object.prototype对象就是它们的原型。比如下面的obj1和obj2对象

var obj1 = new Object();
var obj2 = {};
console.log(Object.getPrototypeOf(obj1))
console.log(Object.getPrototypeOf(obj2))
console.log(Object.getPrototypeOf(obj1) === Object.prototype)
console.log(Object.getPrototypeOf(obj2) === Object.prototype)

2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它

在JavaScript中,我们并不需要关系克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用var obj1 = new Oject()或者var obj2 = {}。此时,引擎内部会从Object.prototype上面克隆一个对象出来,我们最终得到这个对象。

function Person(name) {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name
}
var a = new Person("sven");
console.log(a.name) //输出 seve
console.log(a.getName()) //输出 seve
console.log(Object.getPrototypeOf(a) === Person.prototype) //输出 true

在JavaScript中没有类的概念。在这里 Person 并不是类,而是函数构造器,JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new运算符来调用函数时,此时的函数就是一个构造器。用new运算符来创建对象的过程,实际上也只是先克隆Object.prototype对象,再进行一些其他额外的操作的过程。

3.对象会记住它的原型

如果请求可以在一个链条中依次往后传递,那么每个节点都必须知道它的下一个节点。JavaScript语言中的原型链查找机制,每个对象至少应该先记住它自己的原型。对于JavaScript真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。

JavaScript给对象提供了一个名为__proto__的隐藏属性,某个对象的__proto__属性默认会指向它的构造器的原型对象,即{Constructor}.prototype。实际上__proto__就是对象跟“对象构造器的原型”联系起来的纽带。

4.如果对象无法响应某个请求。它会把这个请求委托它的构造器的原型

这条规则即是原型继承的精髓所在。当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象。

而在JavaScript中,每个对象都是从Object.prototype对象克隆而来的,如果是这样的话,我们只能得到单一的继承关系,即每个对象都继承自 Object.prototype对象,这样的对象系统显然是非常受限。但对象构造器的原型并不仅限于Object.prototype上,而是可以动态指向其他对象。这样一来,当对象a需要借用对象b的能力时,可以有选择性地把对象a的构造器的原型指向对象b,从而达到继承的效果。

原型继承的未来

设计模式在很多时候其实都体现了语言不足之处。设计模式是对语言不足的补充。

不足的是在JavaScript引擎下,通过Object.prototype来创建对象的效率并不高,通常比通过构造函数创建对象要慢。此外还有一些值得注意的地方,比如通过设置构造器的prototype来实现原型继承的时候,除了根对象Object.prototype本身之外,任何对象都会有一个原型。而通过Object.create(null)可以创建出没有原型的对象

另外,ECMAScript6带来了新的Class语法,这让JavaScript看起来像是一门基于类的语言,但其背后仍是原型机制来创建对象。