Home > Article > Web Front-end > Summary of inheritance and polymorphism in Javascript
This article first conducts an in-depth analysis and comparison of various inheritance implementation methods javascript before the release of es6, and then introduces the support for class inheritance in es6 and discusses the advantages and disadvantages. Finally, it introduces "polymorphism" which is rarely touched in JavaScript object-oriented programming, and provides the idea of "operator overloading". This article assumes that you already know or understand the concepts of prototype and prototype chain in js.
Before es6, JavaScript could not be considered an object-oriented programming language in essence, because it did not provide native support at the language level for the characteristics of object-oriented languages such as encapsulation, inheritance, and polymorphism. . However, it introduces the concept of prototype, which allows us to imitate classes in another way, and implements the inheritance of shared properties between parent classes and subclasses and the identity confirmation mechanism through the prototype chain. In fact, the concept of object-oriented does not essentially refer to a certain language feature, but a design idea. If you are well versed in object-oriented programming ideas, you can write object-oriented code even using a process-oriented language like C (a typical representative is the Windows NT kernel implementation), and the same is true for javascript! It is precisely because JavaScript itself does not have a language support standard for object-oriented programming that there are all kinds of dazzling "class inheritance" codes. Fortunately, es6 has added keywords such as class, extends, static to support object-oriented at the language level, but it is still a bit conservative! Let’s first list several common inheritance schemes before es6, then explore the class inheritance mechanism of es6, and finally discuss javascript polymorphism.
In short, it is to directly assign an instance of the parent class to the prototype of the child class. The following example:
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
As shown in point 1 of the code, this method directly newes an instance of the parent class and then assigns it to the prototype of the subclass. This is equivalent to directly assigning all the method attributes in the parent class prototype and various method attributes hanging on this to the prototype of the subclass, simple and crude! Let's look at man again. It is an instance of Man. Because man itself does not have a getClassName method, it will go to the prototype chain to find it, and what it finds is person's getClassName. In this inheritance method, all subclass instances will share an instance of the parent class object. The biggest problem with this solution is that subclasses cannot create private properties through the parent class. For example, each Person has a name. When initializing each Man, we need to specify a different name, and then the subclass passes this name to the parent class. For each man, the name saved in the corresponding person should be Different, but this way simply can't be done. Therefore, this inheritance method is basically not used in actual combat!
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
Here in the constructor of the subclass, use this of the subclass instance to call the constructor of the parent class, thereby inheriting the parent class The effect of class attributes. In this way, every new instance of a subclass will have its own resource (name) after the constructor is executed. However, this method can only inherit the instance attributes declared in the parent class constructor, and does not inherit the attributes and methods of the parent class prototype, so the getName method cannot be found, so an error will be reported in the first place. In order to inherit the prototype of the parent class at the same time, the method of combined inheritance was born:
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"
This example is very simple. This will not only inherit the properties in the constructor, but also copy the parent class. Properties in the prototype chain. However, there is a problem, Man.prototype = new Person(); After this sentence is executed, the prototype of Man is as follows:
> Man.prototype > {name: "default name", className: "person"}
That is to say, the prototype of Man already has a name attribute, and then create man1 The name passed to the constructed function redefines a name attribute through this, which is equivalent to just overwriting the name attribute of the prototype (the name in the prototype is still there), which is very inelegant.
This is the current mainstream inheritance method in es5. Some people have given it the ridiculous name of "parasitic combination inheritance". First of all, let me explain that the two are the same thing. The name of separate combination inheritance was chosen by me. Firstly, it feels better if it is not pretentious, and secondly, it is more accurate. To sum up, we can actually divide inheritance into two steps: constructor attribute inheritance and establishing a link between the prototype of the subclass and the parent class. The so-called separation is a two-step process; combination means inheriting the attributes in the subclass constructor and prototype at the same time.
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"
The Object.creat(obj) method is used here, which will make a shallow copy of the incoming obj object. The main difference from the combined inheritance above is that the prototype of the parent class is copied to the prototype of the subclass. This approach is very clear:
The constructor inherits the properties/methods of the parent class and initializes the parent class.
The subclass prototype is connected to the parent class prototype.
还有一个问题,就是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引入了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的可能对上面的代码感觉很亲切,几乎是自解释的。我们大概解释一下,按照代码中标号对应:
constructor为构造函数,一个类有一个,相当于es5中构造函数标准化,负责一些初始化工作,如果没有定义,js vm会定义一个空的默认的构造函数。
实例方法,es6中可以不加"function"关键字, class内定义的所有函数都会置于该类的原型当中 ,所以,class本身只是一个语法糖。
构造函数中通过super()调用父类构造函数,如果有super方法,需要时构造函数中第一个执行的语句,this关键字在调用super之后才可用。
静态方法,在类定义的外部只能通过类名调用,内部可以通过this调用,并且静态函数是会被继承的。如示例中:sTest是在Person中定义的静函数,可以通过 Man.sTest() 直接调用。
大多数浏览器的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;
不支持静态属性(除函数)。
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视频教程
The above is the detailed content of Summary of inheritance and polymorphism in Javascript. For more information, please follow other related articles on the PHP Chinese website!