您的位置:首页 » 分类: JavaScript & ES2015 (ES6) » 文章: JavaScript ES2015 中对象继承的模式

JavaScript ES2015 中对象继承的模式

小编推荐:掘金是一个高质量的技术社区,从 ECMAScript 6 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。各大应用市场搜索「掘金」即可下载APP,技术干货尽在掌握..

随着期待已久的ES2015(以前称为ES6)到来,JavaScript 配备了专门用于定义 类(classes) 的语法。在这篇文章中,我将探索如何利用类语法用较小的部分来组成一个类。

将层次结构的深度保持在最低限度对于保持代码整洁很重要。这有助于你了解如何巧妙的分割类。对于一个大的代码库,一个选择是用较小的部分创建类;再组合成一个大类。这也是避免重复代码的常见策略。

想像一下,我们正在构建一个游戏,玩家生活在动物世界中。有些是朋友,其他的是敌人(像狗,可能会说所有的猫都是敌人)。我们可以创建一个 HostileAnimal 类,它扩展了类 Animal,作为 Cat 的基类。在某些时候,我们决定添加机器人来伤害人类。我们首先做的是创建 Robot 类。我们现在有两个类具有相似的属性。例如,HostileAnimalRobot 都有 attack() (攻击)方法。

如果我们可以以某种方式在一个单独的类或对象中定义一个称为 Hostile 类来表示敌对, 那么我们可以让 CatRobot 可以重复使用这个类。我们可以以各种方式做到这一点。

多重继承 是传统的 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() {
    // ...
  }
}

多重继承受到 菱形(diamond) 继承问题 的困扰(其中两个父类定义相同的方法)。一些语言通过实现其他策略(如 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 的 面向对象JavaScript- 深入潜入 ES6 classes (愚人码头注:中文译文) 后继续。

概括地说:

  • 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 的 等待片刻,JavaScript支持多重继承! 文章中找到,请参见“A Basic Object.implement Function Check.”一节。

探索应用多重继承和 mixins(混入) 的各种方法。以下所有调查的策略均可在 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", 因为 "myOwnProperty" 是 "myInstance" 的一个属性
console.log(myInstance.hasOwnProperty('myOwnProperty'));
// logs "false", 因为 "myProtoProperty" 不是 "myInstance" 的一个属性, 但是,他是 "myInstance.__proto__" 的一个属性
console.log(myInstance.hasOwnProperty('myProtoProperty'));

这些原型对象可以在运行时创建和修改。首先,我尝试使用 AnimalHostile 类:

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 在运行时定义一个类的能力。

首先,我们将需要基类。在我们的例子中,AnimalRobot 作为基类。如果你想从头开始,一个空类也可以工作。

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 的语言。例如,TypeScript,它是 JavaScript 的一个超集,添加(可选)静态类型 和从其他传统的 OOP 语言中借鉴的模式。

您将在项目中使用上述方法之一吗?你有没有找到更好的方法?欢迎评论!

推荐阅读最新关于 ECMAScript 的文章

原文链接:https://www.sitepoint.com/patterns-object-inheritance-javascript-es2015/

正文完。下面还有一个推广让最好的人才遇见更好的机会!

互联网行业的年轻人,他们面对着怎样的职业瓶颈、困惑与未来选择?过去,这鲜有人关心。资深的职场人,也多半优先选择熟人去推荐机会。

100offer致力于改变现状,帮互联网行业最好的人才发现更好的机会。使用 100offer.com 或 100offer App ,可以一周内获得中国、美国等数千家优质企业的工作机会。

马上去遇见更好的机会
推广结束

关注WEB前端开发官方公众号

关注国内外最新最好的前端开发技术干货,获取最新前端开发资讯,致力于打造高质量的前端技术分享公众号

发表评论

电子邮件地址不会被公开。 必填项已用*标注