边学边译JS工作机制--26. 多态的3种类型

249 阅读8分钟

本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988…

本文阅读指数:3
多态的概念其实比较成熟了,但是本文作者理解的多态似乎跟静态语言里的多态不尽相同。

在现实世界中,一个女性可以具有不同的角色。她可以是同时是妈妈,职工,妻子。根据不同的角色,她需要做不同的行为。这就是多态的概念,只是用另一种形式表现。

在JS中,多态这个概念并不广为人知,但是在面向对象编程中,它是非常核心的一个概念。

面向数据的编程语言,比如Rust。也会用Entity Component System (ECS)实现多态。JS中的编程写法具有不同的模式。在这个主题里,我们将会揭示多态是什么,在JS中怎么使用,多态的不同类型。

什么是多态

假如我们要计算一个区域的面积和周长,我们可能会定义两个方法area() 和 perimeter()。但是我们不可能用一套算法,来计算不同的形状。比如圆形和三角形的周长公式是不一样的。 那我们要先定义出基类 shapes,然后把不同的形状当做它的子类或派生类,他们具有各自的周长计算公式。这就叫多态。 在编程中,多态的定义是一个对象的能力具有多种模式。下一小节,我们会更加深入的理解JS是如何处理多态的。

JS如何处理多态

不同语言实现多态的方式有所不同。比如JS和JAVA都是面向对象的编程语言,但是实现多态的方式也不同。我们也会看到在封装和继承中多态是如何工作的。

多态和面向对象编程

面向对象模型依赖的概念是对象和类。通过this 或 self来改变它的数据字段

我们会对比一下,用JS实现多态和其他面向数据编程实现多态有什么区别(使用ECS)。 使用面向对象编程,我们可以创建一个类来计算不同形状的面积和周长:

image.png

class Shape {
  area() {
    return 0;
  }
  perimeter() {
    return 0;
  }
  toString() {
    return Object.getPrototypeOf(this).constructor.name;
  }
}

class Circle extends Shape {
  constructor(r) {
    super();
    this.radius = r;
  }

  area() {
    return Math.PI * this.radius ** 2;
  }

  perimeter() {
    return Math.PI * this.radius * 2;
  }
}

class Rectangle extends Shape {
  constructor(w, h) {
    super();
    this.width = w;
    this.height = h;
  }

  area() {
    return this.width * this.height;
  }
  perimeter() {
    return 2 * (this.width + this.height);
  }
}

function cumulateShapes(shapes) {
  return shapes.reduce((sum, shape) => {
    if (shape instanceof Shape) {
      console.log(`Shape: ${shape.toString()} - area: ${shape.area()}`);
      console.log(
        `Shape: ${shape.toString()} - perimeter: ${shape.perimeter()}`
      );
      return sum + shape.area();
    }
    throw Error("Bad argument shape.");
  }, 0);
}

const shapes = [new Circle(3), new Rectangle(2, 3)];

console.log(cumulateShapes(shapes));

image.png

如果使用ECS,那么会变成这样

var Position;
var Circle;
var Rectangle;

class CirlceSystem extends Position {
  OnUpdate() {
    ComponentQuery.SelectReadOnly(typeof Position, typeof Circle).ForEachEntity(
      (Entity, Position, Circle) => {
        /* find area and perimeter */
      }
    );
  }
}

class RectangleSystem extends Position {
  OnUpdate() {
    ComponentQuery.SelectReadOnly(
      typeof Position,
      typeof Rectangle
    ).ForEachEntity((entity, Position, Rectangle) => {
      /* find area and perimeter */
    });
  }
}

在JS代码中,我们使用了继承。在ECS代码中,我们使用了ECS模型来实体分发到组件中,解耦数据。

我们深入理解一下,JS中的继承,以及它如何跟多态相关

多态和继承

在面向对象的多态中,继承是非常重要的一个特性。 看一个car的例子

class Car {
  constructor(color, speed) {
    this._speed = speed;
    this._color = color;
  }
}

现在我们有不同的子类了,比如宝马,丰田,本田等等,他们的属性比如颜色和速度有所不同

class BMW extends Car {
  constructor(color, speed, make) {
    super(color, speed);
    this._make = make;
  }

  showInfo() {
    console.log(
      "I’m " +
        this._make +
        ", my color is " +
        this._color +
        ", and my speed is " +
        this._speed
    );
  }
}

class Toyota extends Car {
  constructor(color, speed, make) {
    super(color, speed);
    this._make = make;
  }

  showInfo() {
    console.log(
      "I’m " +
        this._make +
        ", my color is " +
        this._color +
        ", and my speed is " +
        this._speed
    );
  }
}

class Bentely extends Car {
  constructor(color, speed, make) {
    super(color, speed);
    this._make = make;
  }

  showInfo() {
    console.log(
      "I’m " +
        this._make +
        ", my color is " +
        this._color +
        ", and my speed is " +
        this._speed
    );
  }
}

现在,我们给我们的车添加不同的颜色,速度。

var myBentely = new Bentely('Red', '20mph', 'Bentely');
var myBMW = new BMW('Green', '40mph', 'BMW');
var myToyota = new Toyota('White', '60mph', 'Toyota');
console.log(myBentely.showInfo()); 
console.log(myBMW.showInfo());  
console.log(myToyota.showInfo());

image.png 在例子中,子类获取了父类的属性,并定义它。继承可以由当前的从父类甚至祖父类中派生。

JS种继承的类型

JS 继承时一个很大的话题,有很多不同的实现方式,比如基于原型的,类的(虚假的类),函数的。我们简单看一下区别,以及他们如何实现多态:

1.  原型继承 原型的比较简单,就在原型上加方法就可以了

let Car = {
  color: "Red",
};
let BMW = {
  make: "BMW",
};

BMW.__proto__ = Car;

// we can find both properties in BMW now:
console.log("This is a " + BMW.color + " " + BMW.make);

image.png 2.  类继承

前面说过,JS种的类时为伪概念,语法糖,所以我们称之为伪类。 class的实现是基于new关键字,但是调用的是一个函数。比如,我们有一个car对象。

function Car(make, color, speed) {
  this.make = make;
  this.color = color;
  this.speed = speed;
}

我们可以使用new 关键字,来给他创建不同的子类。

var Toyota = new Car ("Toyota", "Red", "100mph");
var Bentley = new Car ("Bentley", "White", "120mph");
var BMW = new Car ("BMW", "Green", "90mph");

使用原型,我们创建了不同的car对象。下一步,我们会看一下如何像继承意向传递原型,以及这样做对多态有什么样的影响 首先,我们创建一个dialogue函数,让我们的card继承它。

function dialogue() {
  console.log('I am ' + this.make);
}

利用原型,让我们的cars来继承它

Car.prototype.dialogue = function () {
  console.log(
    "I am a " +
      this.color +
      "  " +
      this.make +
      " with  " +
      this.speed +
      "  speed "
  );
};
console.log(Toyota.dialogue());
console.log(BMW.dialogue());
console.log(Bentley.dialogue());

image.png

3.  基于函数的继承

基于函数的继承,是给对象加上增强函数

function Person(data) {
  var that = {};
  that.name = data.name;
  return that;
}

// Create a child object, to inherit from the base Person
function Employee(data) {
  var that = Person(data);
  that.sayHello = function () {
    return "Hello, I'm " + that.name;
  };
  return that;
}

var myEmployee = Employee({ name: "Rufi" });
console.log(myEmployee.sayHello());

image.png

多态和封装

理解了继承,再理解封装就很容易了。在写代码的时候,我们经常需要把一些代码封装起来,这样用户从外面就无法访问里面的值。

例如,我们把验证学生特征的数据组合在一起,然后使用基于原型多态的方式来继承。

function Student(name, marks) {
  var student_name = name;
  var student_marks = marks;
  Object.defineProperty(this, "name", {
    get: function () {
      return student_name;
    },
    set: function (student_name) {
      this.student_name = student_name;
    },
  });

  Object.defineProperty(this, "marks", {
    get: function () {
      return student_marks;
    },
    set: function (student_marks) {
      this.student_marks = student_marks;
    },
  });
}
var stud = new Student("Mercy's score is: ", 60);
console.log(stud.name + " " + stud.marks);

image.png

这个例子很好的帮助我们理解JS中的封装和多态。 很多人不理解抽象和封装的区别。抽象,只能看到一部分信息,其他的部分被隐藏了。而封装,是把数据包带一个单独的实体中,外界无法访问。使用封装最主要的原因是控制和校验数据--就像上面的例子一样。

多态的类型

JS中实现多态有多种方式,我们讨论以下几种

Ad-hoc Polymorphism(特设多态)

特设多态是指'视觉上'不同的类型,表现的行为也是不同的。特设多态可以包含同名的,但是参数或返回值不同的函数。

这种类型也被叫做重载,我们看一个操作符的重载。

Operator Overloading

5 + 5; // will print 10
 'I am' + ' ' + '5 years old' // will print I am 5 years old

在上面的例子中,+ 表示了数字相加以及字符串拼接两种范式。

参数化多态

参数化多态处理普通的函数和数据类型,同时维持静态类型安全。普通函数和数据类型,可以被重写,所以不会基于他们的类型进行区分对待。 例如,对象保存了不同的数据类型。它不会基于他们的类型来区分他们的值。

const Ann = {
firstName: 'Ann',
lastName: 'Kim',
Age: 4,
Adult: false
}

上面的Ann对象,包含了Ann的名字-字符串类型,年龄--数字类型,是否成年--布尔类型。尽管他们的类型不同,但是对于对象说,处理的方式是差不多的。 类似的例子还有数组。在JS中,数组可以承载不同的元素。 const Ann = [‘Ann’, ‘Kim’, 4, false];

数组对它包含的元素处理也是类似的,如果你在控制台运行console.log(Ann),能发现所有的元素都被打印出来。

image.png

image.png 看另外一个例子

const id = (x) => x;
id(1); // 1
id("foo"); // "foo"

 id不会因为参数1 and foo的类型来判断他们的值。所以你可以给 id 传入不同类型的参数。

子类型多态

子类型多态包含子类型和子类型数据类型。它不会包含新对象的创建,主要基于接口的实现,以及不同的实现方式。

假如你获得了亲人的遗产---一个书店。那么你可以查阅里面的书,查阅遗产账户,书店客户等等,这叫做继承,你获得了遗产里所有的东西。

假如亲人这份遗产没有给你,你可以选择自己重新开一个,然后承担起你亲戚原来的角色,但是根据自己的喜欢做一些改变---这叫子类型。

看个例子

function Animal () {

}

Animal cat = new Cat ("Kitty");
Animal Dog = new Cat ("puppy");
Animal cat = new Cat ("Kiddy");

//you can go ahead to create different properties for different animals

cat ,dog,goat都是animals的子类型。一个animal可以是任何一个。你可以不同的animal做不同的实现。

常见的JS多态陷阱

我们大概的讲了一下多态,但是也要谨记一些陷阱:

  1. 多态会影响你代码的性能。一个单一的函数要比多态函数运行的快。
  2. 多态会降低代码的可阅读性。为了解决这个问题,所以在使用多态时写好注释。
  3. JS中实现多态很容易,但是要理解继承。因为JS中多态时围绕继承实现的。

为什么使用多态

为了复用。 一方面,因为要使用继承,提升了代码的复用能力。 另一方面,可以把不同类型的数据放在一起处理。比如我们熟悉的数组。

const Ann = [‘Ann’, ‘Kim’, 4, false];

程序中使用多态,最主要的还是让程序扩展性和维护性更好。