GOF设计模式(三)——原型模式和基于原型集成的JavaScript对象系统

前言

在传统的面向对象编程语言中(如Java,C#), 类和对象就像柏拉图所说的“模子”和姜饼人,所有的姜饼人(对象)总是从模子(类)中创建而来,类生成实例就像厨师用模子做出姜饼人。

而在原型编程的思想中,类并不是必须的,对象未必从类中创建,而是通过克隆另一个对象得到。

为了更清晰地了解原型模式,将其与面向对象编程的思想区分开,我们先来了解一种轻巧又基于原型的语言—— Io语言。

IO语言

Io语言在2002年由 Steve Dekorte发明。可以从 http://iolanguage.com 下载到Io语言的解释器。

作为一门基于原型的语言,Io中没有类的概念,每一个对象都是基于另外一个对象的克隆。而所有对象的祖先,即根对象,在Io语言中叫 Object.

现在我们有了根对象Object, 想要创建一个对象叫做Animal, 那么Animal要从Object中复制而来,而Object就是Animal的原型。

 Aniaml := Object clone //克隆动物对象

假设在Io的世界中,所有的Animal都会叫,我们给Animal添加makeSound方法:

Animal makeSound := method( "animal makeSound " print ); 

接着,我们再创建一个 Dog 对象:

Dog := Animal clone

Dog 能够摇尾巴

Dog wagTail := method( "Dog is wagging his tail" print );

以此类推,我们可以创建出更多的对象。在以上举出的例子中存在这样的原型链:

Dog
Animal
Object

原型编程范型的一些规则

Dog对象没有makeSound方法,于是把请求委托给了他的原型Animal, 而Animal对象是有makeSound方法的,所以执行Dog makeSound 可以顺利输出 animal makeSound.
这个机制很简单,但功能却很强大,Java Script和Io一样,基于原型链的委托机制就是原型继承的本质
现在我们明白了原型编程中的一个重要特性,即当对象无法响应某个请求时,会把该请求委托给自己的原型。
总结一下原型编程范型的基本规则:

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

JavaScript中的原型继承

依照原型范型的四条规范,我们来看JavaScript是如何一一实现的。

1. 所有的数据都是对象

JavaScript在设计的时候,模仿java设计了两种数据类型: 基本类型对象类型

基本类型包括 undefined, number, boolean, string, function, object 。按照JS设计者的本意,除了undefined之外,一切都应是对象。为了实现这一目标, number, boolean, string 这几种基本类型数据也可以通过 包装类 的方式变成对象类型数据来处理。

我们不能说JS中所有数据都是对象,但可以说绝大部分数据都是对象。

Io语言中的根对象是Object , 那么JS中的根对象是什么呢?

Object.prototype 对象就是这个根对象。
Object.prototype 对象是一个空对象。
JS中的每一个对象都是从 Object.prototype 对象克隆而来。Object.prototype 对象就是它们的原型。例如

var obj1 = new Object();
var obj2 = {};

Object.getPrototypeOf 来查看这两个对象的原型:

console.log(Object.getPrototypeOf(obj1 === Object.prototype));  //true
console.log(Object.getPrototypeOf(obj2 === Object.prototype));  //true

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

在Io语言中克隆有一个明显的标志,就是关键字 clone, 而在JS中,这个过程由引擎内部实现。我们只需要显示地调用var obj = new object()obj2 = {},引擎内部会从 Object.prototype 上面克隆一个对象出来。

new运算符是如何得到一个对象的呢?例如:

function Person(name){
  this.name = name;
}

Person.prototype.getName = function(){
  return this.name;
}

var a = new Person( 'Mary' ) ;

console.log( a.name ); //Mary
console.log( a.getName() ); //Mary
console.log( Object.getPrototypeOf( a ) === Person.prototype ); //true

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

我们可以通过下面这段代码来理解new运算的过程:

function Person(name){
  this.name = name;
}

Person.prototype.getName = function(){
  return this.name;
}

objectFactory() {
  let obj = new Object(), //从Object.prototype上克隆一个空的对象
      Constructor = [].shift.call(arguments); //取得外部传入的构造器, 此例是Person
  obj._proto__ = Constructor.prototype; //指向正确的原型
  let ret = Constructor.apply(obj, arguments);
  return typeof ret === 'object' ? ret : obj;
}

3. 对象会记住自己的原型

就JS的真正实现来说, 其实并不能说对象有原型,而只能说对象的构造器有原型。所以,对于 “ 对象把请求委托给它自己的原型” 这句话,更好的说法是对象把请求委托给它的构造器原型
JS给对象提供了一个名为 “proto” 的隐藏属性指向它的构造器的原型对象,即 {Constructor}.prototype。

4. 如果对象无法响应某个请求,它会把这个请求委托给自己的原型

在之前Io语言的举例中,因为每个Io对象都可以作为原型被克隆,所以存在一条天然的原型链:

Dog
Animal
Object

而在JS中,每个对象都是从 Object.prototype 对象克隆而来。如果这样的话,我们只能得到非常单一的继承关系,即每个对象都继承自 Object.prototype 对象,这样的对象系统十分受限。

实际上,虽然JS的对象最初都是 Object.prototype 对象克隆而来的,但对象构造器的原型并不仅限于Object.prototype上,而是可以动态指向其他对象。比如, 当对象a要借用对象b的方法时,可以有选择性地将对象a的构造器的原型指向b,从而达到继承的效果。

常见的原型继承方式:

var obj = { name: 'Mary'};

var A = function(){};
A.prototype = obj;

var a = new A();
console.log(a.name);  // Mary

当我们期望得到一个“类”继承自另外一个“类”的效果时(注意JS中并没有真正的的“类”的概念),可以用下面的方式:

var Dog = function(){};
Dog.prototype = { call:'Wang!' };

var Samoyed = function(){};
Samoyed.prototype = new Dog(); 

var Maya = new Samoyed();
console.log(Maya.call);   // Wang!

分析下在执行这段代码的时候,引擎做了什么事情。

  • 尝试遍历对象 Maya 中的所有属性,但没有找到 name 这个属性
  • 查找 name 属性被委托给 Maya._proto_ ,即 Maya 构造器的prototype , 即 Samoyed.prototype, 而Samoyed.prototype 被设置为一个通过 new Dog() 创建出来的对象。
  • 该对象中依然没有 name 这个属性,于是请求被委托给这个对象构造器的原型 Dog.prototype.
  • Dog.prototype 中找到name 属性,并返回它的值。

和把Samoyed.prototype 直接指向一个字面量对象相比, 通过Samoyed.prototype = new Dog() 形成的原型链比之前多一层。但二者没有实质区别,都是将对象构造器的原型指向另一个对象,继承总是发生在对象和对象之间

原型继承的未来

JS的 Object.create 是原型继承的天然实现, 使用 Object.create 来完成原型继承似乎更直观(类似Io语言中的 clone)。美中不足是,当前JS引擎下,通过 Object.create 创建对象的效率不高,比通过构造函数要慢。

通过设置构造器 prototype 来实现原型继承的时候,除了根对象 Object.prototype 本身之外, 任何对象都会有一个原型。而通过 Object.create(null) 可以创建出一个没有原型的对象。

ECMAScript6 带来了新的Class语法,使它看起来好像是一门基于类的语言,但其背后仍是通过原型机制来创建对象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章