随着人们期待已久的ES2015(以前称为ES6)的到来,JavaScript配备了专门用于定义类的语法。 在本文中,我将探讨是否可以利用类语法从较小的部分组成类。
保持层次结构深度最小对于保持代码干净很重要。 明智地安排班级对您有所帮助。 对于大型代码库,一种选择是从较小的部分中创建类。 作文课。 这也是避免重复代码的常见策略。
想象一下,我们正在构建一个游戏,其中玩家生活在动物世界中。 有些是朋友,有些则是敌对的(像我这样的狗人可能会说所有的猫都是敌对的生物)。 我们可以创建一个HostileAnimal
类,该类扩展了Animal
,作为Cat
的基类。 在某个时候,我们决定添加旨在伤害人类的机器人。 我们要做的第一件事是创建Robot
类。 现在,我们有两个具有相似属性的类。 HostileAnimal
, HostileAnimal
和Robot
都能够attack()
。
如果我们可以以某种方式在单独的类或对象中定义敌对性,比如说Hostile
,我们可以将Cat
和Robot
重用。 我们可以通过各种方式做到这一点。
多重继承是某些经典OOP语言支持的功能。 顾名思义,它使我们能够创建从多个基类继承的类。 在以下Python代码中查看Cat
类如何扩展多个基类:
class Animal(object):
def walk(self):
# ...
class Hostile(object):
def attack(self, target):
# ...
class Dog(Animal):
# ...
class Cat(Animal, Hostile):
# ...
dave = Cat();
dave.walk();
dave.attack(target);
接口是(类型化的)经典OOP语言中的常见功能。 它允许我们定义一个类应该包含哪些方法(有时是属性)。 如果没有该类,则编译器将引发错误。 如果Cat
不具有attack()
或walk()
方法,则以下TypeScript代码将引发错误:
interface Hostile {
attack();
}
class Animal {
walk();
}
class Dog extends Animal {
// ...
}
class Cat extends Animal implements Hostile {
attack() {
// ...
}
}
多重继承遭受菱形问题 (其中两个父类定义相同的方法)。 一些语言通过实现其他策略来避免这个问题,例如mixins 。 Mixins是仅包含方法的微小类。 除了扩展这些类之外,mixin还包含在另一个类中。 例如,在PHP中,mixin是使用Traits实现的。
class Animal {
// ...
}
trait Hostile {
// ...
}
class Dog extends Animal {
// ...
}
class Cat extends Animal {
use Hostile;
// ...
}
class Robot {
use Hostile;
// ...
}
回顾:ES2015类语法
如果您没有机会深入研究ES2015类或感到对它们不了解,请确保在继续之前阅读Jeff Mott的“ 面向对象的JavaScript —深入ES6类” 。
简而言之:
-
class Foo { ... }
描述了一个名为Foo
的类 -
class Foo extends Bar { ... }
描述了一个类Foo
,它扩展了另一个类Bar
在类块中,我们可以定义该类的属性。 对于本文,我们只需要了解构造函数和方法:
-
constructor() { ... }
是一个保留函数,在创建时执行(new Foo()
) -
foo() { ... }
创建一个名为foo
的方法
类语法主要是JavaScript原型模型上的语法糖。 它没有创建类,而是创建了一个函数构造函数:
class Foo {}
console.log(typeof Foo); // "function"
这里的要点是,JavaScript不是基于类的OOP语言。 甚至有人可能认为语法具有欺骗性,给人以为是欺骗性的印象。
编写ES2015类
可以通过创建引发错误的伪方法来模仿接口。 继承后,必须重写该函数以避免该错误:
class IAnimal {
walk() {
throw new Error('Not implemented');
}
}
class Dog extends IAnimal {
// ...
}
const robbie = new Dog();
robbie.walk(); // Throws an error
如前所述,这种方法依赖于继承。 要继承多个类,我们将需要多个继承或混合。
另一种方法是编写定义类后验证类的实用程序函数。 可以在“ 等待片刻”中找到一个示例,JavaScript不支持多重继承! 由Andrea Giammarchi撰写。 请参见“基本对象。实现功能检查”部分。
是时候探索各种方法来应用多个继承和混合。 以下所有检查的策略都可以在GitHub上找到 。
Object.assign(ChildClass.prototype, Mixin...)
在ES2015之前,我们使用了原型进行继承。 所有函数都具有prototype
属性。 使用new MyFunction()
创建实例时,将prototype
复制到实例中的属性。 当您尝试访问实例中没有的属性时,JavaScript引擎将尝试在原型对象中查找它。
为了演示,请看下面的代码:
function MyFunction () {
this.myOwnProperty = 1;
}
MyFunction.prototype.myProtoProperty = 2;
const myInstance = new MyFunction();
// logs "1"
console.log(myInstance.myOwnProperty);
// logs "2"
console.log(myInstance.myProtoProperty);
// logs "true", because "myOwnProperty" is a property of "myInstance"
console.log(myInstance.hasOwnProperty('myOwnProperty'));
// logs "false", because "myProtoProperty" isn’t a property of "myInstance", but "myInstance.__proto__"
console.log(myInstance.hasOwnProperty('myProtoProperty'));
这些原型对象可以在运行时创建和修改。 最初,我尝试对Animal
和Hostile
使用类:
class Animal {
walk() {
// ...
}
}
class Dog {
// ...
}
Object.assign(Dog.prototype, Animal.prototype);
上面的方法不起作用,因为类方法不可枚举 。 实际上,这意味着Object.assign(...)
不会从类中复制方法。 这也使得创建将方法从一个类复制到另一个类的函数变得困难。 但是,我们可以手动复制每个方法:
Object.assign(Cat.prototype, {
attack: Hostile.prototype.attack,
walk: Animal.prototype.walk,
});
另一种方法是放弃类并将对象用作混合。 一个积极的副作用是,不能使用混合对象来创建实例,从而防止滥用。
const Animal = {
walk() {
// ...
},
};
const Hostile = {
attack(target) {
// ...
},
};
class Cat {
// ...
}
Object.assign(Cat.prototype, Animal, Hostile);
优点
- Mixins无法初始化
缺点
- 需要额外的一行代码
- Object.assign()有点晦涩
- 重塑原型继承以与ES2015类一起使用
在构造函数中合成对象
使用ES2015类,可以通过在构造函数中返回一个对象来覆盖实例:
class Answer {
constructor(question) {
return {
answer: 42,
};
}
}
// { answer: 42 }
new Answer("Life, the universe, and everything");
我们可以利用该功能从子类中的多个类组成一个对象。 请注意, Object.assign(...)
仍然无法与mixin类一起使用,因此我也在这里使用了对象:
const Animal = {
walk() {
// ...
},
};
const Hostile = {
attack(target) {
// ...
},
};
class Cat {
constructor() {
// Cat-specific properties and methods go here
// ...
return Object.assign(
{},
Animal,
Hostile,
this
);
}
}
由于this
是在上述上下文中引用的类(具有不可枚举的方法),因此Object.assign(..., this)
不会复制Cat
的方法。 相反,您将必须this
明确设置字段和方法,以便Object.assign()
能够应用这些字段和方法,如下所示:
class Cat {
constructor() {
this.purr = () => {
// ...
};
return Object.assign(
{},
Animal,
Hostile,
this
);
}
}
这种方法不切实际。 因为您要返回的是新对象而不是实例,所以它实质上等效于:
const createCat = () => Object.assign({}, Animal, Hostile, {
purr() {
// ...
}
});
const thunder = createCat();
thunder.walk();
thunder.attack();
我认为我们可以同意后者更具可读性。
优点
- 它有效,我猜呢?
缺点
- 很晦涩
- ES2015类语法的零收益
- ES2015类的滥用
类工厂功能
这种方法利用了JavaScript在运行时定义类的能力。
首先,我们将需要基类。 在我们的示例中, Animal
和Robot
作为基类。 如果您想从头开始,那么也可以使用空类。
class Animal {
// ...
}
class Robot {
// ...
}
接下来,我们必须创建一个工厂函数,该函数将返回一个扩展了Base
类的新类,该类作为参数传递。 这些是mixin:
const Hostile = (Base) => class Hostile extends Base {
// ...
};
现在我们可以将任何类传递给Hostile
函数,该函数将返回一个新的类,将Hostile
和我们传递给该函数的任何类组合在一起:
class Dog extends Animal {
// ...
}
class Cat extends Hostile(Animal) {
// ...
}
class HostileRobot extends Hostile(Robot) {
// ...
}
我们可以通过几个类来应用多个mixin:
class Cat extends Demonic(Hostile(Mammal(Animal))) {
// ...
}
您还可以将Object
用作基类:
class Robot extends Hostile(Object) {
// ...
}
优点
- 易于理解,因为所有信息都在类声明标头中
缺点
- 在运行时创建类可能会影响启动性能和/或内存使用率
结论
当我决定研究此主题并撰写有关此主题的文章时,我期望JavaScript的原型模型对生成类有帮助。 由于类语法使方法不可枚举,因此对象操作变得更加困难,几乎不切实际。
类语法可能会产生这样的错觉,即JavaScript是基于类的OOP语言,但事实并非如此。 使用大多数方法,您将必须修改对象的原型以模仿多重继承。 最后一种方法是使用类工厂函数,这是使用mixin组成类的可接受策略。
如果您发现基于原型的编程存在限制,则可能需要考虑一下自己的心态。 原型提供了您可以利用的无与伦比的灵活性。
如果出于某种原因仍然喜欢经典编程,则可能需要研究可编译为JavaScript的语言。 例如, TypeScript是JavaScript的超集,它添加了(可选)静态类型和模式,您可以从其他经典OOP语言中识别出这些类型。
您将在项目中使用以上两种方法之一吗? 您找到更好的方法了吗? 在评论中让我知道!
Jeff Mott , Scott Molinari , Vildan Softic和Joan Yin 对此文章进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
From: https://www.sitepoint.com/patterns-object-inheritance-javascript-es2015/