ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript の継承とポリモーフィズムのまとめ

JavaScript の継承とポリモーフィズムのまとめ

零下一度
零下一度オリジナル
2017-05-17 09:17:471084ブラウズ

この記事では、まず es6 のリリース前に javascript でのさまざまな継承実装方法を詳細に分析および比較し、次に es6 でのクラス継承のサポートを紹介し、利点と欠点について説明します。最後に、JavaScript のオブジェクト指向プログラミングではあまり触れられることのない「ポリモーフィズム」を紹介し、「演算子のオーバーロード」という考え方を提供します。この記事は、js のプロトタイプとプロトタイプ チェーンの概念をすでに知っている、または理解していることを前提としています。

es6 より前では、JavaScript はカプセル化、継承、ポリモーフィズムなどのオブジェクト指向言語の特性を言語レベルでネイティブにサポートしていなかったので、本質的にオブジェクト指向プログラミング言語とはみなされませんでした。しかし、プロトタイプの概念が導入され、別の方法でクラスを模倣できるようになり、親クラスとサブクラス間の共有プロパティの継承と、プロトタイプチェーンによる同一性確認メカニズムが実現されます。実際、オブジェクト指向の概念は本質的に特定の言語機能を指すのではなく、設計上のアイデアを指します。オブジェクト指向プログラミングの考え方に精通している場合は、C などのプロセス指向言語 (代表的なものは Windows NT カーネル実装) を使用してもオブジェクト指向コードを作成できます。これは JavaScript にも当てはまります。 JavaScript 自体がオブジェクト指向プログラミングの言語サポート標準を持っていないからこそ、さまざまな種類の目もくらむような「クラス継承」コードが存在します。幸いなことに、es6 には言語レベルでオブジェクト指向をサポートするために classextendsstatic などのキーワードが追加されていますが、まだ少し保守的です。まず、es6 以前の一般的な継承スキームをいくつかリストし、次に es6 のクラス継承メカニズムを調べ、最後に JavaScript ポリモーフィズムについて説明します。

ES6以前の継承

プロトタイプ代入方法

要するに、親クラスのインスタンスを子クラスのプロトタイプに直接代入することです。次の例:

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

コードのポイント 1 に示すように、このメソッドは親クラスのインスタンスを直接更新し、それをサブクラスのプロトタイプに割り当てます。これは、親クラスのプロトタイプ内のすべてのメソッド属性と、これにぶら下がっているさまざまなメソッド属性をサブクラスのプロトタイプに直接割り当てるのと同じであり、単純かつ粗雑です。 man をもう一度見てみましょう。これは Man のインスタンスです。man 自体には getClassName メソッドがないため、プロトタイプ チェーンに行き、それを見つけます。それが person の getClassName です。この継承方法では、すべてのサブクラス インスタンスが親クラス オブジェクトのインスタンスを共有します。この解決策の最大の問題は、サブクラスが親クラスを介してプライベート プロパティを作成できないことです。たとえば、各人物には名前があり、それぞれの Man を初期化するときは、異なる名前を指定する必要があります。その後、サブクラスはこの名前を親クラスに渡しますが、対応する人物に保存される名前は異なる必要があります。この方法は単純に実行できません。ですので、この継承方法は実戦では基本的には使いません!

Call the constructormethod

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

ここでは、サブクラスのコンストラクター内で、サブクラスインスタンスのthisを使用して親クラスのコンストラクターを呼び出すことで、親クラスのプロパティを継承する効果を実現しています。このようにして、コンストラクターの実行後、サブクラスのすべての新しいインスタンスが独自のリソース (名前) を持つようになります。ただし、このメソッドは親クラスのコンストラクターで宣言されたインスタンス属性のみを継承し、親クラスのプロトタイプのプロパティとメソッドを継承しないため、getName メソッドが見つからないため、ポイント 1 でエラーが報告されます。 。親クラスのプロトタイプを同時に継承するために、結合継承というメソッドが生まれました。

結合継承

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"

この例は非常に単純です。これは、コンストラクター内のプロパティを継承するだけでなく、コピーも行います。親クラスのプロトタイプチェーン内のプロパティ。ただし、問題があります。 Man.prototype = new Person(); この文を実行すると、Man のプロトタイプは次のようになります。

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

つまり、Man のプロトタイプにはすでに name 属性があり、後で man1 が作成されるときに、関数の名前がコンストラクターに渡されます。これは、プロトタイプの name 属性を単に上書きするのと同じであり (プロトタイプ内の名前はまだ存在します)、これは非常に洗練されていません。 。

Separate Combination Inheritance

これは、ES5 で現在主流の継承方法であり、「Parasite Combination Inheritance」というとんでもない名前を付けている人もいます。まず最初に、この2つは同じものであることを説明します。個別の組み合わせ継承という名前は、第一に、それが気取らないほうが良いと思うこと、そして第二に、より正確であることを理由に、私が選択しました。要約すると、実際には継承を 2 つのステップに分けることができます。コンストラクター属性の継承と、サブクラスのプロトタイプと親クラスの間のリンクの確立です。いわゆる分離は 2 段階のプロセスであり、結合とは、サブクラスのコンストラクターとプロトタイプの属性を同時に継承することを意味します。

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"

ここでは Object.creat(obj) メソッドが使用されており、受信した obj オブジェクトの浅いコピーを作成します。上記の結合継承との主な違いは、親クラスの プロトタイプ がサブクラスのプロトタイプにコピーされることです。このアプローチは非常に明確です:

  1. はコンストラクターで親クラスのプロパティ/メソッドを継承し、親クラスを初期化します。

  2. サブクラスのプロトタイプと親クラスのプロトタイプが接続されています。

还有一个问题,就是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视频教程

以上がJavaScript の継承とポリモーフィズムのまとめの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。