本文先对es6发布以前javascript各类继承实现方式进行深刻的分析比较,而后再介绍es6中对类继承的支持以及优缺点讨论。最后介绍了javascript面向对象编程中不多被涉及的“多态”,并提供了“运算符重载”的思路。本文假设你已经知道或了解了js中原型、原型链的概念。javascript
es6以前,javascript本质上不能算是一门面向对象的编程语言,由于它对于封装、继承、多态这些面向对象语言的特色并无在语言层面上提供原生的支持。可是,它引入了原型(prototype)的概念,可让咱们以另外一种方式模仿类,并经过原型链的方式实现了父类子类之间共享属性的继承以及身份确认机制。其实,面向对象的概念本质上来说不是指某种语言特性,而是一种设计思想。若是你深谙面向对象的编程思想,即便用c这种面向过程的语言也能写出面向对象的代码(典型的表明就是windows NT 内核实现),而javascript亦是如此!正是因为javascript自己对面向对象编程没有一个语言上的支持标准,因此才有了五花八门、使人眼花缭乱的“类继承”的代码。所幸,es6增长了class、extends、static等关键字用以在语言层面支持面向对象,可是,仍是有些保守!咱们先列举出es6以前常见的几种继承方案,而后再来一探es6的类继承机制,最后再讨论下javascript多态。java
简而言之,就是直接将父类的一个实例赋给子类的原型。以下示例:c++
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处所示,这种方法是直接new 了一个父类的实例,而后赋给子类的原型。这样也就至关于直接将父类原型中的方法属性以及挂在this上的各类方法属性全赋给了子类的原型,简单粗暴!咱们再来看看man,它是Man的一个实例,由于man自己没有getClassName方法,那么就会去原型链上去找,找到的是person的getClassName。这种继承方式下,全部的子类实例会共享一个父类对象的实例,这种方案最大问题就是子类没法经过父类建立私有属性。好比每个Person都有一个名字,咱们在初始化每一个Man的时候要指定一个不一样名字,而后子类将这个名字传递给父类,对于每一个man来讲,保存在相应person中的name应该是不一样的,可是这种方式根本作不到。因此,这种继承方式,实战中基本不用!git
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去调用父类的构造函数,从而达到继承父类属性的效果。这样一来,每new一个子类的实例,构造函数执行完后,都会有本身的一份资源(name)。可是这种办法只能继承父类构造函数中声明的实例属性,并无继承父类原型的属性和方法,因此就找不到getName方法,因此1处会报错。为了同时继承父类原型,从而诞生了组合继承的方式:es6
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的原型以下:github
> Man.prototype
> {name: "default name", className: "person"}复制代码
也就是说Man的原型中已经有了一个name属性,而以后建立man1时传给构造的函数的name则是经过this从新定义了一个name属性,至关于只是覆盖掉了原型的name属性(原型中的name依然还在),这样很不优雅。chrome
这是目前es5中主流的继承方式,有些人起了一个吊炸天的名字“寄生组合继承”。首先说明一下,二者是一回事。分离组合继承的名字是我起的,一来感受不装逼会好点,二来,更确切。综上所述,其实咱们能够将继承分为两步:构造函数属性继承和创建子类和父类原型的连接。所谓的分离就是分两步走;组合是指同时继承子类构造函数和原型中的属性。编程
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对象进行浅拷贝。和上面组合继承的主要区别就是:将父类的原型复制给了子类原型。这种作法很清晰:windows
还有一个问题,就是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对象!下面再来看一下下面的测试代码,先猜猜一、二、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的可能对上面的代码感受很亲切,几乎是自解释的。咱们大概解释一下,按照代码中标号对应:
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;复制代码
总结一下,和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复制代码
其中一、二、三、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也就无能为力了。
本文系统的介绍了javascript类继承和多态。如要转载请注明做者和原文连接。最后向你们安利一下个人开源项目:fly.js ,欢迎star。如文中有误,欢迎斧正。
参考资料