《JavaScript应用程序设计》一一3.9使用Stamps进行原型继承

3.9使用Stamps进行原型继承

对象在JavaScript中,有各式各样灵活的特性,相比之下通过Object.create()方法所构建出的对象,感觉就像被“阉割”了一样。开发者总是需要自己去编写额外的代码来实现诸如让享元对象支持数据隐蔽特性这样的特性,所以当你需要将多个对象上的功能做组合时,一般情况下你会感觉非常无力。
现今多数JavaScript类库中提供了一套与类继承概念相似的对象复用机制,基于原型继承的JavaScript类库占比还是太少,所以就更别提什么业界标准了。既然少量语法糖就能够在JavaScript中模拟类的概念,那么为什么不能构建一个囊括了JavaScript所有强大特性的原型继承系统呢?
当聊起JavaScript中的对象构建时,我们不禁首先会问自己,在JavaScript中,对象所具有的最为重要的特性有哪些?
· 原型代理
· 实例状态
· 封装性
Stamps是一个工厂函数,它拥有一组公有属性与方法,这些属性方法用来定义所构建对象的原型代理、默认的实例属性以及为封装私有属性所需的函数。在构建对象方面,Stamps提供了3种不同的继承方式。
· 原型代理:原型继承
· 实例状态:属性混入
· 封装性:闭包式继承
Stampit(https://github.com/dilvie/stampit)是为本书而写的类库,用来向读者展示如何利用JavaScript自带的语言特性来简化原型继承,它向外界仅暴露一个入口函数,其函数签名如下:
stampit(methods, state, enclose);
让我们看看如何使用Stampit来构建对象:

var testObj = stampit(
// methods
{
  delegateMethod: function delegateMethod() {
    return 'shared property';
  }
},

// state
{
  instanceProp: 'instance property'
},

// enclose
function () {
  var privateProp = 'private property';

  this.getPrivate = function getPrivate() {
    return privateProp;
  }
}).create();

test('Stampit with params', function () {
  equal(testObj.delegateMethod(), 'shared property',
    'delegate methods should be reachable');

  ok(Object.getPrototypeOf(testObj).delegateMethod,
    'delegate methods should be stored on the ' +
    'delegate prototype');

  equal(testObj.instanceProp, 'instance property',
    'state should be reachable.');

  ok(testObj.hasOwnProperty('instanceProp'),
    'state should be instance safe.');

  equal(testObj.hasOwnProperty('privateProp'), false,
    'should hide private properties');

  equal(testObj.getPrivate(), 'private property',
    'should support privileged methods');
});

在上例中,我们在stampit实例上调用create()方法,它返回testObj的实例。代码下方的测试用例显示,testObj实例已经拥有了诸多原型继承体系下的杀手级特性,你已经无需再花费大量精力去实现自己的工厂函数了。
stampit实例支持方法链式调用,上例中的对象也可以被这样创建:

var testObj = stampit().methods({
    delegateMethod: function delegateMethod() {
      return 'shared property';
    }
  })
  .state({
    instanceProp: 'instance property'
  })
  .enclose(function () {
    var privateProp = 'private property';

    this.getPrivate = function getPrivate() {
      return privateProp;
    }
  })
  .create();

由Object.create()构建出的对象会默认使用原型上定义的方法,这些方法被所有实例所共享,这样做在很大程度上节省了内存开销。同理,如果你在程序运行期间修改了原型中的方法,所有实例均会受到波及,如下例:

var stamp = stampit().methods({
    delegateMethod: function delegateMethod() {
      return 'shared property';
    }
  }),
  obj1 = stamp(),
  obj2 = stamp();

Object.getPrototypeOf(obj1).delegateMethod =
  function () {
    return 'altered';
  };

test('Prototype mutation', function () {
  equal(obj2.delegateMethod(), 'altered',
    'Instances share the delegate prototype.');
});

state()方法采用了“属性混入”的方式,它将传入的对象视为实例的原型代理,并将该对象的每项属性拷贝后添加至新实例中。由于是拷贝而非直接引用,所以你可以放心大胆地对属性做后期修改。所有stampit实例在做对象构建时,均接受一个可选的字典对象,字典对象中的属性会被混入新实例中,这让对象实例化这一过程变得更为简单。

var person = stampit().state({name: ''}),
  jimi = person({name: 'Jimi Hendrix'});

test('Initialization', function () {

  equal(jimi.name, 'Jimi Hendrix',
    'Object should be initialized.');

});

enclose()方法则采用了“闭包式继承”的方式, 你可以给它传入函数,这些函数彼此独立并且各自拥有闭包作用域,从而可以确保数据的隐蔽性与唯一性。此外,如果在函数中存在多个同名特权方法的定义,那么后一项始终会有限覆盖前一项。在stampit实例中enclose()方法可以被调用多次,所传入的函数会在对象构建时被“激活”。
在初始化一个对象时,有时并不需要将参数一次性全部传入,所以我一直建议将对象的实例化与初始化解耦,引入读写方法(Getter/Setter)可以解决这个问题。

var person = stampit().enclose(function () {
  var firstName = '',
    lastName = '';

  this.getName = function getName() {
    return firstName + ' ' + lastName;
  };

  this.setName = function setName(options) {
    firstName = options.firstName || '';
    lastName = options.lastName || '';
    return this;
  };
}),

jimi = person().setName({
  firstName: 'Jimi',
  lastName: 'Hendrix'
});

test('Init method', function () {

equal(jimi.getName(), 'Jimi Hendrix',
  'Object should be initialized.');

});

前面我们介绍了用stampit进行对象构建,其实这仅仅是JavaScript面向对象特性的冰山一角。接下来我们介绍的内容你很难在现有的JavaScript流行类库中寻觅到,甚至连ES6规范中都没有相关定义。
首先,看如何使用闭包来封装私有数据:

var a = stampit().enclose(function () {
  var a = 'a';

  this.getA = function () {
    return a;
  };
});

a().getA(); // 'a'

stampit实例使用函数作用域来封装私有数据,请注意读写方法(Getter/Setter)需定义在函数内部,才可以访问到闭包中的变量,这一规则同样适用于JavaScript中的所有特权函数。
来看另一个例子:

var b = stampit().enclose(function () {
  var a = 'b';

  this.getB = function () {
    return a;
  };
});

b().getB(); // 'b'

这个“拼写错误”是有意为之的,它是为了向你展示实例a与实例b各自封装的同名私有变量不会对彼此的使用造成影响,而且这么做的有趣之处在于:

var c = stampit.compose(a, b),
  foo = c();

foo.getA(); // 'a'
foo.getB(); // 'b'

stampit的静态方法compose()让你能够从多个stampit实例中做原型继承。上述示例演示了使用compose()方法来实现多重继承,我们看到所继承的属性中甚至连私有数据都有囊括,而现今的类继承体系中是做不到这点的。
stampit实例有一个名为fixed的特别属性,它存储着所有methods(实例方法)、state(属性)与enclose(包裹函数)的对象原型。在实例化期间,state对象的所有属性被拷贝至实例中,确保了实例属性操作的安全性;methods对象则作为实例的原型代理来使用,从而使得多个实例间可以共享一套方法;enclose对象将所有函数当作闭包来使用,实现了对私有数据的访问控制。
compose()方法的用途与$.extend()方法很接近,不过它并不是像$.extend()方法那样使用外来对象的属性做扩展,而是借助了stampit实例的fixed属性,compose()方法先是将来自多个stampit实例下的fixed属性做合并,随后向外界返回一个新stampit实例。在对同名属性进行合并时,compose()会优先使用顺序靠后的属性,在这一点上,它与$.extend(), _.extend()等方法的合并策略是一致的。

时间: 2024-09-20 16:38:30

《JavaScript应用程序设计》一一3.9使用Stamps进行原型继承的相关文章

JavaScript高级程序设计 阅读笔记(十四) js继承机制的实现_javascript技巧

继承 继承是面向对象语言的必备特征,即一个类能够重用另一个类的方法和属性.在JavaScript中继承方式的实现方式主要有以下五种:对象冒充.call().apply().原型链.混合方式. 下面分别介绍. 对象冒充 原理:构造函数使用this关键字给所有属性和方法赋值.因为构造函数只是一个函数,所以可以使ClassA的构造函数成为ClassB的方法,然后调用它.ClassB就会收到ClassA的构造函数中定义的属性和方法. 示例: 复制代码 代码如下: function ClassA(sCol

《JavaScript应用程序设计》导读

前言 在本书中我们先后提到了不少Web开发类书籍,但对于那些想从头开始学习构建完整JavaScript应用的读者来说,市面上真正值得推荐的JavaScript教程又显得十分稀缺.与此同时,现在几乎每一个互联网创业公司的人员配备中都少不了资深JavaScript应用程序开发者的角色.本书旨在帮助人们了解如何构建易于扩展和维护的完整JavaScript应用. 本书并不打算向读者详细讲解JavaScript语言基础,它基于你现有的JavaScript知识结构,向你展示那些会让你的编码工作长久受益的Ja

《JavaScript应用程序设计》一一1.3 对象

1.3 对象 JavaScript拥有许多面向对象编程语言的特性.JSON(JavaScript Object Notation)被视为是现今Web应用通信与数据持久化的格式标准,它在JavaScript中仅仅只是对象字面量语法的一个子集而已.JavaScript 使用原型而不是类作为其继承模型,新对象通过原型链自动继承其父级对象的属性与方法,并且可以随时对原型对象作修改,这使得JavaScript成为一门极具灵活性的动态语言.在多数情况下,使用原型继承不仅灵活性要比单纯模拟Java中的类继承要

《JavaScript应用程序设计》一一第3章 对象

第3章 对象JavaScript拥有原型继承.动态对象扩展.闭包等特性,在现今市面上流行的所有编程语言中,基于对象编程的JavaScript最具灵活性与表现力.在JavaScript中,你可以将诸如函数.数组.键/值对以及一些基础数据结构都视为对象,甚至一些原始数据类型在用点语法做属性操作时,也会被JavaScript隐式当作对象处理.为了能够调用原型链上的方法,原始数据类型在使用时会被临时包裹为对象,例如:'tonya@example.com'.split('@')[1]; // => exa

JavaScript高级程序设计(第3版)学习笔记3 js简单数据类型_基础知识

ECMAScript是一种动态类型的语言,构建于5种简单数据类型(Undefined.Null.Boolean.Number.String)和一种复杂数据类型(Object)的基础之上.这篇文章就来复习一下简单数据类型,我会尽量从编程实践的角度来描述,下面代码运行环境为FireFox 14.0.1. 简单数据类型 简单数据类型 取值 Undefined undefined(只有一个值) Null null(只有一个值) Boolean true|false(只有两个值) Number 数值 St

JavaScript高级程序设计(第3版)学习笔记9 js函数(下)_基础知识

再接着看函数--具有魔幻色彩的对象. 9.作为值的函数 在一般的编程语言中,如果要将函数作为值来使用,需要使用类似函数指针或者代理的方式来实现,但是在ECMAScript中,函数是一种对象,拥有一般对象具有的所有特征,除了函数可以有自己的属性和方法外,还可以做为一个引用类型的值去使用,实际上我们前面的例子中已经有过将函数作为一个对象属性的值,又比如函数也可以作为另一个函数的参数或者返回值,异步处理中的回调函数就是一个典型的用法. 复制代码 代码如下: var name = 'linjisong'

JavaScript高级程序设计(第3版)学习笔记8 js函数(中)_基础知识

6.执行环境和作用域 (1)执行环境(execution context):所有的JavaScript代码都运行在一个执行环境中,当控制权转移至JavaScript的可执行代码时,就进入了一个执行环境.活动的执行环境从逻辑上形成了一个栈,全局执行环境永远是这个栈的栈底元素,栈顶元素就是当前正在运行的执行环境.每一个函数都有自己的执行环境,当执行流进入一个函数时,会将这个函数的执行环境压入栈顶,函数执行完之后再将这个执行环境弹出,控制权返回给之前的执行环境. (2)变量对象(variable ob

JavaScript高级程序设计(第3版)学习笔记7 js函数(上)_基础知识

变量类型 在说函数之前,先来说说变量类型. 1.变量:变量在本质上就是命名的内存空间. 2.变量的数据类型:就是指变量可以存储的值的数据类型,比如Number类型.Boolean类型.Object类型等,在ECMAScript中,变量的数据类型是动态的,可以在运行时改变变量的数据类型. 3.变量类型:是指变量本身的类型,在ECMAScript中,变量类型就只有两种:值类型和引用类型.当变量的数据类型是简单数据类型时,变量类型就是值类型,当变量的数据类型是对象类型时,变量类型就是引用类型.在不引起

JavaScript高级程序设计(第3版)学习笔记6 初识js对象_基础知识

在房子里面可以放你想放的任意事物--如果你有足够的美学造诣,你甚至可以弄一个房中房试试--当然,为了方便管理,我们会给房子里存放的所有事物都会取上一个不重复的名字,比如医药房间里的各种药品名称.在ECMAScript中,你可以在对象中存放任意你想放的数据,同样,我们需要给存放的数据取一个名字--也就是对象的属性名,再存放各种数据.再看看ECMA-262中对象的定义:无序属性的集合,其属性可以包含简单数据类型值.对象或者函数. 进入对象,我开始有些激动了,说实话,让我想起做这系列学习笔记的最初原因