随着期待已久的ES2015(以前称为ES6)到来,JavaScript 配备了专门用于定义 类(classes) 的语法。在这篇文章中,我将探索如何利用类语法用较小的部分来组成一个类。
将层次结构的深度保持在最低限度对于保持代码整洁很重要。这有助于你了解如何巧妙的分割类。对于一个大的代码库,一个选择是用较小的部分创建类;再组合成一个大类。这也是避免重复代码的常见策略。
想像一下,我们正在构建一个游戏,玩家生活在动物世界中。有些是朋友,其他的是敌人(像狗,可能会说所有的猫都是敌人)。我们可以创建一个 HostileAnimal
类,它扩展了类 Animal
,作为 Cat
的基类。在某些时候,我们决定添加机器人来伤害人类。我们首先做的是创建 Robot
类。我们现在有两个类具有相似的属性。例如,HostileAnimal
和 Robot
都有 attack()
(攻击)方法。
如果我们可以以某种方式在一个单独的类或对象中定义一个称为 Hostile
类来表示敌对, 那么我们可以让 Cat
和 Robot
可以重复使用这个类。我们可以以各种方式做到这一点。
多重继承 是传统的 OOP 语言支持的一项功能。顾名思义,它给我们带来一种能力:创建一个继承自多个基类的类。看看 Cat
类如何在以下 Python 代码中扩展多个基类:
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 语言一个常见的特性。它允许我们定义一个类应该包含什么方法(有时是属性)。如果那个类没有方法或者属性,编译器会抛出错误。以下 TypeScript 代码中,如果 Cat
没有 attack()
或 walk()
方法,就会抛出一个错误。
interface Hostile { attack(); } class Animal { walk(); } class Dog extends Animal { // ... } class Cat extends Animal implements Hostile { attack() { // ... } }
多重继承受到 的困扰(其中两个父类定义相同的方法)。一些语言通过实现其他策略(如 mixins)来规避这个问题。Mixins(混入)是只包含一些方法的微小类。而不是扩展这些类,mixins 被包含在另一个类中。例如在 PHP 中,使用Traits 实现 mixins 。
class Animal { // ... } trait Hostile { // ... } class Dog extends Animal { // ... } class Cat extends Animal { use Hostile; // ... } class Robot { use Hostile; // ... }
一个新的轮子: ES2015 Class 语法
如果你没有了解 ES2015 classes 或觉得你对这些语法还不是很了解的话,请务必先阅读 Jeff Mott 的 (注:中文译文) 后继续。
概括地说:
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 Classes
接口可以通过创建一个虚拟方法来模拟,但会抛出错误。一旦继承后,该函数必须被覆盖以避免这个错误:
class IAnimal { walk() { throw new Error('Not implemented'); } } class Dog extends IAnimal { // ... } const robbie = new Dog(); robbie.walk(); // Throws an error
如前所述,这种方法依赖于继承。要继承多个类,我们将需要多重继承或 mixins(混入) 。
另一种方法是编写一个实用函数,在定义了一个类之后验证它。
一个例子可以在 Andrea Giammarchi 的 文章中找到,请参见“A Basic Object.implement Function Check.”一节。
探索应用多重继承和 mixins(混入) 的各种方法。以下所有调查的策略均可在 上获得。
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", 因为 "myOwnProperty" 是 "myInstance" 的一个属性 console.log(myInstance.hasOwnProperty('myOwnProperty')); // logs "false", 因为 "myProtoProperty" 不是 "myInstance" 的一个属性, 但是,他是 "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, });
另一种方法是抛弃类,使用对象作为 mixins(混入)。一个确定的副作用是 mixin 对象不能用于创建实例,防止滥用。
const Animal = { walk() { // ... }, }; const Hostile = { attack(target) { // ... }, }; class Cat { // ... } Object.assign(Cat.prototype, Animal, Hostile);
优点
- Mixins(混入) 不能被初始化
缺点
- 需要一个额外的代码行
Object.assign()
有点晦涩- 重新设计原型继承来与 ES2015 classes 结合使用
在构造函数中组合对象
使用 ES2015 classes,您可以通过在构造函数中返回一个对象来覆盖实例:
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-这里声明明确的属性和方法 // ... 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 classes
类工厂函数
这种方法利用 JavaScript 在运行时定义一个类的能力。
首先,我们将需要基类。在我们的例子中,Animal
和 Robot
作为基类。如果你想从头开始,一个空类也可以工作。
class Animal { // ... } class Robot { // ... }
接下来,我们必须创建一个工厂函数,返回一个扩展 Base
类的新类,作为参数传递。这些是 mixins :
const Hostile = (Base) => class Hostile extends Base { // ... };
现在我们可以将任何类传递给 Hostile
函数,它将返回一个新的类,这个类结合了 Hostile
和我们传递给函数的任何类:
class Dog extends Animal { // ... } class Cat extends Hostile(Animal) { // ... } class HostileRobot extends Hostile(Robot) { // ... }
我们可以通过几个 classes 应用多个 mixins :
class Cat extends Demonic(Hostile(Mammal(Animal))) { // ... }
你也可以使用 Object
作为一个基类:
class Robot extends Hostile(Object) { // ... }
优点
- 更容易理解,因为所有信息都在类的头部声明
缺点
- 在运行时创建类可能会影响启动性能 和/或 内存使用
结论
当我决定研究这个话题并撰写一篇文章时,我期望 JavaScript 的原型模型有助于生成类。因为类语法使得方法不可枚举,对象操作变得更加困难,几乎不切实际。
类语法可能会让人产生 JavaScript 是基于类的 OOP 语言的错觉,但他不是。大多数方法,您必须修改对象的原型来模拟多个继承。最后一种方法,使用类工厂函数,是使用 mixins 组合 classes 一个可接受的策略。 注:了解更多关于 JavaScript中的工厂函数 。
如果您发现基于原型的本赛季限制,你可能想看看你的倾向。原型提供无与伦比的灵活性,您可以利用它。
如果由于任何原因,你还是喜欢传统的本赛季语言,您可能需要学习一下编译为 JavaScript 的语言。例如,,它是 JavaScript 的一个超集,添加(可选)静态类型 和从其他传统的 OOP 语言中借鉴的模式。
您将在项目中使用上述方法之一吗?你有没有找到更好的方法?欢迎评论!
推荐阅读最新关于 ECMAScript 的文章
- ECMAScript 2015(ES6)的十大特征
- JavaScript ES6(ES2015)入门-核心特性概述
- ES6 新特性范例大全
- 现在就可以使用的5个 ES6 特性
- 面向对象的 JavaScript – 深入了解 ES6 类
- 使用 ES2017 中的 Async(异步) 函数 和 Await(等待)
- JavaScript ECMAScript 2015 (ES6) 和 ECMAScript 2016 (ES7) 新特性速查表
- ECMAScript 6 Modules(模块)系统及语法详解
- 学习 ES2015 新特性
- JavaScript ES2015 中对象继承的模式
- JavaScript 新书:探索 ES2016 与 ES2017(包含了ES2016 与 ES2017 的最新特性)
原文链接:
最新评论
写的挺好的
有没有兴趣翻译 impatient js? https://exploringjs.com/impatient-js/index.html
Flexbox playground is so great!
感谢总结。
awesome!
这个好像很早就看到类似的文章了
比其他的教程好太多了
柯理化讲的好模糊…没懂