一文弄懂TypeScript中的混合(Mixin)
<p><strong>1.前言</strong></p><p>由于TypeScrip中的类不支持多继承,所以引入了混合(Mixin)的特性,可以间接实现多继承的效果。</p>
<p><strong>2.正文</strong></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明一个汽车类Vehicle,它有drive方法</span>
<span style="color: rgba(0, 0, 0, 1)">class Vehicle {
drive(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'驾驶'<span style="color: rgba(0, 0, 0, 1)">)
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明轿车类Car,它有work方法</span>
<span style="color: rgba(0, 0, 0, 1)">class Car extends Vehicle{
work(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'上班代步'<span style="color: rgba(0, 0, 0, 1)">)
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明越野车类Suv,它有cross方法</span>
<span style="color: rgba(0, 0, 0, 1)">class Suv extends Vehicle{
cross(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'山地越野'<span style="color: rgba(0, 0, 0, 1)">)
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明跨界车类Crossover, 继承Car和Suv,既可以上班代步,也可以山地越野</span>
class Crossover extends Car, Suv{ <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这里ts报错:Classes can only extend a single class. 子类只能继承一个父类</span>
<span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<p>以上示例为了实现让Crossover类同时拥有work和cross方法,我们尝试一个类同时继承多个类,ts报错了,下面利用Mixin的特性,来实现这一功能:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">class Vehicle {
drive(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'驾驶'<span style="color: rgba(0, 0, 0, 1)">)
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明轿车类Car,它有work方法</span>
<span style="color: rgba(0, 0, 0, 1)">class Car extends Vehicle{
work(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'上班代步'<span style="color: rgba(0, 0, 0, 1)">)
}
getSpace(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'small'<span style="color: rgba(0, 0, 0, 1)">);
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明越野车类Suv,它有cross方法</span>
<span style="color: rgba(0, 0, 0, 1)">class Suv extends Vehicle{
cross(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'山地越野'<span style="color: rgba(0, 0, 0, 1)">)
}
getSpace(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'big'<span style="color: rgba(0, 0, 0, 1)">);
}
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明跨界车类Crossover, 继承Car和Suv,既可以上班代步,也可以山地越野</span>
<span style="color: rgba(0, 0, 0, 1)">class Crossover extends Vehicle {
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这里需要创建需要合并类(Crossover)的同名接口,并继承多个类Car、Suv</span>
<span style="color: rgba(0, 0, 0, 1)">interface Crossover extends Car, Suv {}
applyMixins(Crossover, )
let crossover </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Crossover()
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 该混合函数摘自TypeScript官方手册,如果derivedCtor和constructors类中存在同名方法,则后者覆盖前者</span>
<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) </span>=><span style="color: rgba(0, 0, 0, 1)"> {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) </span>=><span style="color: rgba(0, 0, 0, 1)"> {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) </span>||<span style="color: rgba(0, 0, 0, 1)">
Object.create(</span><span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">)
);
});
});
}
crossover.drive() </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 驾驶</span>
crossover.work() <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 上班代步</span>
crossover.cross() <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 山地越野</span>
crossover.getSpace() <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> big,同名方法getSpace是Suv中的,applyMixins函数传入的第二个参数数组最后一个类是Suv</span></pre>
</div>
<p>以上Crossover类通过Mixin实现继承Car和Suv中的方法,并且也继承了第二级类Vehicle中的方法,需要注意的是,对于同名的方法,会存在方法覆盖的问题。</p>
<p>有的同学会对上面Crossover接口有疑问,为什么要声明跟类名一样的接口呢?作用在哪?</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这是混合最基础的代码,声明接口,让Crossover实现接口</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 声明接口MergeInterface</span>
<span style="color: rgba(0, 0, 0, 1)">interface MergeInterface extends Car, Suv { }
class Crossover implements MergeInterface {
work(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> { }
getSpace(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> { }
drive(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> { }
cross(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> { }
}
applyMixins(Crossover, )
let crossover </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Crossover()
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 以上代码中不妨将接口名MergeInterface改为Crossover与类名一致,ts中允许接口名和类名相同,因为它们处于不同的命名空间下,互相不影响</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> =></span>
<span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 接口名称改为和类名一致,俩个互不影响</span>
<span style="color: rgba(0, 0, 0, 1)">interface Crossover extends Car, Suv { }
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 由于类名和接口名一致,不需要实现接口Crossover中的方法,省去书写函数签名的时间</span>
<span style="color: rgba(0, 0, 0, 1)">class Crossover implements Crossover {}
applyMixins(Crossover, )
let crossover </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Crossover()
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 当类名和接口名相同,implements Crossover可以省略,如下</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> =></span>
<span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">interface Crossover extends Car, Suv { }
class Crossover {}
applyMixins(Crossover, )
let crossover </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Crossover()
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这就是最开始的代码,它是由以上几步简化而来。</span></pre>
</div>
<p>在 TypeScript 中,使用跟类同名的接口来实现 mixin 模式是一种约定俗成的做法,它带来了一些好处:</p>
<ol>
<li><strong>语义清晰</strong>: 使用跟类同名的接口可以使代码的意图更加明确。接口的名称与类名相同时,直观上能够表示接口与该类有关联,是该类的一种扩展或混入。</li>
<li><strong>类型推断和检查</strong>: TypeScript 的类型系统可以更好地推断和检查类型,通过将类名和接口名相同,TypeScript 可以更准确地判断类是否实现了特定的接口。这对于 mixin 模式很重要,因为它确保 mixin 提供的功能被正确应用于类。</li>
<li><strong>保持一致性</strong>: 在代码中保持类名和接口名相同可以遵循一致性原则。当你查看代码时,可以快速理解类及其扩展的接口之间的关系。</li>
</ol>
<p> </p>
<p><strong>3.拓展</strong></p>
<p>利用Mixin的思想还能做一些有趣的事儿!</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface AnimalConfig {
type: string
name: string
voice: string
}
class AnimalSpeak {
speak(config: AnimalConfig): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
const {type, name, voice} </span>=<span style="color: rgba(0, 0, 0, 1)"> config
console.log(`${type </span>+ name}的叫声是${voice}~<span style="color: rgba(0, 0, 0, 1)">`);
}
dogSpeak(name: string): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(`小狗${name}的叫声是汪汪</span>~<span style="color: rgba(0, 0, 0, 1)">`);
}
catSpeak(name: string): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
console.log(`小猫${name}的叫声是喵喵</span>~<span style="color: rgba(0, 0, 0, 1)">`);
}
}
</span><span style="color: rgba(0, 0, 255, 1)">function</span> extend<T, U><span style="color: rgba(0, 0, 0, 1)">(to: T, from: U) {
</span><span style="color: rgba(0, 0, 255, 1)">for</span> (const key <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> from) {
;(to as T </span>& U) =<span style="color: rgba(0, 0, 0, 1)"> from as any
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> to as T &<span style="color: rgba(0, 0, 0, 1)"> U
}
const context </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> AnimalSpeak()
const instance </span>=<span style="color: rgba(0, 0, 0, 1)"> AnimalSpeak.prototype.speak.bind(context)
extend(instance, context)
instance({type: </span>'小猫', name: '咪咪', voice: '喵喵'}) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 小猫咪咪的叫声是喵喵~</span>
<span style="color: rgba(0, 0, 0, 1)">
instance.dogSpeak(</span>'哮天犬') <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 小狗哮天犬的叫声是汪汪~</span></pre>
</div>
<p>以上示例中context是AnimalSpeak类的实例对象,instance是一个方法,利用extend方法,将context对象中的属性方法赋值给instance,这样instance既可以当做方法使用,也可以当做对象调用其中的方法,非常灵活!</p>
<p><strong>脚踏实地行,海阔天空飞~</strong></p>
<p> </p><br><br>
来源:https://www.cnblogs.com/coder--wang/p/17713418.html
頁:
[1]