Javascript의 상속 및 다형성 요약

零下一度
풀어 주다: 2017-05-17 09:17:47
원래의
1142명이 탐색했습니다.

본 글에서는 먼저 es6 출시 전 javascript의 다양한 상속 구현 방법을 심층 분석 및 비교한 후, es6의 클래스 상속 지원을 소개하고 장단점을 논의합니다. 마지막으로 JavaScript 객체지향 프로그래밍에서 거의 다루지 않는 "다형성"을 도입하고 "연산자 오버로딩"이라는 아이디어를 제공합니다. 이 글에서는 여러분이 js의 프로토타입과 프로토타입 체인의 개념을 이미 알고 있거나 이해하고 있다고 가정합니다.

es6 이전에는 JavaScript가 캡슐화 등 객체지향 언어의 특성에 대해 언어 수준에서 네이티브 지원을 제공하지 않았기 때문에 본질적으로 객체지향 프로그래밍 언어로 간주할 수 없었습니다. , 상속 및 다형성. 그러나 클래스를 다른 방식으로 모방할 수 있는 프로토타입 개념을 도입하고, 프로토타입 체인을 통해 상위 클래스와 하위 클래스 간의 공유 속성 상속과 신원 확인 메커니즘을 실현합니다. 사실, 객체지향의 개념은 본질적으로 특정 언어 기능을 의미하는 것이 아니라 디자인 아이디어를 의미합니다. 객체지향 프로그래밍 아이디어에 정통하다면 C와 같은 프로세스지향 언어(전형적인 대표자는 Windows NT 커널 구현)를 사용해도 객체지향 코드를 작성할 수 있으며, 자바스크립트도 마찬가지입니다! 온갖 종류의 눈부신 "클래스 상속" 코드가 존재하는 것은 바로 JavaScript 자체가 객체 지향 프로그래밍을 위한 언어 지원 표준을 갖고 있지 않기 때문입니다. 다행히 es6에서는 언어 수준에서 객체지향을 지원하기 위해 class, extends, static 등의 키워드를 추가했습니다. 하지만 아직은 다소 보수적입니다! 먼저 es6 이전의 몇 가지 일반적인 상속 체계를 나열한 다음 es6의 클래스 상속 메커니즘을 탐색하고 마지막으로 자바스크립트 다형성에 대해 논의하겠습니다.

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 자체에는 getClassName 메소드가 없기 때문에 이를 찾기 위해 프로토타입 체인으로 이동하며 찾은 것은 person의 getClassName입니다. 이 상속 방법에서 모든 하위 클래스 인스턴스는 상위 클래스 객체의 인스턴스를 공유합니다. 이 솔루션의 가장 큰 문제점은 하위 클래스가 상위 클래스를 통해 개인 속성을 생성할 수 없다는 것입니다. 예를 들어, 각 Person에는 이름이 있습니다. 각 Man을 초기화할 때 다른 이름을 지정해야 하며, 하위 클래스는 이 이름을 상위 클래스에 전달합니다. 각 Man에 대해 해당 사람에 저장된 이름은 달라야 합니다. 방법으로는 도저히 할 수 없습니다. 따라서 이 상속 방식은 실제 전투에서는 기본적으로 사용되지 않습니다!

생성자 호출 메서드

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
로그인 후 복사

여기 하위 클래스의 생성자에서 하위 클래스 인스턴스의 이것을 사용하여 상위 클래스의 생성자를 호출하여 상위 클래스를 상속합니다. class 클래스 속성의 효과. 이러한 방식으로 하위 클래스의 모든 새 인스턴스는 생성자가 실행된 후 자체 리소스(이름)를 갖게 됩니다. 그러나 이 메소드는 상위 클래스 생성자에 선언된 인스턴스 속성만 상속할 수 있고 상위 클래스 프로토타입의 속성과 메소드는 상속하지 않으므로 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의 프로토타입에는 이미 이름이 있습니다. 속성을 생성한 다음 man1을 생성합니다. 생성된 함수에 전달된 이름은 이를 통해 이름 속성을 재정의합니다. 이는 프로토타입의 이름 속성(프로토타입의 이름이 여전히 남아 있음)을 덮어쓰는 것과 동일하며 매우 우아하지 않습니다.

분리 조합 상속

이것은 현재 es5의 주류 상속 방식입니다. 어떤 사람들은 "기생 조합 상속"이라는 말도 안되는 이름을 붙였습니다. 우선 두 가지가 같은 것이라는 점을 먼저 설명하겠습니다. 분리 조합 상속의 이름은 제가 선택했습니다. 첫째, 가식적이지 않으면 기분이 좋고, 둘째, 더 정확합니다. 요약하면 실제로 상속은 생성자 특성 상속과 하위 클래스의 프로토타입과 상위 클래스 간의 링크 설정이라는 두 단계로 나눌 수 있습니다. 소위 분리는 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿