原文链接:http://raganwald.com/2015/06/17/functional-mixins.html

在“原型即对象”中,我们看到可以对原型使用 Object.assign 来模拟 mixin,原型是 JavaScript 中类概念的基石。现在我们将回顾这个概念,并进一步探究如何将功能糅合进类。

首先,简单回顾一下:在 JavaScript 中,类是通过一个构造函数和它的原型来定义的,无论你是用 ES5 语法,还是使用 class 关键字。类的实例是通过 new 调用构造器的方式创建的。实例从构造器的 prototype 属性上继承共享的方法。

对象 mixin 模式

如果多个类共享某些行为,或者希望对庞杂的原型对象进行功能提取,这时候就可以使用 mixin 来对原型进行扩展。 
 
如这里的 Todo 类
class Todo {
constructor (name) {
this.name = name || 'Untitled';
this.done = false;
}
do () {
this.done = true;
return this;
}
undo () {
this.done = false;
return this;
}
}

 
用于颜色编码的 mixin 如下:
const Coloured = {
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
}
};

 
将颜色编码功能糅合到 Todo 原型上是简而易行的:
Object.assign(Todo.prototype, Coloured);

new Todo('test')
.setColourRGB({r: 1, g: 2, b: 3})
//=> {"name":"test","done":false,"colourCode":{"r":1,"g":2,"b":3}}

 
我们还可以升级为使用私有属性:
const colourCode = Symbol("colourCode");

const Coloured = {
setColourRGB ({r, g, b}) {
this[colourCode]= {r, g, b};
return this;
},
getColourRGB () {
return this[colourCode];
}
};

至此,非常简单明了。我们将这称为一种 “模式”,像菜谱一样,是解决某种问题的独特的代码组织方式。
 

函数式 mixin

上面的对象 mixin 功能完好,但用它来解决问题要分两步走:定义 mixin 然后扩展类的原型。Angus Croll 指出将 mixin 定义成函数而不是对象会是更优雅的做法,并称之为函数式 mixin。再次以 Coloured 为例,将它改写成函数的形式:
const Coloured = (target) =>
Object.assign(target, {
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
}
}); Coloured(Todo.prototype);

 
我们可以定义一个工厂函数,并从命名上体现该模式:
const FunctionalMixin = (behaviour) =>
target => Object.assign(target, behaviour);

 
现在我们可以精要地定义函数式 mixin:
const Coloured = FunctionalMixin({
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
}
});

 

可枚举性

如果我们探究 class 声明类的方式下对 prototype 的操作,可以发现声明的方法默认是不可枚举的。这可以避免一个常见问题——遍历实例的 key 时程序员有时忘记检测 .hasOwnProperty。
 
而我们的对象 mixin 模式无法做到这点,定义在 mixin 中的方法默认是可枚举的。如果我们故意将其设置为不可枚举,Object.assign 就不会将它们糅合到目标原型了,因为 Object.assign 只会将可枚举的属性赋值到目标对象上。
 
这将导致以下情形:
Coloured(Todo.prototype)

const urgent = new Todo("finish blog post");
urgent.setColourRGB({r: 256, g: 0, b: 0}); for (let property in urgent) console.log(property);
// =>
name
done
colourCode
setColourRGB
getColourRGB

正如所见,setColourRGB 和 getColourRGB 方法被枚举出来了,而 do 和 undo 方法却没有。这对于不健壮的代码会是个问题,因为我们不可能每次都重写别处的代码,处处加上 hasOwnProperty 检测。
 
该问题使用函数式 mixin 便可迎刃而解,我们可以神乎其神地让 mixin 表现得和 class 声明类似,这是函数式 mixin 的好处之一:
const FunctionalMixin = (behaviour) =>
function (target) {
for (let property of Reflect.ownKeys(behaviour))
Object.defineProperty(target, property, { value: behaviour[property] })
return target;
}

将上面 mixin 的主体部分作为一种代码模板一遍遍写出来不但累人而且容易出错,而将其封装到函数里则是一种小进步。
 
 

mixin 的职责

和类一样,mixin 是元对象:它们给实例定义行为。除了以方法的形式定义对象的行为,类还负责初始化实例。有的时候,类和元对象还会具有其他的功能。
 
例如,有时某个概念涉及到一组人尽皆知的常量。如果使用类,那么将这些常量定义在 class 本身上则非常方便,这时 class 本身充当了命名空间的作用。
class Todo {
constructor (name) {
this.name = name || Todo.DEFAULT_NAME;
this.done = false;
}
do () {
this.done = true;
return this;
}
undo () {
this.done = false;
return this;
}
} Todo.DEFAULT_NAME = 'Untitled'; // If we are sticklers for read-only constants, we could write:
// Object.defineProperty(Todo, 'DEFAULT_NAME', {value: 'Untitled'});

我们无法使用 “简单 mixin” 做同样的事,因为默认情况下,“简单 mixin” 的所有属性最终都被糅合到实例的 prototype 上。例如,我们想定义 Coloured.RED, Coloured.GREEN, Coloured.BLUE,但我们并不想在任何实例个体上定义 RED, GREEN, BLUE。
 
同样,我们可以借助函数式 mixin 来解决该问题。FunctionalMixin 工厂函数将接收一个可选的字典,该字典包含只读的 mixin 属性,该字典通过一个特殊的键给出:
const shared = Symbol("shared");

function FunctionalMixin (behaviour) {
const instanceKeys = Reflect.ownKeys(behaviour)
.filter(key => key !== shared);
const sharedBehaviour = behaviour[shared] || {};
const sharedKeys = Reflect.ownKeys(sharedBehaviour); function mixin (target) {
for (let property of instanceKeys)
Object.defineProperty(target, property, { value: behaviour[property] });
return target;
}
for (let property of sharedKeys)
Object.defineProperty(mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
return mixin;
} FunctionalMixin.shared = shared;

 
现在我们便可以这样写:
const Coloured = FunctionalMixin({
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
},
[FunctionalMixin.shared]: {
RED: { r: 255, g: 0, b: 0 },
GREEN: { r: 0, g: 255, b: 0 },
BLUE: { r: 0, g: 0, b: 255 },
}
}); Coloured(Todo.prototype) const urgent = new Todo("finish blog post");
urgent.setColourRGB(Coloured.RED); urgent.getColourRGB()
//=> {"r":255,"g":0,"b":0}

mixin 本身的方法

JavaScript 中属性未必是值(和函数相对)。有时候,类也具有方法。同样,有时 mixin 具有自己的方法也是合理的,比如当涉及到 instanceof 时。
 
在 ECMAScript 的之前版本中,instanceof 操作符检查实例的 prototype 是否和构造函数的 prototype 相匹配。它和“类”配合使用没啥问题,但却无法直接和 mixin 协同工作。
urgent instanceof Todo
//=> true urgent instanceof Coloured
//=> false

这是 mixin 存在的问题。另外,程序员可能根据需要创建动态类型,或者直接使用 Object.create 和 Object.setPrototypeOf 管理原型,它们都可能导致 instanceof 工作不正常。ECMAScript 2015 提供了一种方式来覆写内置的 instanceof 的行为,即对象可以定义一个特殊的方法,该方法属性的名字是一个既定的符号——Symbol.hasInstance。
 
我们可以简单测试一下:
Object.defineProperty(Coloured, Symbol.hasInstance, {value: (instance) => true});
urgent instanceof Coloured
//=> true
{} instanceof Coloured
//=> true

 
当然,上面的例子在语义上是不对的。然而借助该技术,我们可以这样做:
const shared = Symbol("shared");

function FunctionalMixin (behaviour) {
const instanceKeys = Reflect.ownKeys(behaviour)
.filter(key => key !== shared);
const sharedBehaviour = behaviour[shared] || {};
const sharedKeys = Reflect.ownKeys(sharedBehaviour);
const typeTag = Symbol("isA"); function mixin (target) {
for (let property of instanceKeys)
Object.defineProperty(target, property, { value: behaviour[property] });
target[typeTag] = true;
return target;
}
for (let property of sharedKeys)
Object.defineProperty(mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
Object.defineProperty(mixin, Symbol.hasInstance, {value: (instance) => !!instance[typeTag]});
return mixin;
} FunctionalMixin.shared = shared; urgent instanceof Coloured
//=> true
{} instanceof Coloured
//=> false

你需要为了照顾 instanceof 而专门做此实现吗?很可能不需要,因为自己实现一套多态机制是不得已而为之的做法。但这一点使得编写测试用例很顺手,并且一些激进的框架开发者可能在函数的多分派和模式匹配上求索,或许会用上这一点。
 

总结

对象 mixin 的迷人之处在于简单:它不需要在对象字面值和 Object.assign 之上做一层抽象。
 
然而,通过 mixin 模式定义的行为和通过 class 关键字定义的行为存在些许差异。体现差异的两个例子分别是可枚举性以及 mixin 自身的属性(如常量和类似于 [Symbol.hasInstance] 的 mixin 自身方法)。
 
函数式 mixin 使得实现上述功能成为可能,不过生成函数式 mixin 的 FunctionalMixin 函数引入了一定复杂性。
 
一般来说,最好保持同一个问题域下的代码表现尽可能相似,而这有时不可避免地增加基础代码的复杂性。但这一点更多的是一种指导思想,而非需要恪守的万能教条。出于此,对象 mixin 模式和函数式 mixin 在 JavaScript 中都有各自的一席之地。

ES2015 中的函数式Mixin的更多相关文章

  1. C++学习35 模板中的函数式参数

    C++对模板类的支持比较灵活,模板类的参数中除了可以有类型参数,还可以有普通参数.例如: template<typename T, int N> class Demo{ }; N 是一个普 ...

  2. 可爱的 Python : Python中的函数式编程,第三部分

    英文原文:Charming Python: Functional programming in Python, Part 3,翻译:开源中国 摘要:  作者David Mertz在其文章<可爱的 ...

  3. Java 中的函数式编程(Functional Programming):Lambda 初识

    Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...

  4. C#中的函数式编程:序言(一)

    学了那么久的函数式编程语言,一直想写一些相关的文章.经过一段时间的考虑,我决定开这个坑. 至于为什么选择C#,在我看来,编程语言分三类:一类是难以进行函数式编程的语言,这类语言包括Java6.C语言等 ...

  5. java基础---->java8中的函数式接口

    这里面简单的讲一下java8中的函数式接口,Function.Consumer.Predicate和Supplier. 函数式接口例子 一.Function:接受参数,有返回参数 package co ...

  6. (数据科学学习手札48)Scala中的函数式编程

    一.简介 Scala作为一门函数式编程与面向对象完美结合的语言,函数式编程部分也有其独到之处,本文就将针对Scala中关于函数式编程的一些常用基本内容进行介绍: 二.在Scala中定义函数 2.1 定 ...

  7. ES2015中的解构赋值

    ES2015中允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,被称为”解构(Destructering)“. 以前,为变量赋值,只能指定值. /** * 以前,为变量赋值,只能直接指定值 * ...

  8. ES2015中let的暂时性死区(TDZ)

    Tomporal Dead Zone (TDZ)是ES2015中对作用域新的专用定义.是对于某些遇到在区块作用域绑定早于声明语句时的情况.Tomporal Dead Zone (TDZ)可以理解为时间 ...

  9. Apache Beam中的函数式编程理念

    不多说,直接上干货! Apache Beam中的函数式编程理念 Apache Beam的编程范式借鉴了函数式编程的概念,从工程和实现角度向命令式妥协. 编程的领域里有三大流派:函数式.命令式.逻辑式. ...

随机推荐

  1. vue(5)—— vue的路由插件—vue-router 常用属性方法

    前端路由 看到这里可能有朋友有疑惑了,前端也有路由吗?这些难道不应该是在后端部分操作的吗?确实是这样,但是现在前后端分离后,加上现在的前端框架的实用性,为的就是均衡前后端的工作量,所以在前端也有了路由 ...

  2. js 学习之路9:运算符

    1. 算数运算符 运算符 描述 例子 结果 + 加 x=y+2 x=7 - 减 x=y-2 x=3 * 乘 x=y*2 x=10 / 除 x=y/2 x=2.5 % 求余数 (保留整数) x=y%2 ...

  3. iOS中Safari浏览器select下拉列表文字太长被截断的处理方法

    网页中的select下拉列表,文字太长的话在iOS的Safari浏览器里会被自动截断,显示成下面这种: 安卓版的浏览器则没有这个问题. 如何让下拉列表中的文字在iOS的Safari浏览器里显示完整呢? ...

  4. Python开发 文件操作

    阅读目录 1.读写文件 open()将会返回一个file对象,基本语法: open(filename,mode) filename:是一个包含了访问的文件名称的路径字符串 mode:决定了打开文件的模 ...

  5. python学习——读取染色体长度(六:读取含有染色体长度的文件)

    含有染色体长的文件chr_len.txt chr1 10chr2 20chr3 30chr4 40chr5 50 python脚本 #传递命令行参数 import sys # 导入模块 # 从命令行获 ...

  6. RobotFramework和Eclipse集成-安装和使用说明

    1.安装python3. 安装说明: https://www.cnblogs.com/Simple-Small/p/9179061.html 2.RF安装命令:Pip install RobotFra ...

  7. 【故障公告】推荐系统中转站撑爆服务器 TCP 连接引发的故障

    上周五下午,我们在博客中部署了推荐系统,在博文下方显示“最新IT新闻”的地方显示自动推荐的关联博文.我们用的推荐系统是第四范式的推荐服务,我们自己只是搭建了一个推荐系统中转站(基于 ASP.NET C ...

  8. 我对DFS的理解

    我对DFS的理解 [何为DFS] 深度优先搜索(Depth-First-Search),简称DFS.是一种常见搜索算法.其方法是从原点不断一条路扩散,当无路可走时回退来走下一条路,直至找到目标或遍历. ...

  9. 译注(2): How to Write a 21st Century Proof

    原文:Computer Scientist Tells Mathematicians How To Write Proofs 对比一下下面两个证明哪个更好? 版本一: "A square a ...

  10. C#技巧记录——持续更新

    作为一名非主修C#的程序员,在此记录下学习与工作中C#的有用内容,持续更新 对类型进行约束,class指定了类型必须是引用类型,new()指定了类型必须具有一个无参的构造函数 where T : cl ...