共计 9043 个字符,预计需要花费 23 分钟才能阅读完成。
本文先对 es6 公布之前 javascript 各种继承实现形式进行深刻的剖析比拟,而后再介绍 es6 中对类继承的反对以及优缺点探讨。最初介绍了 javascript 面向对象编程中很少被波及的“多态”,并提供了“运算符重载”的思路。本文假如你曾经晓得或理解了 js 中原型、原型链的概念。
es6 之前,javascript 实质上不能算是一门面向对象的编程语言,因为它对于封装、继承、多态这些面向对象语言的特点并没有在语言层面上提供原生的反对。然而,它引入了原型 (prototype) 的概念,能够让咱们以另一种形式模拟类,并通过原型链的形式实现了父类子类之间共享属性的继承以及身份确认机制。其实,面向对象的概念实质上来讲不是指某种语言个性,而是一种设计思维。如果你深谙面向对象的编程思维,即应用 c 这种面向过程的语言也能写出面向对象的代码(典型的代表就是 windows NT 内核实现),而 javascript 亦是如此!正是因为 javascript 自身对面向对象编程没有一个语言上的反对规范,所以才有了形形色色、令人目迷五色的“类继承”的代码。所幸,es6 减少了 class、extends、static 等关键字用以在语言层面反对面向对象,然而,还是有些激进!咱们先列举出 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 处所示,这种办法是间接 new 了一个父类的实例,而后赋给子类的原型。这样也就相当于间接将父类原型中的办法属性以及挂在 this 上的各种办法属性全赋给了子类的原型,简略粗犷!咱们再来看看 man, 它是 Man 的一个实例,因为 man 自身没有 getClassName 办法,那么就会去原型链下来找,找到的是 person 的 getClassName。这种继承形式下,所有的子类实例会共享一个父类对象的实例,这种计划最大问题就是 子类无奈通过父类创立公有属性。比方每一个 Person 都有一个名字,咱们在初始化每个 Man 的时候要指定一个不同名字,而后子类将这个名字传递给父类,对于每个 man 来说,保留在相应 person 中的 name 应该是不同的,然而这种形式基本做不到。所以,这种继承形式,实战中根本不必!
调用构造函数形式
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 处会报错。为了同时继承父类原型,从而诞生了组合继承的形式:
组合继承
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 则是通过 this 从新定义了一个 name 属性,相当于只是笼罩掉了原型的 name 属性(原型中的 name 仍然还在),这样很不优雅。
拆散组合继承
这是目前 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 对象进行浅拷贝。和下面组合继承的次要区别就是:将父类的 原型 复制给了子类原型。这种做法很清晰:
- 构造函数中继承父类属性/办法,并初始化父类。
- 子类原型和父类原型建立联系。
还有一个问题,就是 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 源码的简化
-------------------------
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 的可能对下面的代码感觉很亲切,简直是自解释的。咱们大略解释一下,依照代码中标号对应:
- constructor 为构造函数,一个类有一个,相当于 es5 中构造函数标准化,负责一些初始化工作,如果没有定义,js vm 会定义一个空的默认的构造函数。
- 实例办法,es6 中能够不加 ”function” 关键字,class 内定义的所有函数都会置于该类的原型当中,所以,class 自身只是一个语法糖。
- 构造函数中通过 super()调用父类构造函数,如果有 super 办法,须要时构造函数中第一个执行的语句,this 关键字在调用 super 之后才可用。
- 静态方法,在类定义的内部只能通过类名调用,外部能够通过 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 继承的有余
- 不反对动态属性(除函数)。
- class 中不能定义公有变量和函数。class 中定义的所有函数都会被放倒原型当中,都会被子类继承,而属性都会作为实例属性挂到 this 上。如果子类想定义一个公有的办法或定义一个 private 变量,便不能间接在 class 花括号内定义,这真的很不不便!
总结一下,和 es5 相比,es6 在语言层面上提供了面向对象的局部反对,尽管大多数时候只是一个语法糖,但应用起来更不便,语意化更强、更直观,同时也给 javascript 继承提供一个规范的形式。还有很重要的一点就是-es6 反对原生对象继承。
多态
多态(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 也就无能为力了。