JavaScript中的串联继承与类继承 [英] Concatenative inheritance vs class inheritance in JavaScript

查看:61
本文介绍了JavaScript中的串联继承与类继承的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我刚开始看时,

串联继承对我来说就像一个合成,但是人们一直将其命名为继承.但是,类使用原型创建将对象连接在一起的原型链.现在的问题是,连接继承和类继承是否都使用同一对象?这是这两种情况的示例
串联继承

 功能人员(姓名,地址){const _name =名称const _address =地址const toString =()=>`名称:$ {this.name},地址:$ {this.address}`返回 {_名称,_地址,toString}}函数Employee(姓名,地址,薪水){const getAnnualSalary =()=>12 *薪水返回Object.assign({getAnnualSalary},Person(姓名,地址))} 

类继承

 班级人员{构造函数(名称,地址){this.name =名称this.address =地址}toString(){返回`name:$ {this.name},地址:$ {this.address}`}}类员工扩展人员{构造函数(名称,地址,薪水){超级(姓名,地址)this.salary =薪水}getAnnualSalary(){返回12 * this.salary}} 

解决方案

以下解释试图简短但全面.

让我们首先关注 Person 的不同实现,并让我们从基于 class 的版本开始,因为它的实现是干净的,而不像很多方面容易出错.它的工厂对应对象.

类Person {... toString(){...}} 具有 Person 类型特定的 toString 方法.后者被实现为 Person prototype 方法.因此,任何 Person 实例(例如 myPerson )都具有其 own toString 方法./p>

如果 toString 在e处被调用.G. myPerson ,将在此实例的 原型链 中查找该方法.因为该方法是在 Person.prototype.toString 上立即发现的,所以它会自动在 myPerson 的上下文中调用(通过显式调用...也可以实现此目的). Person.prototype.toString.call(myPerson); ).

  class Person {构造函数(名称,地址){this.name =名称;this.address =地址;}toString(){返回`name:$ {this.name},地址:$ {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');//自动原型委托,因此是继承的方法.console.log('myPerson.toString():',myPerson.toString());//明确的原型授权...简单且可预期.console.log('Person.prototype.toString.call(myPerson):',Person.prototype.toString.call(myPerson));console.log('\ n');//带有* alien *对象的显式原型委托...console.log(`Person.prototype.toString.call({名称:简·多伊(Jane Doe)",地址:"123 Main St Anytown",}):`,Person.prototype.toString.call({名称:简·多伊(Jane Doe)",地址:"123 Main St Anytown",}));  

  .as-console-wrapper {min-height:100%!important;最高:0;}  

关于由OP提供的 Person factory 实现,一个人必须对代码进行注释,还需要对其进行清理(其中,原因是基于意见的)...

 函数Person(名称,地址){const _name =名称;const _address =地址;const toString =()=>`名称:$ {this.name},地址:$ {this.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;最高:0;}  

...除了具有两个引用失败源的 toString 方法之外,一方面 this.name this._name的命名冲突 this.address this._address ,另一方面,选择箭头功能,在这种情况下,该功能仅知道" 将全局上下文作为 toString 方法的 this 上下文... ...也不需要(技术上)常量 _name 的附加功能范围code>, _address toString .

如果确实像...那样简单地实施工厂,那么所有这些问题都将得到解决.

 函数Person(名称,地址){返回 {名称,地址,toString:function(){返回`name:$ {this.name},地址:$ {this.address}`;}};}const myPerson = Person('John Doe','123 Main St Anytown');console.log('myPerson:',myPerson);console.log('myPerson +":',myPerson +");//不涉及继承//由上述工厂创建的任何对象.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;最高:0;}  

从上面经过清理的工厂示例的其他日志记录中也可以看到,上述工厂创建的任何对象都没有继承(除了 Object.prototype 的最基本的继承)).


现在是时候使用子类别"与增强/合成/混合" 部分...

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

通过 extends Person 获得子类别 Employee ,并实现了 super Employee 的构造函数中调用,每次调用后者时,都会创建一个具有三个 own 属性-<直接调用了 Employee 构造函数以及来自 super name address 的code> salary >呼叫,也可以通过诸如... Person.call(this,name,address) ...的委派呼叫来实现,以防 Person 不是类构造器,但是是普通的构造器函数(与JavaScript class 不相关).同时,此实例与原型链相关联,该链将通过记录下一个示例代码...

公开.

  class Person {构造函数(名称,地址){this.name =名称;this.address =地址;}toString(){返回`name:$ {this.name},地址:$ {this.address}`}}类员工扩展人员{构造函数(名称,地址,薪水){超级(姓名,地址)this.salary =薪水}getAnnualSalary(){返回12 * this.salary}}const myEmployee = new Employee('John Doe','123 Main St Anytown',6000);console.log('((myEmployee instanceof Employee)?',(Employee的myEmployee实例));console.log('(myEmployee instanceof Person)吗?',(myEmployee的Person实例));console.log('\ n');console.log('((Object.getPrototypeOf(myEmployee)Employee的实例)吗?',(Employee的Object.getPrototypeOf(myEmployee)实例));console.log('((Object.getPrototypeOf(myEmployee)instanceof Person)?',(Person.getPrototypeOf(myEmployee)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');//自动原型授权,//因此是通过//`Employee.prototype.getAnnualSalary`.console.log('myEmployee.getAnnualSalary():',myEmployee.getAnnualSalary());//自动原型授权,//因此是通过//`Person.prototype.toString`.console.log('myEmployee.toString():',myEmployee.toString());  

  .as-console-wrapper {min-height:100%!important;最高:0;}  

与上述基于类的方法相比, Employee 工厂的实现通过 Object.assign 混合其他属性来扩展对象(文字),实在是苗条...

 功能Employee(姓名,地址,薪水){const getAnnualSalary =()=>12 *薪水;返回Object.assign({getAnnualSalary},Person(name,address));} 

...但是同样,OP的实现容易出错.这次是由于将 salary 保留在工厂的本地功能范围内.因此, salary 永远不会像它的 classy 一样变成(变成)公共财产.在每次调用 Employee 工厂时都会创建的闭包中,它保持不变.

Employee 的实现不会创建闭包,也不会使 salary 成为公共且可变的属性,它的实现可能看起来类似于以下代码...

 函数Person(名称,地址){返回 {名称,地址,toString:function(){返回`name:$ {this.name},地址:$ {this.address}`;}};}函数Employee(姓名,地址,薪水){return Object.assign(Person(name,address),{薪水,getAnnualSalary:函数(){返回(12 * this.salary);}});}const myEmployee = Employee('John Doe','123 Main St Anytown',6000);console.log('myEmployee:',我的员工);console.log('myEmployee.getAnnualSalary():',myEmployee.getAnnualSalary());console.log('myEmployee.toString():',myEmployee.toString());  

  .as-console-wrapper {min-height:100%!important;最高:0;}  

从上面的日志记录中可以很明显地看出,所谓的 Concatenative Inheritance 会产生数据斑点.在公共状态(数据属性)和行为(操作/处理此类状态/数据的方法)之间没有区别.更重要的是,如果要管理封装和对封装数据的受控访问,则此方法确实失去了其轻量化和易于掌握的优势.

人们可能会考虑使用这种方法来获得某种程度上有限的引用,每个引用都具有可管理的数量的属性.我认为,在基于原型的语言的上下文中,这种代码重用技术也不应使用名称​​ inheritance ,因为它可以有效地防止任何委派.后者是JavaScript管理继承的核心.

我将这种方法及其相关的思想体系称为实际上是什么……基于工厂的可组合复用单元".

需要明确的是,我个人是继承而非继承的坚决拥护者……在我看来,基于组合/混合的代码重用方法要好得多.OP正在苦苦挣扎.

concatenative inheritance works like a composition to me when I look to it at the beginning, but people keep naming it as an inheritance. classes, however, use the prototype to create a prototype chain that connects objects together. the question now is, if both concatenative inheritance and class inheritance do the same thing which one to use? here is an example of both scenarios
concatenative inheritance

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))
}



the class inheritance


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 }
}

解决方案

The following explanation tries to be brief but comprehensive.

Let's focus first on the different implementations of Person and also let's start with the class based version, for its implementation is clean unlike the in many aspects error prone one of its factory counterpart.

class Person { ... toString() { ... } } features a Person type specific toString method. The latter is implemented as a prototype method of Person. Thus any Person instance like myPerson does not feature its own toString method.

In case toString gets invoked at e. g. myPerson, the method will be looked up at this very instance' prototype chain. Because the method was found (immediately) at Person.prototype.toString, it automatically gets invoked within myPerson's context (something one also can achieve by explicitly invoking ... 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; }

Regarding the factory implementation of Person provided by the OP, one has to comment on the code and also is in need of sanitizing it (, with the sanitizing part of cause being an opinion based one) ...

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; }

... Besides the toString method featuring two sources of reference failures ... on one hand the naming conflict of this.name vs this._name and this.address vs this._address and on the other hand choosing an arrow function which in this case only "knows" about the global context as the toString method's this context ... there is also no (technical) need of the additional function scope of the constants _name, _address and toString.

All these problems are solved if one does implement the factory as straightforward as ...

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; }

As one can see too, from the additional logging of the above sanitized factory example, there is no inheritance involved for any object created by the above factory (besides the most basic one of Object.prototype).


It's time now for the "sub classing" versus "augmentation / composition / mixin" part ...

... and again, let's start with the class based version of an Employee as provided by the OP.

Having sub classed Employee from Person via extends and having implemented the super call within the Employee's constructor, one does, with every invocation of the latter, create an instance which features three own properties - salary from directly having invoked the Employee constructor as well as name and address from the super call which one also could achieve by a delegation call like ... Person.call(this, name, address) ... in case Person was not a class constructor but an ordinary constructor function (which is not related to JavaScript class). In the same time this instance gets associated with a prototype chain that will be unveiled by the logging of the next example code ...

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; }

In comparison to the above class based approach the implementation of an Employee factory which augments an object (literal) by mixing in additional properties via Object.assign is downright slim ...

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

... But again, the OP's implementation is error prone. This time it is due to keeping salary within the factory's local function scope. Thus salary never becomes (turns into) a public property like it does with its classy counterpart. It remains immutable within a closure that will be created every time the Employee factory gets invoked.

An implementation of Employee which does not create closures and makes salary a public and mutable property too might look close to the following code ...

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; }

From the above logging it should be quite obvious that the so called Concatenative Inheritance produces data blobs. There is no separation in between publically carried state (data properties) and behavior (methods that operate/process such state/data). More importantly, if it comes to managing encapsulation and controlled access of encapsulated data this approach does lose its advantage of being lightweighted and easy to grasp on.

One might consider this approach for a somehow limited amount of references, each with a manageable amount of properties. In my opinion, this technique of code-reuse, within the context of a prototype based language, should also not feature the name inheritance for it actively prevents any delegation; and the latter is the very core of how JavaScript manages inheritance.

I would call this approach and its related system of thoughts what it actually is … "Factory based Composable Units Of Reuse".

And just to be clear, I personally am a strong advocate of Composition over Inheritance … there are, again in my opinion, just much nicer approaches for composition/mixin based code-reuse than the one the OP was struggling with.

这篇关于JavaScript中的串联继承与类继承的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆