Heim  >  Artikel  >  Web-Frontend  >  Zusammenfassung der Vererbung und des Polymorphismus in Javascript

Zusammenfassung der Vererbung und des Polymorphismus in Javascript

零下一度
零下一度Original
2017-05-17 09:17:471084Durchsuche

In diesem Artikel wird zunächst eine eingehende Analyse und ein Vergleich verschiedener Methoden zur Vererbungsimplementierung in Javascript vor der Veröffentlichung von es6 durchgeführt. Anschließend wird die Unterstützung der Klassenvererbung in es6 vorgestellt und die Vor- und Nachteile erörtert. Schließlich wird der „Polymorphismus“ eingeführt, der in der objektorientierten JavaScript-Programmierung selten berührt wird, und die Idee der „Operatorüberladung“ vermittelt. In diesem Artikel wird davon ausgegangen, dass Sie die Konzepte von Prototypen und Prototypenketten in js bereits kennen oder verstehen.

Vor es6 konnte JavaScript im Wesentlichen nicht als objektorientierte Programmiersprache betrachtet werden, da es auf Sprachebene keine native Unterstützung für die Merkmale objektorientierter Sprachen wie die Kapselung bot , Vererbung und Polymorphismus. Es führt jedoch das Konzept des Prototyps ein, das es uns ermöglicht, Klassen auf andere Weise zu imitieren und die Vererbung gemeinsamer Eigenschaften zwischen übergeordneten Klassen und Unterklassen sowie den Identitätsbestätigungsmechanismus über die Prototypenkette zu realisieren. Tatsächlich bezieht sich das Konzept der Objektorientierung nicht unbedingt auf ein bestimmtes Sprachmerkmal, sondern auf eine Designidee. Wenn Sie sich mit objektorientierten Programmierideen gut auskennen, können Sie objektorientierten Code sogar mit einer prozessorientierten Sprache wie C schreiben (ein typischer Vertreter ist die Windows NT-Kernelimplementierung), und das Gleiche gilt für Javascript! Gerade weil JavaScript selbst über keinen Sprachunterstützungsstandard für objektorientierte Programmierung verfügt, gibt es alle Arten von schillernden „Klassenvererbungs“-Codes. Glücklicherweise hat es6 Schlüsselwörter wie class, extends und static hinzugefügt, um die Objektorientierung auf Sprachebene zu unterstützen. Dies ist jedoch immer noch etwas konservativ! Lassen Sie uns zunächst einige gängige Vererbungsschemata vor ES6 auflisten, dann den Klassenvererbungsmechanismus von ES6 untersuchen und schließlich den Javascript-Polymorphismus diskutieren.

Vererbung vor ES6

Prototyp-Zuweisungsmethode

Kurz gesagt besteht darin, eine Instanz der übergeordneten Klasse direkt dem Prototyp der untergeordneten Klasse zuzuweisen. Das folgende Beispiel:

function Person(name){
 this.name=name;
 this.className="person" 
}
Person.prototype.getClassName=function(){
 console.log(this.className)
}

function Man(){
}

Man.prototype=new Person();//1
//Man.prototype=new Person("Davin");//2
var man=new Man;
>man.getClassName()
>"person"
>man instanceof Person
>true

Wie bei Punkt 1 im Code gezeigt, erstellt diese Methode direkt eine Instanz der übergeordneten Klasse und weist sie dann dem Prototyp der Unterklasse zu. Dies entspricht der direkten Zuweisung aller Methodenattribute im Prototyp der übergeordneten Klasse und verschiedener daran hängender Methodenattribute zum Prototyp der Unterklasse, einfach und grob! Schauen wir uns noch einmal den Mann an. Es handelt sich um eine Instanz von Man. Da der Mensch selbst keine getClassName-Methode hat, wird er zur Prototypenkette gehen, um sie zu finden, und was er findet, ist der getClassName der Person. Bei dieser Vererbungsmethode teilen sich alle Unterklasseninstanzen eine Instanz des übergeordneten Klassenobjekts. Das größte Problem bei dieser Lösung besteht darin, dass Unterklassen keine privaten Eigenschaften über die übergeordnete Klasse erstellen können. Beispielsweise hat jede Person einen Namen. Bei der Initialisierung jedes Mannes müssen wir einen anderen Namen angeben, und dann übergibt die Unterklasse diesen Namen an die übergeordnete Klasse. Für jeden Mann sollte der in der entsprechenden Person gespeicherte Name anders sein Das geht einfach nicht. Daher wird diese Vererbungsmethode im tatsächlichen Kampf grundsätzlich nicht verwendet!

Rufen Sie die Konstruktor--Methode auf

function Person(name){
 this.name=name;
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
var man1=new Man("Davin");
var man2=new Man("Jack");
>man1.name
>"Davin"
>man2.name
>"Jack"
>man1.getName() //1 报错
>man1 instanceof Person
>true

Hier im Konstruktor der Unterklasse verwenden Sie diese der Unterklasseninstanz, um den Konstruktor der übergeordneten Klasse aufzurufen und so zu erreichen Die Auswirkung der Vererbung übergeordneter Klassenattribute. Auf diese Weise verfügt jede neue Instanz einer Unterklasse nach der Ausführung des Konstruktors über eine eigene Ressource (Name). Diese Methode kann jedoch nur die im Konstruktor der übergeordneten Klasse deklarierten Instanzattribute erben und erbt nicht die -Attribute und -Methoden des übergeordneten Klassenprototyps. Daher kann die getName-Methode nicht gefunden werden, sodass ein Fehler auftritt als Erstes berichtet. Um gleichzeitig den Prototyp der übergeordneten Klasse zu erben, wurde die Methode der kombinierten Vererbung geboren:

Kombinierte Vererbung

function Person(name){
 this.name=name||"default name"; //1
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
//继承原型
Man.prototype = new Person();
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"

Dieses Beispiel ist sehr einfach. Dies wird nicht nur der Fall sein erben Sie die Eigenschaften im Konstruktor, kopieren Sie sie aber auch in die Prototypenkette der übergeordneten Klasse. Es gibt jedoch ein Problem: Man.prototype = new Person(); Nach der Ausführung dieses Satzes lautet der Prototyp von Man wie folgt:

> Man.prototype
> {name: "default name", className: "person"}

Das heißt, der Prototyp von Man hat ihn bereits ein Namensattribut und dann Der Name der Funktion, die beim Erstellen von man1 an den Konstruktor übergeben wurde, definiert dadurch ein Namensattribut neu, was dem einfachen Überschreiben des Namensattributs des Prototyps entspricht (der Name im Prototyp ist immer noch vorhanden). sehr unelegant.

Getrennte kombinatorische Vererbung

Dies ist die aktuelle gängige Vererbungsmethode in es5. Einige Leute haben ihr den lächerlichen Namen „parasitäre kombinatorische Vererbung“ gegeben. Lassen Sie mich zunächst erklären, dass beides dasselbe ist. Der Name der Trennungskombinationsvererbung wurde von mir gewählt. Erstens fühlt es sich besser an, wenn er nicht prätentiös ist, und zweitens ist er genauer. Zusammenfassend lässt sich sagen, dass wir die Vererbung tatsächlich in zwei Schritte unterteilen können: die Vererbung von Konstruktorattributen und die Herstellung einer Verbindung zwischen dem Prototyp der Unterklasse und der übergeordneten Klasse. Die sogenannte Trennung ist ein zweistufiger Prozess, bei dem die Attribute gleichzeitig im Unterklassenkonstruktor und im Prototyp geerbt werden.

function Person(name){
 this.name=name; //1
 this.className="person" 
}
Person.prototype.getName=function(){
 console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
//注意此处
Man.prototype = Object.create(Person.prototype);
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"

Hier wird die Methode Object.creat(obj) verwendet, die eine flache Kopie des eingehenden obj-Objekts erstellt. Der Hauptunterschied zur oben genannten kombinierten Vererbung besteht darin, dass der Prototyp der übergeordneten Klasse in den Prototyp der untergeordneten Klasse kopiert wird. Dieser Ansatz ist sehr klar:

  1. Erben Sie die Eigenschaften/Methoden der übergeordneten Klasse im Konstruktor und initialisieren Sie die übergeordnete Klasse.

  2. Der Unterklassen-Prototyp ist mit dem Elternklassen-Prototyp verbunden.

还有一个问题,就是constructor属性,我们来看一下:

> Person.prototype.constructor
< Person(name){
   this.name=name; //1
   this.className="person" 
 }
> Man.prototype.constructor
< Person(name){
   this.name=name; //1
   this.className="person" 
  }

constructor是类的构造函数,我们发现,Person和Man实例的constructor指向都是Person,当然,这并不会改变instanceof的结果,但是对于需要用到construcor的场景,就会有问题。所以一般我们会加上这么一句:

Man.prototype.constructor = Man

综合来看,es5下,这种方式是首选,也是实际上最流行的。

行文至此,es5下的主要继承方式就介绍完了,在介绍es6继承之前,我们再往深的看,下面是独家干货,我们来看一下Neat.js中的一段简化源码(关于Neat.js,这里是传送门Neat.js官网,待会再安利):

//下面为Neat源码的简化
-------------------------
function Neat(){
  Array.call(this)
}
Neat.prototype=Object.create(Array.prototype)
Neat.prototype.constructor=Neat
-------------------------
//测试代码
var neat=new Neat;
>neat.push(1,2,3,4)
>neat.length //1
>neat[4]=5
>neat.length//2
>neat.concat([6,7,8])//3

现在提问,上面分割线包起来的代码块干了件什么事?

对,就是定义了一个继承自数组的Neat对象!下面再来看一下下面的测试代码,先猜猜1、2、3处执行的结果分别是什么?期望的结果应该是:

4
5
1,2,3,4,5,6,7,8

而实际上却是:

4
4
[[1,2,3,4],6,7,8]

呐尼!这不科学啊 !why ?

我曾在阮一峰的一篇文章中看到的解释如下:

因为子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this ,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。ES5是先新建子类的实例对象 this ,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性 [[DefineOwnProperty]] ,用来定义新属性时,更新 length 属性,这个内部属性无法在子类获取,导致子类的 length 属性行为不正常。

然而,事实并非如此!确切来说,并不是原生构造函数会忽略掉 apply 方法传入的this而导致属性无法绑定。要不然1处也不会输出4了。还有,neat依然可以正常调用push等方法,但继承之后原型上的方法有些也是有问题的,如neat.concat。其实可以看出,我们通过 Array.call(this) 也是有用的,比如length属性可用。但是,为什么会出问?根据症状,可以肯定的是最终的this肯定有问题,但具体是什么问题呢?难道是我们漏了什么地方导致有遗漏的属性没有正常初始化?或者就是浏览器初始化数组的过程比较特殊,和自定义对象不一样?首先我们看第一种可能,唯一漏掉的可能就是数组的静态方法(上面的所有继承方式都不会继承父类静态方法)。我们可以测试一下:

for(var i in  Array){
 console.log(i,"xx")
}

然而并没有一行输出,也就是说Array并没有静态方法。当然,这种方法只可以遍历可枚举的属性,如果存在不可枚举的属性呢?其实即使有,在浏览器看来也应该是数组私有的,浏览器不希望你去操作!所以第一种情况pass。那么只可能是第二种情况了,而事实,直到es6出来后,才找到了答案:

ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的 所有行为 都可以继承。

请注意我加粗的文字。“所有”,这个词很微妙,不是“没有”,那么言外之意就是说es5是部分了。根据我之前的测试(在es5下),下标操作和concat在chrome下是有问题的,而大多数函数都是正常的,当然,不同浏览器可能不一样,这应该也是jQuery每次操作后的结果集以一个新的扩展后的数组的形式返回而不是本身继承数组(然后再直接返回this的)的主要原因,毕竟jQuery要兼容各种浏览器。而Neat.js面临的问题并没有这么复杂,只需把有坑的地方绕过去就行。言归正传,在es5中,像数组一样的,浏览器不让我们愉快与之玩耍的对象还有:

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

es6的继承方式

es6引入了class、extends、super、static(部分为ES2016标准)

class Person{
  //static sCount=0 //1
  constructor(name){
     this.name=name; 
     this.sCount++;
  }
  //实例方法 //2
  getName(){
   console.log(this.name)
  }
  static sTest(){
    console.log("static method test")
  }
}

class Man extends Person{
  constructor(name){
    super(name)//3
    this.sex="male"
  }
}
var man=new Man("Davin")
man.getName()
//man.sTest()
Man.sTest()//4
输出结果:
Davin
static method test

ES6明确规定,Class内部只有静态方法,没有静态属性,所以1处是有问题的,ES7有一个静态属性的 提案 ,目前Babel转码器支持。熟悉java的可能对上面的代码感觉很亲切,几乎是自解释的。我们大概解释一下,按照代码中标号对应:

  1. constructor为构造函数,一个类有一个,相当于es5中构造函数标准化,负责一些初始化工作,如果没有定义,js vm会定义一个空的默认的构造函数。

  2. 实例方法,es6中可以不加"function"关键字, class内定义的所有函数都会置于该类的原型当中 ,所以,class本身只是一个语法糖。

  3. 构造函数中通过super()调用父类构造函数,如果有super方法,需要时构造函数中第一个执行的语句,this关键字在调用super之后才可用。

  4. 静态方法,在类定义的外部只能通过类名调用,内部可以通过this调用,并且静态函数是会被继承的。如示例中:sTest是在Person中定义的静函数,可以通过 Man.sTest() 直接调用。

es6和es5继承的区别

大多数浏览器的ES5实现之中,每一个对象都有 proto 属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和 proto 属性,因此同时存在两条继承链。

(1)子类的 proto 属性,表示构造函数的继承,总是指向父类。

(2)子类 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性。

class A {
}

class B extends A {
}

B.proto === A // true
B.prototype.proto === A.prototype // true

上面代码中,子类 B 的 proto 属性指向父类 A ,子类 B 的 prototype 属性的 proto 属性指向父类 A 的 prototype 属性。

这样的结果是因为,类的继承是按照下面的模式实现的:

class A {
}

class B {
}

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B继承A的静态属性
Object.setPrototypeOf(B, A);

Object.setPrototypeOf的简单实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.proto = proto;
  return obj;
}

因此,就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.proto = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.proto = A;

这两条继承链,可以这样理解:作为一个对象,子类( B )的原型( proto 属性)是父类( A );作为一个构造函数,子类( B )的原型( prototype 属性)是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.proto = A.prototype;

es6继承的不足

  1. 不支持静态属性(除函数)。

  2. class中不能定义私有变量和函数。class中定义的所有函数都会被放倒原型当中,都会被子类继承,而属性都会作为实例属性挂到this上。如果子类想定义一个私有的方法或定义一个private 变量,便不能直接在class花括号内定义,这真的很不方便!

总结一下,和es5相比,es6在语言层面上提供了面向对象的部分支持,虽然大多数时候只是一个语法糖,但使用起来更方便,语意化更强、更直观,同时也给javascript继承提供一个标准的方式。还有很重要的一点就是-es6支持原生对象继承。

更多es6类继承资料请移步:MDN Classess 。

多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。这是标准定义,在c++中实现多态的方式有虚函数、抽象类、模板,在java中更粗暴,所有函数都是“虚”的,子类都可以重写,当然java中没有虚函数的概念,我们暂且把相同签名的、子类和父类可以有不同实现的函数称之为虚函数,虚函数和模版(java中的范型)是支持多态的主要方式,因为javascript中没有模版,所以下面我们只讨论虚函数,下面先看一个例子:

function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.toString=function(){
 return "I am a Person, my name is "+ this.name
}
function Man(name,age){
  Person.apply(this,arguments)
}
Man.prototype = Object.create(Person.prototype);
Man.prototype.toString=function(){
  return "I am a Man, my name is"+this.name;
}
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
> person+""
> "I am a Person, my name is Neo"
> man1+""
> "I am a Man, my name isDavin"
> man1<man2 //期望比较年龄大小 1
> false

上面例子中,我们分别在子类和父类实现了toString方法,其实,在js中上述代码原理很简单,对于同名函数,子类会覆父类的,这种特性其实就是虚函数,只不过js中不区分参数个数,也不区分参数类型,只看函数名称,如果名称相同就会覆盖。现在我们来看注释1,我们期望直接用比较运算符比较两个man的大小(按年龄),怎么实现?在c++中有运算符重载,但java和js中都没有,所幸的是,js可以用一种变通的方法来实现:

function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.valueOf=function(){
 return this.age
}
function Man(name,age){
  Person.apply(this,arguments)
}

Man.prototype = Object.create(Person.prototype);
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
var man3=new Man("Joe",19)

>man1<19//1
>true
>person==19//2
>true
>man1<man2//3
>true
>man2==man3 //4 注意
>true
>person==man2//5
>false

其中1、2、3、5在所有js vm下结果都是确定的。但是4并不一定!javascript规定,对于比较运算符,如果一个值是对象,另一个值是数字时,会先尝试调用valueOf,如果valueOf未指定,就会调用toString;如果是字符串时,则先尝试调用toString,如果没指定,则尝试valueOf,如果两者都没指定,将抛出一个类型错误异常。如果比较的两个值都是对象时,则比较的时对象的引用地址,所以若是对象,只有自身===自身,其它情况都是false。现在我们回过头来看看示例代码,前三个都是标准的行为。而第四点取决于浏览器的实现,如果严格按照标准,这应该算是chrome的一个bug ,但是,我们的代码使用时双等号,并非严格相等判断,所以浏览器的相等规则也会放宽。值得一提的是5,虽然person和man2 age都是19,但是结果却是false。 总结一下,chrome对相同类的实例比较策略是先会尝试转化,然后再比较大小,而对非同类实例的比较,则会直接返回false,不会做任何转化。 所以我的建议是:如果数字和类实例比较,永远是安全的,可以放心玩,如果是同类实例之间,可以进行 非等 比较,这个结果是可以保证的,不要进行相等比较,结果是不能保证的,一般相等比较,变通的做法是:

var equal= !(ob1<ob2||ob1>ob2) 
//不小于也不大于,就是等于,前提是比较操作符两边的对象要实现valueOf或toString

当然类似toString、valueOf的还有toJson方法,但它和重载没有什么关系,故不冗述。

数学运算符

让对象支持数学运算符本质上和让对象支持比较运算符原理类似,底层也都是通过valueOf、toString来转化实现。 但是通过这种覆盖原始方法模拟的运算符重载有个比较大局限就是:返回值只能是数字!而c++中的运算符重载的结果可以是一个对象 。试想一下,如果我们现在要实现一个复数类的加法,复数包括实部与虚部,加法要同时应用到两个部分,而相加的结果(返回值)仍然是一个复数对象,这种情况下,javascript也就无能为力了。

【相关推荐】

1. 特别推荐“php程序员工具箱”V0.1版本下载

2. 免费js在线视频教程

3. php.cn独孤九贱(3)-JavaScript视频教程

Das obige ist der detaillierte Inhalt vonZusammenfassung der Vererbung und des Polymorphismus in Javascript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn