Maison > interface Web > js tutoriel > Résumé de l'héritage et du polymorphisme en Javascript

Résumé de l'héritage et du polymorphisme en Javascript

零下一度
Libérer: 2017-05-17 09:17:47
original
1167 Les gens l'ont consulté

Cet article effectue d'abord une analyse approfondie et une comparaison des différentes méthodes d'implémentation de l'héritage dans javascript avant la sortie d'es6, puis présente la prise en charge de l'héritage de classe dans es6 et discute des avantages et des inconvénients. Enfin, il introduit le « polymorphisme » rarement abordé dans la programmation orientée objet JavaScript, et fournit l'idée de « surcharge d'opérateurs ». Cet article suppose que vous connaissez ou comprenez déjà les concepts de prototype et de chaîne de prototypes en js.

Avant es6, JavaScript ne pouvait pas être considéré par essence comme un langage de programmation orienté objet, car il ne fournissait pas de support natif au niveau du langage pour les caractéristiques des langages orientés objet tels que l'encapsulation. , l'héritage et le polymorphisme. Cependant, il introduit le concept de prototype, qui nous permet d'imiter les classes d'une autre manière, et réalise l'héritage des propriétés partagées entre les classes parentes et les sous-classes et le mécanisme de confirmation d'identité via la chaîne de prototypes. En fait, le concept d’orientation objet ne fait essentiellement pas référence à une certaine fonctionnalité du langage, mais à une idée de conception. Si vous connaissez bien les idées de programmation orientée objet, vous pouvez écrire du code orienté objet même en utilisant un langage orienté processus comme C (un représentant typique est l'implémentation du noyau Windows NT), et il en va de même pour Javascript ! C'est précisément parce que JavaScript lui-même n'a pas de norme de support de langage pour la programmation orientée objet qu'il existe toutes sortes de codes « d'héritage de classe » éblouissants. Heureusement, es6 a ajouté des mots-clés tels que class, extends et static pour prendre en charge l'orientation objet au niveau du langage. Listons d'abord plusieurs schémas d'héritage courants avant es6, puis explorons le mécanisme d'héritage de classe d'es6 et enfin discutons du polymorphisme javascript.

Héritage avant ES6

Méthode d'affectation de prototype

En bref, il s'agit d'attribuer directement une instance de la classe parent au prototype de la classe enfant. L'exemple suivant :

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
Copier après la connexion

Comme indiqué au point 1 du code, cette méthode renouvelle directement une instance de la classe parent puis l'assigne au prototype de la sous-classe. Cela équivaut à attribuer directement tous les attributs de méthode du prototype de la classe parent et divers attributs de méthode accrochés à celui-ci au prototype de la sous-classe, simple et grossier ! Regardons à nouveau man. C'est une instance de Man. Parce que man lui-même n'a pas de méthode getClassName, il ira dans la chaîne de prototypes pour la trouver, et ce qu'il trouve est le getClassName de la personne. Dans cette méthode d'héritage, toutes les instances de sous-classe partageront une instance de l'objet de classe parent. Le plus gros problème avec cette solution est que les les sous-classes ne peuvent pas créer de propriétés privées via la classe parent. Par exemple, chaque personne a un nom. Nous devons spécifier un nom différent lors de l'initialisation de chaque homme, puis la sous-classe transmet ce nom à la classe parent. Pour chaque homme, le nom enregistré dans la personne correspondante doit être différent, mais ceci. c'est tout simplement impossible. Par conséquent, cette méthode d'héritage n'est fondamentalement pas utilisée dans le combat réel !

Appelez la méthode constructeur

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
Copier après la connexion

Ici dans le constructeur de la sous-classe, utilisez celle de l'instance de la sous-classe pour appeler le constructeur de la classe parent, obtenant ainsi L'effet de l'héritage des attributs de la classe parent. De cette façon, chaque nouvelle instance d'une sous-classe aura sa propre ressource (nom) après l'exécution du constructeur. Cependant, cette méthode ne peut hériter que des attributs d'instance déclarés dans le constructeur de la classe parent, et n'hérite pas des attributs et méthodes du prototype de la classe parent, donc la méthode getName est introuvable, donc une erreur sera signalé au point 1. Afin d'hériter en même temps du prototype de la classe parent, la méthode d'héritage combiné est née :

Héritage combiné

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"
Copier après la connexion

Cet exemple est très simple, non seulement le les propriétés du constructeur doivent être héritées, mais également copiées. Propriétés dans la chaîne de prototypes de la classe parent. Cependant, il y a un problème, Man.prototype = new Person(); Une fois cette phrase exécutée, le prototype de Man est le suivant :

> Man.prototype
> {name: "default name", className: "person"}
Copier après la connexion

C'est-à-dire que le prototype de Man a déjà un attribut name, puis Le nom de la fonction passé au constructeur lors de la création de man1 redéfinit un attribut name via cela, ce qui équivaut à simplement écraser l'attribut name du prototype (le nom dans le prototype est toujours là), ce qui est très inélégant.

Héritage combinatoire séparé

Il s'agit de la méthode d'héritage courante actuelle dans es5. Certaines personnes lui ont donné un nom ridicule « héritage combinatoire parasite ». Tout d’abord, permettez-moi d’expliquer que les deux sont la même chose. Le nom d'héritage de combinaison de séparation a été choisi par moi. Premièrement, c'est mieux s'il n'est pas prétentieux, et deuxièmement, il est plus précis. Pour résumer, on peut en fait diviser l'héritage en deux étapes : l'héritage des attributs du constructeur et l'établissement d'un lien entre le prototype de la sous-classe et la classe parent. La soi-disant séparation est un processus en deux étapes ; la combinaison signifie hériter en même temps des attributs du constructeur de sous-classe et du prototype.

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"
Copier après la connexion

La méthode Object.creat(obj) est utilisée ici, qui fera une copie superficielle de l'objet obj entrant. La principale différence par rapport à l'héritage combiné ci-dessus est que le prototype de la classe parent est copié dans le prototype de la classe enfant. Cette approche est très claire :

  1. Héritez les attributs/méthodes de la classe parent dans le constructeur et initialisez la classe parent.

  2. Le prototype de la sous-classe est connecté au prototype de la classe parent.

还有一个问题,就是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" 
  }
Copier après la connexion

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

Man.prototype.constructor = Man
Copier après la connexion

综合来看,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
Copier après la connexion

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

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

4
5
1,2,3,4,5,6,7,8
Copier après la connexion

而实际上却是:

4
4
[[1,2,3,4],6,7,8]
Copier après la connexion

呐尼!这不科学啊 !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")
}
Copier après la connexion

然而并没有一行输出,也就是说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()
Copier après la connexion

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
Copier après la connexion

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
Copier après la connexion

上面代码中,子类 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);
Copier après la connexion

Object.setPrototypeOf的简单实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.proto = proto;
  return obj;
}
Copier après la connexion

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

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

Object.setPrototypeOf(B, A);
// 等同于
B.proto = A;
Copier après la connexion

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

Object.create(A.prototype);
// 等同于
B.prototype.proto = A.prototype;
Copier après la connexion

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
Copier après la connexion

上面例子中,我们分别在子类和父类实现了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
Copier après la connexion

其中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
Copier après la connexion

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

数学运算符

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

【相关推荐】

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

2. 免费js在线视频教程

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal