原型即对象(以及认识这点的重要性)
原文链接:http://raganwald.com/2015/06/10/mixins.html
预备知识:该文章假设读者熟悉JavaScript 对象,知道原型是如何给对象定义行为的,知道构造函数是什么,知道构造器的 prototype 属性和它创建的对象之间是如何产生联系的。对 ES2015 语法有些了解也将有所帮助。
一直以来,我们可以这样创建一个 JavaScript 类:
function Person (first, last) { this.rename(first, last); } Person.prototype.fullName = function fullName () { return this.firstName + " " + this.lastName; }; Person.prototype.rename = function rename (first, last) { this.firstName = first; this.lastName = last; return this; }
Person 是一个构造函数,同时也是一个类,当然是 JavaScript 世界中的“类”。
ECMAScript 2015 提供了 class 关键字以及“紧凑的方法记号”,它们是编写函数,并给其 prototype 属性赋值方法的语法糖(虽然实际情况更复杂,但和此处不相关)。所以我们现在可以如下编写 Person 类:
class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } };
不错。但究其本质,你仍然不过编写了一个名为 Person 的构造函数,并且 Person.prototype 是这样的一个对象:
{ fullName: function fullName () { return this.firstName + " " + this.lastName; }, rename: function rename (first, last) { this.firstName = first; this.lastName = last; return this; } }
原型即对象
如果我们想改变一个 JS 对象的行为,可以通过添加、删除,或修改作为对象属性的函数,它们就是对象的方法。这和大多数基于类的语言不同,这些语言往往提供特殊的语法形式来定义方法(如 Ruby 的 def)。
JavaScript 中的原型不过是普通的对象,正由于它们是普通的对象,我们便可以对原型的方法进行增删改,即对绑定到原型对象作为其属性的那些函数进行操作。
这也正是上面 ES5 代码所干的事,并且 ES6 的 class 语法“去语法糖”后也会得到相同代码。
原型即普通对象的概念意味着我们可以把原型当普通对象对待。例如,除了将函数一个个绑定到原型上,还可以使用 Object.assign 一次性绑定。
function Person (first, last) { this.rename(first, last); } Object.assign(Person.prototype, { fullName: function fullName () { return this.firstName + " " + this.lastName; }, rename: function rename (first, last) { this.firstName = first; this.lastName = last; return this; } })
当然,我们也可以使用紧凑方法声明语法
function Person (first, last) { this.rename(first, last); } Object.assign(Person.prototype, { fullName () { return this.firstName + " " + this.lastName; }, rename (first, last) { this.firstName = first; this.lastName = last; return this; } })
mixins
由于 class 语法最终转化为构造函数和原型的形式,我们可以混合使用两种技术:
class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } }; Object.assign(Person.prototype, { addToCollection (name) { this.collection().push(name); return this; }, collection () { return this._collected_books || (this._collected_books = []); } })
上例中我们将书籍收集相关的方法糅合到了 Person 类上。这点非常不错,因为我们得以让代码具有 point-free 风格,同时命名方面也很棒。
const BookCollector = { addToCollection (name) { this.collection().push(name); return this; }, collection () { return this._collected_books || (this._collected_books = []); } }; class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } };
Object.assign(Person.prototype, BookCollector);
只要愿意,这一过程可一直进行
const BookCollector = { addToCollection (name) { this.collection().push(name); return this; }, collection () { return this._collected_books || (this._collected_books = []); } }; const Author = { writeBook (name) { this.books().push(name); return this; }, books () { return this._books_written || (this._books_written = []); } }; class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } }; Object.assign(Person.prototype, BookCollector, Author);
为什么 mixins 可能有用武之地
通过基础功能(Person)和 mixins(BookCollector 和 Author)相结合的方式创建类可获得几点好处。
首先,有时候无法将功能完美分解成树形结构。书的作者有时是公司,而不是人。并且古籍书店也可能像藏书家一样收集图书。
类似于 BookCollector 或 Author 的 mixin 可被糅合到多个类上。试图使用“继承”来实现功能有时不太确切。
另一个好处不容易从玩具用例上看出,但在实际项目中类的定义可以变得非常庞杂。即使某个 mixin 没有在多个类中使用,将一个大类分解成多个 mixin 也有助于实现“单一责任原则”。每个 mixin 可以只做一件事。这使得代码变得容易理解和测试。
为什么要知道这些
还有其他方法可以分解类的功能(如委托和组合),但这里想说的是如果我们想使用 mixin,这是非常容易的,因为 JavaScript 并没有庞杂的 OOP 机制对程序施加进行严格的模型限制。
例如在 Ruby 中,mixins 用起来也很方便,这得益于与生俱来的 modules 特性。但在其他面向对象语言中,mixins 就没那么顺手了,因为类系统没有相应的支持,并且 mixin 对元编程也不是很友好。
JavaScript 使用简单部件(对象,函数以及属性)实现 OOP 的这一选择促进了新思潮的发展。