串联继承与 class 中的 JavaScript 继承

Concatenative inheritance vs class inheritance in JavaScript

当我一开始看到它时,串联继承对我来说就像一个组合,但人们一直将其命名为继承。 classes,但是,使用原型来创建将对象连接在一起的原型链。现在的问题是,如果串联继承和 class 继承都做同样的事情,应该使用哪一个? 这是两种情况的示例
串联继承

function Person(name, address) {
 const _name = name
 const _address = address
 const toString = () => `name: ${this.name}, address: ${this.address}`
 return {
   _name,
   _address,
   toString
 }
}


function Employee(name, address, salary) {
 const getAnnualSalary = () => 12 * salary
 return Object.assign({ getAnnualSalary }, Person(name, address))
}



class继承


class Person {
  constructor(name, address) {
    this.name = name
    this.address = address
  }
  toString() { return `name: ${this.name}, address: ${this.address}` }
}


class Employee extends Person {
  constructor(name, address, salary) {
    super(name, address)
    this.salary = salary
  }

  getAnnualSalary() { return 12 * this.salary }
}

以下解释力求简明扼要。

让我们首先关注 Person 的不同实现,同时让我们从基于 class 的版本开始,因为它的实现与 in 不同它的 factory 对应方之一的许多方面容易出错。

class Person { ... toString() { ... } } 具有 Person 类型特定的 toString 方法。后者作为 Personprototype 方法实现。因此,任何像 myPerson 这样的 Person 实例 not 具有其 own toString 方法。

万一 toString 在 e 被调用。 G。 myPerson,将在这个实例中查找该方法'原型链。因为在 Person.prototype.toString 处(立即)找到了该方法,所以它会在 myPerson 的上下文中自动被调用(也可以通过显式调用来实现... Person.prototype.toString.call(myPerson);)。

class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
  toString() {
    return `name: ${ this.name }, address: ${ this.address }`
  }
}
const myPerson = new Person('John Doe', '123 Main St Anytown');

console.log(
  'Object.keys(myPerson) :',
  Object.keys(myPerson)
);
console.log('\n');

console.log(
  "myPerson.hasOwnProperty('toString') ?",
  myPerson.hasOwnProperty('toString')
);
console.log(
  "Person.prototype.hasOwnProperty('toString') ?",
  Person.prototype.hasOwnProperty('toString')
);
console.log('\n');

// automatic protoypal delegation, hence an inherited method.
console.log(
  'myPerson.toString() :',
  myPerson.toString()
);

// explicit protoypal delegation ... easy and expectable.
console.log(
  'Person.prototype.toString.call(myPerson) :',
  Person.prototype.toString.call(myPerson)
);
console.log('\n');

// explicit protoypal delegation ... with an *alien* object.
console.log(
`Person.prototype.toString.call({
  name: 'Jane Doe',
  address: '123 Main St Anytown',
}) :`,
Person.prototype.toString.call({
  name: 'Jane Doe',
  address: '123 Main St Anytown',
}));
.as-console-wrapper { min-height: 100%!important; top: 0; }

关于 OP 提供的 Personfactory 实现,必须对代码进行评论并且还需要对其进行消毒(,消毒部分原因是基于意见)...

function Person(name, address) {
  const _name = name;
  const _address = address;
  const toString = () => `name: ${ this.name }, address: ${ this.address }`
  return {
    _name,
    _address,
    toString
  };
}
const myPerson = Person('John Doe', '123 Main St Anytown');

console.log('myPerson :', myPerson);
console.log('myPerson + "" :', myPerson + "");
.as-console-wrapper { min-height: 100%!important; top: 0; }

...除了具有两个引用失败源的toString方法...一方面是this.namethis._namethis.addressthis.address的命名冲突this._address 另一方面,选择一个箭头函数,在这种情况下,只有 “知道” 全局上下文作为 toString 方法的 this 上下文...也没有(技术上的)常量 _name_addresstoString.

的附加功能范围的需要

如果像...一样直接实现工厂,所有这些问题都将得到解决

function Person(name, address) {
  return {
    name,
    address,
    toString: function () {
      return `name: ${ this.name }, address: ${ this.address }`;
    }
  };
}
const myPerson = Person('John Doe', '123 Main St Anytown');

console.log('myPerson :', myPerson);
console.log('myPerson + "" :', myPerson + "");


// There is no inheritance involved for
// any object created by the above factory.

console.log(
  'Object.keys(myPerson) :',
  Object.keys(myPerson)
);

console.log(
  "myPerson.hasOwnProperty('toString') ?",
  myPerson.hasOwnProperty('toString')
);

console.log(
  "(Object.getPrototypeOf(myPerson) === Object.prototype) ?",
  (Object.getPrototypeOf(myPerson) === Object.prototype)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

也可以看出,从上述经过清理的工厂示例的附加日志记录中,上述工厂创建的任何对象都没有涉及继承(除了 Object.prototype 中最基本的一个)。


现在是“sub classing”与“增强/组合/混合” 部分的时间了......

...再次,让我们从 OP 提供的基于 class 的 Employee 版本开始。

Person 通过 extends sub classed Employee 并实现了 super 调用在 Employee 的构造函数中,每次调用后者时,都会创建一个具有 三个 own 属性 的实例 - salary 从直接调用 Employee 构造函数以及 nameaddresssuper 调用也可以通过委托调用来实现,例如 .. . Person.call(this, name, address) ... 如果 Person 不是 class 构造函数 而是普通的 构造函数 (与 JavaScript class 无关)。同时,此实例与 原型链 相关联,该链将在下一个示例代码的日志记录中揭开...

class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
  toString() {
    return `name: ${ this.name }, address: ${ this.address }`
  }
}

class Employee extends Person {
  constructor(name, address, salary) {
    super(name, address)
    this.salary = salary
  }

  getAnnualSalary() { return 12 * this.salary }
}

const myEmployee = new Employee('John Doe', '123 Main St Anytown', 6000);


console.log(
  '(myEmployee instanceof Employee) ?',
  (myEmployee instanceof Employee)
);
console.log(
  '(myEmployee instanceof Person) ?',
  (myEmployee instanceof Person)
);
console.log('\n');

console.log(
  '(Object.getPrototypeOf(myEmployee) instanceof Employee) ?',
  (Object.getPrototypeOf(myEmployee) instanceof Employee)
);
console.log(
  '(Object.getPrototypeOf(myEmployee) instanceof Person) ?',
  (Object.getPrototypeOf(myEmployee) instanceof Person)
);
console.log('\n');

console.log(
  'Object.keys(myEmployee) :',
  Object.keys(myEmployee)
);
console.log('\n');

console.log(
  "myEmployee.hasOwnProperty('getAnnualSalary') ?",
  myEmployee.hasOwnProperty('getAnnualSalary')
);
console.log(
  "Employee.prototype.hasOwnProperty('getAnnualSalary') ?",
  Employee.prototype.hasOwnProperty('getAnnualSalary')
);
console.log('\n');

console.log(
  "myEmployee.hasOwnProperty('toString') ?",
  myEmployee.hasOwnProperty('toString')
);
console.log(
  "Employee.prototype.hasOwnProperty('toString') ?",
  Employee.prototype.hasOwnProperty('toString')
);
console.log(
  "Person.prototype.hasOwnProperty('toString') ?",
  Person.prototype.hasOwnProperty('toString')
);
console.log('\n');

// automatic protoypal delegation,
// hence an inherited method via
// `Employee.prototype.getAnnualSalary`.
console.log(
  'myEmployee.getAnnualSalary() :',
  myEmployee.getAnnualSalary()
);

// automatic protoypal delegation,
// hence an inherited method via
// `Person.prototype.toString`.
console.log(
  'myEmployee.toString() :',
  myEmployee.toString()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

与上述基于 class 的方法相比,Employee 工厂通过 Object.assign 混合附加属性来扩充对象(文字)的实现是彻头彻尾的苗条.. .

function Employee(name, address, salary) {
  const getAnnualSalary = () => 12 * salary;
  return Object.assign({ getAnnualSalary }, Person(name, address));
}

...但是,OP 的实现同样容易出错。这次是因为将 salary 保持在工厂的本地函数范围内。因此 salary 永远不会像 classy 那样变成(变成)public 属性。它在每次调用 Employee 工厂时都会创建的闭包中保持不变。

Employee 的实现不创建闭包并使 salary 成为 public 和可变的 属性 可能看起来接近以下代码 ...

function Person(name, address) {
  return {
    name,
    address,
    toString: function () {
      return `name: ${ this.name }, address: ${ this.address }`;
    }
  };
}

function Employee(name, address, salary) {
  return Object.assign(Person(name, address), {
    salary,
    getAnnualSalary: function () {
      return (12 * this.salary);
    }
  });
}

const myEmployee = Employee('John Doe', '123 Main St Anytown', 6000);

console.log(
  'myEmployee :',
  myEmployee
);

console.log(
  'myEmployee.getAnnualSalary() :',
  myEmployee.getAnnualSalary()
);
console.log(
  'myEmployee.toString() :',
  myEmployee.toString()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

从上面的记录中可以很明显地看出,所谓的串联继承 会产生数据块。 public盟友携带的状态(数据属性)和行为(operate/process 这样的 state/data 的方法)之间没有分离。更重要的是,如果涉及管理封装和封装数据的受控访问,这种方法确实失去了轻量级和易于掌握的优势。

人们可能会考虑将此方法用于某种数量有限的引用,每个引用都具有可管理数量的属性。在我看来,code-reuse 这种技术,在基于原型的语言的上下文中,也不应该以名称 inheritance 为特色,因为它会主动阻止任何委托;后者是 JavaScript 如何管理继承的核心。

我会把这种方法及其相关的思想体系称为它实际上是什么……“基于工厂的可组合重用单元”

还有需要明确的是,我个人是 组合优于继承 的坚定拥护者……在我看来,基于 composition/mixin 的 code-reuse 比 code-reuse 有更好的方法OP 正在努力解决的问题。

串联继承和原型继承(在 JS 中,有时使用 class 关键字实现)是两种不同的委托方法。委托是一种机制,通过该机制,一个对象可以从其他对象而不是从 class(在 Java 意义上)定义中获取其部分或全部状态和行为。

在 JS 中,继承这个词与通过原型继承委托给祖先对象有很强的关联,但(令人惊讶的是)根据一些人的说法,this is not always the case.

Concatenative inheritance is the process of combining the properties of one or more source objects into a new destination object. Believe it or not, it is the most commonly used form of inheritance in JavaScript.

还有:

In JS, the essence of concatenative inheritance is often masked by the common name “mixins”. Confusingly, “mixins” have other meanings in other languages, and even in some JavaScript libraries. It also has uses that would be confusing to call “mixins”. Because of those reasons, I prefer the more precise term “concatenative inheritance”.

请注意,尽管 Java脚本包含“class 语法”(例如 class foo extends bar {}),但它没有“classical”或 class-based 通常意义上的继承。在 JavaScript 中,使用 class 语法的继承 always 通过原型继承实现。因此,在 JavaScript 中,class-based 继承几乎完全是 original prototypical inheritance model 的语法糖,自 Brendan Eich 的第一个 ten-day 版本以来一直存在于 JavaScript 中语言。

“组合”是一个重载的术语。 Object composition usually involves having one object delegate functionality or state to another object contained within it. Technically it specifically means the lifetime of the sub-object is tied to that of the composite, however, in my experience "composition" is usually used to mean "aggregation", which is composition, but whereby the lifetime of the sub-objects is not controlled by the composite, with them usually being injected via a constructor function. Functional composition, on the other hand, is a pattern of combining programming elements 其中函数调用以 f(g(x)).

的形式嵌套

您可以通过遵循对象组合模式来实现串联继承,将对象提供给函数以串联到复合对象上。

在下面的示例中,p 的实例将包括作为 ancestor 提供的对象上存在的功能。

function createP(ancestor) {
  const p = { 
    ...ancestor, 
    bar() {} 
  }
  return p
}
const o = { foo() {} }
const p = createP(o) // `p` now has both methods `foo` and `bar`

对于 JS 中的原型继承,有一个固定的、语言支持的机制,用于通过原型链实现动态 look-up 功能。

在原型继承中,继承的功能位于原型链某处的单独对象上。这种间接赋予这种继承风格不同的 non-functional 特征。

例如:

  • 可以说,与通过连接继承相比,通过原型继承包含哪些功能可能不太清楚,因为功能可以存在于(可能很长的)原型链中的任何位置。此外,可以在创建对象后从原型链中添加和删除功能。
  • 对于 API,原型继承对于保留子对象的功能很有用,使它们看起来简单,同时使它们能够以面向对象的方式方便地公开位于原型链上的功能。例如。您可以直接在您创建的每个数组上直接调用 Array#splice[].splice(...),即使 splice 函数位于其他地方(在 Array.prototype 上)。
  • 在原型继承中,祖先对象的方法需要以这样一种方式编写,即它们的目标(即。this)可以是另一个对象。可以说在串联继承中 this 的使用是 de-emphasised.
  • 在原型继承中,继承者和祖先之间存在持续的隐式耦合。这可能是错误的来源,并使代码更难推理。
  • 更一般地,在连接继承中 built-in JS 原型继承机制(包括 newthisextendssuper、原型链等)是 de-emphasised。有一种思路,popularized by Douglas Crockford, that this part of the JavaScript language complicates more often than it simplifies,应该尽可能避免。串联继承提供了另一种继承机制。
  • 串联继承绕过了原型继承的一些本体论约束。原型 link 暗示了“is-a” 关系,因此在串联继承中不暗示本体论关系。您可以根据需要将任意数量的对象混合在一起,以准确获得所需的功能(仅此而已)。

在这两种方法之间进行选择是主观偏好和风格的问题。两种方法都有其用武之地。