原型即对象(以及认识这点的重要性)

原文链接: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 的这一选择促进了新思潮的发展。 
 
posted @ 2018-11-26 17:04  AveryBy  阅读(208)  评论(0编辑  收藏  举报