什么是Mixin 函数
在编程中,mixin 类似于一个固有名词,可以理解为混合或混入,通常不进行直译,本文也是同样。
Mixin 函数 是指能够给对象添加属性或行为,并可以通过管道连接在一起的组合工厂函数,就如同流水线上的工人。Mixin 函数不依赖或要求一个基础工厂或构造函数:简单地将任意一个对象传入一个 mixin,就会得到一个增强之后的对象。
Mixin 函数的特点
- 数据封装
- 继承私有状态
- 多继承
- 覆盖重复属性
- 无需基础类
Mixin 函数的由来
现代软件开发的核心就是组合:我们将一个庞大复杂的问题,分解成更小,更简单的问题,最终将这些问题的解决办法组合起来就变成了一个应用程序。他们的组合就定义了应用的结构。
组合的最小单位就是以下两者之一:函数和数据结构
通常,组合对象由类继承实现,其中子类从父类继承其大部分功能,并扩展或覆盖部分。这种方法导致了 is-a 问题,比如:管理员是一名员工,这引发了许多设计问题:
- 高耦合:由于子类的实现依赖于父类,所以类继承是面向对象设计中最紧密的耦合。
- 脆弱的子类:由于高耦合,对父类的修改可能会破坏子类。软件作者可能在不知情的情况下破坏了第三方管理的代码。
- 层次不灵活:根据单一祖先分类,随着长时间的演变,最终所有的类都将不适用于新用例。
- 重复问题:由于层次不灵活,新用例通常是通过重复而不是扩展来实现的,这导致不同的类有着相似的类结构。而一旦重复创建,在创建其子类时,该继承自哪个类以及为什么继承于这个类就不清晰了。
- 大猩猩和香蕉问题:“...面向对象语言的问题是他们会获得所有与之相关的隐含环境。比如你想要一个香蕉,但你得到的会是一只拿着香蕉的大猩猩,以及一整片丛林。” - Joe Armstrong(Coders at Work)
假设管理员是一名员工,你如何处理聘请外部顾问暂时行使管理员职务的情况?如果你事先知道所有的需求,类继承可能有效,但我从没有看到过这种情况。随着不断地使用,新问题和更有效的流程将会被发现,应用程序和需求不可避免地随着时间的推移而发展和演变。
Mixin 提供了更灵活的方法
什么是 Mixin?
“组合优于继承。” - 设计模式:可重用面向对象软件的元素
Mixin 是对象组合的一种,它将部分特性混入复合对象中,使得这些属性成为复合对象的属性。
面向对象编程中的 "mixin" 一词来源于冰激凌店。不同于将不同口味的冰激凌预先混合,每个顾客可以自由混合各种口味的冰激凌,从而创造出属于自己的冰激凌口味。
对象 mixin 与之类似:从一个空对象开始,然后一步步扩展它。由于 JavaScript 支持动态对象扩展,所以在 JavaScript 中使用对象 mixin 是非常简单的。它也是 JavaScript 中最常见的继承形式,来看一个例子:
const chocolate = {
hasChocolate: () => true
};
const caramelSwirl = {
hasCaramelSwirl: () => true
};
const pecans = {
hasPecans: () => true
};
console.log(chocolate.hasChocolate())
const iceCream = Object.assign({}, chocolate, caramelSwirl, pecans);
/*
// 支持对象扩展符的话也可以写成这样...
const iceCream = {...chocolate, ...caramelSwirl, ...pecans};
*/
console.log(`
hasChocolate: ${ iceCream.hasChocolate() }
hasCaramelSwirl: ${ iceCream.hasCaramelSwirl() }
hasPecans: ${ iceCream.hasPecans() }
`);
/* 输出
hasChocolate: true
hasCaramelSwirl: true
hasPecans: true
*/
什么是函数继承?
函数继承是指通过函数来增强对象实例实现特性继承的过程。该函数建立一个闭包使得部分数据是私有的,并通过动态对象扩展使得对象实例拥有新的属性和方法。
来看一下这个词的创造者 Douglas Crockford 所给出的例子。
// 父类
function base(spec) {
var that = {}; // Create an empty object
that.name = spec.name; // Add it a "name" property
return that; // Return the object
}
// 子类
function child(spec) {
// 调用父类构造函数,返回父类中的that对象,这个that对象中有name属性,也就是spec这个参数值
var that = base(spec);
that.sayHello = function() { // Augment that object
return 'Hello, I\'m ' + that.name;
};
return that; // Return it
}
// Usage
var result = child({ name: 'a functional object' });
console.log(result.sayHello()); // "Hello, I'm a functional object"
由于 child()
同 base()
紧密耦合在一起,当你想添加 grandchild()
, greatGrandchild()
等时,你将面对类继承中许多常见的问题。
Mixin 函数解决函数继承的问题
Mixin 函数是一系列将新的属性或行为混入特定对象的组合函数。它不依赖或需要一个基础工厂方法或构造器,只需将任意对象传入一个 mixin 方法,它就会被扩展。
const flying = function (o) {
let isFlying = false
return Object.assign({}, o, {
fly () {
isFlying = true;
return this;
},
isFlying: () => isFlying,
land () {
isFlying = false;
return this;
}
});
};
const bird = flying({});
console.log(bird)
console.log( bird.isFlying() ); // false
console.log( bird.fly().isFlying() ); // true
这里需要注意,当调用 flying()
时需要传递一个被扩展的对象。Mixin 函数被设计用来实现函数组合,继续看下去。
const quacking = quack => o => Object.assign({}, o, {
quack: () => quack
});
const quacker = quacking('Quack!')({});
console.log( quacker.quack() ); // 'Quack!'
上面代码就相当于
const quacking = function(quack) {
return function quacking_in(o) {
return Object.assign({}, o, {
quack: function() {
return quack
}
});
}
}
const quacker = quacking('Quack')
console.log(quacker) // [Function: quacking_in]
const quacker_in = quacker()
console.log(quacker_in) // { quack: [Function: quack] }
console.log(quacker_in.quack()) //Quack
组合 Mixin 函数
通过简单的函数组合就可以将 mixin 函数组合起来。
但是,这看上去有点丑陋,调试或重新排列组合顺序也有点困难。
当然,这只是标准的函数组合,而我们可以通过一些好的办法来将它们组合起来,比如 compose()
或 pipe()
。如果,使用 pipe()
就需反转函数的调用顺序,才能保持相同的执行顺序。当属性冲突时,最后的属性生效。