##缘起## 工做中须要用到Javascript,关注了Javascript中继承复用的问题,翻阅了网上的各类关于Javascript继承的帖子,感受大都思考略浅,并无作过多说明,简单粗暴的告诉你实现Javascript继承有1.2.3.4.5几种方式,名字都叫啥,而后贴几行代码就算是玩了。
无论大家懂没懂,反正我着实没懂。
随后研读了《Javascript高级程序设计》的部分章节,对Javascript的继承机制略有体会。思考以后,遂而分享而且阐述了如何实现抽象类、接口、多态甚至是类型转换的思路。
##JS继承,那就先说“继承”## 凡是玩过一、2种面向对象的语言的人大都不难概括出继承全部的几个特性:javascript
##构造一个类## 说到构造一个Javascript的类,网上的说法五花八门。java
var Person = function(name, age) { this.name = name; this.age = age; }; var p = new Person("小王", 10);
注1:此为代码1,后面可能做为引用。
注2: var Person = function(){};
等同于 function Persson(){}
,前一种定义函数的方式没有名字,故而在var的后面跟上其名字,然后面function定义直接就跟了名字Person了。不过事实上我更喜欢后一种,由于能够少写一个var和分号。可是若是在局部做用域要定义一个临时类,我仍是喜欢前一种,这是一种变量的方式。在局部做用域我更喜欢定义变量而不是函数或者类等结构性的东西,C语言后遗症,呵呵。
注3:其实这种构建类的方式能够说成是经过构造器(constructor)来构造一个类。
3. 也有人说,应该用Prototype来构造一个类,简要代码以下:架构
var Person = function (){}; Person.prototype.name = "小王"; Person.prototype.age = 10; var p = new Person();
注4:这种构造方式,咱们能够暂且称之为“用prototype”的方式来构造。
注5:此为代码2,后面可能做引用。
**类的构建方式虽然五花八门,可是大抵都是以上两种或者其组合的变种。但是咱们何时用构造函数来构建?何时用prototype?何时二者结合使用呢?**要明白这个,咱们先来看看new关键字。
###new,你到底干了什么事儿?## new关键字在绝大多数面向对象的语言中都扮演者举足轻重的位置,javascript中也不例外。StackOverflow上有一篇帖子关于new关键字的玄机,我以为说的很好:Javascript中的new关键字背后到底作了什么
翻译以下,为了懒得移步的童鞋,PC端的童鞋能够直接点过去。app
- 建立一个新的简单的Object类型的的对象;
- 把Object的内部的**[[prototype]]属性设置为构造函数prototype属性。这个[[prototype]]**属性在Object内部是没法访问到的,而构造函数的prototype是能够访问到的;
- 执行构造函数,若是构造函数中用到了this关键字,那就把这个this替换为刚刚建立的那个object对象。
注6:其实某个对象的[[prototype]]**属性在不少宿主环境中已经能够访问到,例如Chrome和IE10均可以,用__proto__就能够访问到,若是下面出现了__proto__字样,那就表明一个对象的内部prototype。
上面说了一大通,又是构造器,又是prototype,不知所云。下面依次解释。
###prototype### prototype属性在构造函数中能够访问到,在对象中须要经过__prototype__访问到。它究竟是什么?prototype中定义了一个类所共享的属性和方法。这就意味着:一旦prototype中的某个属性的值变了,那么全部这个类的实例的该属性的值都变了。请看代码:函数
function Person() { } Person.prototype.name = "小明"; var p1 = new Person(); console.log(Person.prototype); console.log(p1.__proto__); var p2 = new Person(); console.log(p1.name + "\t" + p2.name); Person.prototype.name = "小王"; console.log(p1.name + "\t" + p2.name);
注7:此为代码3。 输出结果以下:
经过这个代码3的实验,咱们能够得出如下结论:测试
Person {name: "小明"}
###this和构造函数### 看完了上面的new关键字作的第3步,咱们不可贵出,其实利用constructor的方式来构造类本质:先new一个临时实例对象,将this关键字替换为临时实例对象关键字,而后使用[对象].[属性]=xxx
的方式来构建一个对象,再将其返回。
但是这样带来一个问题就是:方法不被共享。
请看代码4实验:this
function Person() { this.name = "小明"; this.showName = function() { console.log(this.name) }; } var p1 = new Person(); var p2 = new Person(); p1.showName(); p2.showName(); p1.showName = function() { console.log("我不是小明,我是小王"); } p1.showName(); p2.showName();
注8:以上为代码4。
其运行结果为:
咱们知道,类的同一个方法,应该尽可能保持共享,由于他们属于同一个类,那么这一个方法应该相同,因此应该保持共享,否则会浪费内存。
咱们的Person类中含有方法showName,虽然p1和p2实例属于两个实例对象,可是其showName却指向了不一样的内存块!
这可怎么办?
对,请出咱们的prototype,它能够实现属性和方法的共享。请看代码5实验:prototype
function Person() { this.name = "小明"; } Person.prototype.showName = function() { console.log(this.name); } var p1 = new Person(); var p2 = new Person(); p1.showName(); p2.showName(); Person.prototype.showName = function() { console.log("个人名字是" + this.name); } p1.showName(); p2.showName();
注9:以上为代码5 。 运行结果以下:
这样咱们很是完美地完成了一个类的构建,他知足:翻译
function Person1() { } // prototype 没有彻底被改写 Person1.prototype.showName = function() { console.log(this.name); }; var p1 = new Person1(); console.log(p1 instanceof Person1); console.log(p1.constructor); function Person2() { } // prototype 彻底被改写 Person2.prototype = { showName : function() { console.log(this.name); } }; var p2 = new Person2(); console.log(p2 instanceof Person2); console.log(p2.constructor);
注10:以上为代码6 。 运行结果以下:
经过以上代码6的实验,咱们能够看出:重写整个prototype会将对象的constructor指针直接指向了Object,从而致使了constructor不明的问题。
如何解决呢?咱们能够经过显示指定其constructor为Person便可。
请看代码7:设计
function Person2() { } // prototype 彻底被改写 Person2.prototype = { constructor : Person2, // 显示指定其constructor showName : function() { console.log(this.name); } }; var p2 = new Person2(); console.log(p2 instanceof Person2); console.log(p2.constructor);
注11:以上为代码7 。 运行结果以下:
###对象、constructor和prototype三者之间的关系 上面说了那么多,我想你们都有点被constructor、prototype、对象搞得云里雾里的,其实我刚开始也是这样。下面我总结叙述一下这三者之间的关系,相信看了以后就会逐渐明白的:
###Javascript的Object架构###
解释以下:
var f1 = new Foo();
建立了一个Foo对象;__proto__
属性,指向了一个prototype的实例对象Foo.prototype
;Foo.prototype
有个constructor
属性,指向了Foo构造函数,这个属性的值标明了,这个f1对象的类型,也即f1 instanceof Foo的结果为true;Foo.prototype
;###Javascript对象的属性查找方式 咱们访问一个Javascript对象的属性(含“方法”)的时候,查找过程究竟是什么样的呢?
先找先找对象属性,对象的属性中没有,那就找对象的prototype共享属性
请看代码8:
var p = { name : "小明" }; //对象中能查找到name console.log(p.name); //对象中找不到myName,查找其prototype属性,因为p是Object类型的对象,故而查找Object的prototype是否有myName console.log(p.myName); Object.prototype.myName="个人名字是小明"; console.log(p.myName);
注12:以上为代码8 。
结果以下:
此处不难理解,很少作解释。
##按照“继承”理念来实现JS继承## 在咱们懂了prototype、constructor、对象、new以后,咱们能够真正按照“继承”的理念来实现javascript的继承了。
###原型链 试想一下,若是构造函数的prototype对象的__proto__指针(每一个实例对象都有一个__proto__指针)指向的是另外一个prototype对象(咱们称之为prototype对象2)的话,而prototype对象2的constructor指向的是构建prototype对象2的构造函数。那么依次往复,就构成了原型链。
上面的话有点绕口,你们多多体会。
我结合上面的Javascript对象的架构继续给你们说说:
原型链只能继承共享的属性和方法,对于非共享的属性和方法,咱们须要经过显示调用父类构造函数来实现
查找对象的属性的修正:
因此很简单,咱们想要实现Javascript的继承已经呼之欲出了:
###继承构造函数中定义的属性和方法 咱们经过call或者apply方法便可实现父类构造函数调用,而后把当前对象this和参数传递给父类,这样就能够实现继承构造函数中定义的属性和方法了。请看代码9:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.showName = function() { console.log(this.name); }; function Male(name, age) { // 调用Person构造函数,把this和参数传递给Person Person.apply(this, arguments); this.sex = "男"; } var m = new Male("小明", 20); console.log(m); console.log(m instanceof Male); console.log(m instanceof Person); console.log(m instanceof Object);
执行结果以下:
Person.prototype
中继承过来的。m instanceof Person
结果为false, 显然m.__proto__.constructor
指向的是Male构造函数,而非Person。m instanceof Object
的结果却为true,那是由于m的原型链的上一级为Object类型,故而instance of Object
的结果为true。###继承prototype中定义的属性和方法,而且与继承构造函数结合起来### 如何继承prototype中定义的属性和方法呢?
直接把父类的prototype给子类的prototype不就好了。
的确,这样是可以实现方法共享,但是一旦子类的prototype的某个方法被重写了,那么父类也会搁着变更,怎么办?
**new一个父类!**赋值给子类的prototype。
请看代码10:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.showName = function() { console.log(this.name); }; function Male(name, age) { // 调用Person构造函数,把this和参数传递给Person Person.apply(this, arguments); this.sex = "男"; } // 继承prototype Male.prototype = new Person(); var m = new Male("小明", 20); console.log(m); console.log(m instanceof Male); console.log(m instanceof Person); console.log(m instanceof Object);
结果以下:
你们能够看到m对象不只有name, age , sex三个属性,并且经过其原型链能够找到showName方法。
若是你们仔细观察,会发现多出了两个undefined值的name和age!
为何?!
究其缘由,由于在执行Male.prototype = new Person()
的时候,这两个属性就在内存中分配了值了。并且改写了Male的整个prototype,致使Male对象的constructor也跟着变化了,这也很差。
这并非咱们想要的!咱们只是单纯的想要继承prototype,而不想要其余的属性。
怎么办?
借用一个空的构造函数,借壳继承prototype,而且显示设置constructor
代码以下:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.showName = function() { console.log(this.name); }; function Male(name, age) { // 调用Person构造函数,继承构造函数的属性,把this和参数传递给Person Person.apply(this, arguments); this.sex = "男"; } // 借用一个空的构造函数 function F() { } F.prototype = Person.prototype; // 继承prototype Male.prototype = new F(); // 显示指定constructor Male.prototype.constructor = Male; var m = new Male("小明", 20); console.log(m); m.showName(); console.log(m.constructor == Male); console.log(m instanceof Person); console.log(m instanceof Male); console.log(m instanceof F);
执行结果:
咱们可喜的将m的constructor正本清源!并且instanceof类型判断都没有错误(instanceof本质上是经过原型链找的,只要有一个原型知足了那结果就为true)。 ##继承prototype的封装&测试 上述继承prototype的代码非常丑陋,让咱们封装起来吧。而且测试了一下代码:
// 继承prototype & 设定subType的constructor为子类,不跟着prototype变化而变化 function inheritPrototype(subType, superType) { // 如下三行能够写成一个新的函数来完成 function F() { } // 把F的prototype指向父类的prototype,修改整个prototype而不是部分prototype F.prototype = superType.prototype; // new F()完成两件事情,1. 执行F构造函数,为空;2. 执行F的prototype的内存分配,这里就是父类,也就是Person的getAge方法 // 因此这里是继承了父类的getAge()方法,赋值给了proto var proto = new F(); // proto的构造函数显示指定为子类(因为上面重写了F的prototype,故而构造函数也变化了) proto.constructor = subType; // 实现真正意义上的prototype的继承,而且constructor为子类 subType.prototype = proto; } function Person(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; }; } Person.prototype.getAge = function() { return this.age; }; function Male(name, age) { Person.apply(this, [name, age]); // 借用构造函数继承属性 this.sex = "男"; this.getSex = function() { return this.sex; }; } inheritPrototype(Male, Person); // 方法覆盖 Male.prototype.getAge = function() { return this.age + 1; }; var p = new Person("好女人", 30); var m = new Male("好男人", 30); console.log(p); console.log(m); console.log(p.getAge()); console.log(m.getAge());
运行结果为:
至此,咱们已经完成了真正意义上的javascript继承!
让咱们再来回头验证一下,TDD嘛~呵呵
##总结## 咱们是经过:
至此,较为漂亮的完成了Javascript的继承!
经过此思路,想要实现抽象类,接口等面向对象的概念应该也不是难事吧。呵呵。
抽象类:父类构造函数中只有方法定义,则该父类即为抽象父类。
接口:父类构造函数中方法定义为空。
多态:父类中调用一个未实现的函数,在子类中实现便可。
类型转换:把中间层F断掉,从新指定实例对象的__proto__指向的prototype对象,那么F中继承的方法将不复存在,故而调用方法就是直接调用被指向的prototype对象的方法了。关于类型转换的代码以下:
// 继承prototype & 设定subType的constructor为子类,不跟着prototype变化而变化 function inheritPrototype(subType, superType) { // 如下三行能够写成一个新的函数来完成 function F() { } // 把F的prototype指向父类的prototype,修改整个prototype而不是部分prototype F.prototype = superType.prototype; // new F()完成两件事情,1. 执行F构造函数,为空;2. 执行F的prototype的内存分配,这里就是父类,也就是Person的getAge方法 // 因此这里是继承了父类的getAge()方法,赋值给了proto var proto = new F(); // proto的构造函数显示指定为子类(因为上面重写了F的prototype,故而构造函数也变化了) proto.constructor = subType; // 实现真正意义上的prototype的继承,而且constructor为子类 subType.prototype = proto; } function Person(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; }; } Person.prototype.getAge = function() { return this.age; }; function Male(name, age) { Person.apply(this, [name, age]); // 借用构造函数继承属性 this.sex = "男"; this.getSex = function() { return this.sex; }; } inheritPrototype(Male, Person); // 方法覆盖 Male.prototype.getAge = function() { return this.age + 1; }; var p = new Person("好女人", 30); var m = new Male("好男人", 30); console.log(p); console.log(m); // 将m转换为Person类型从而调用Person类的方法 m.\__proto\__ = Person.prototype; console.log(p.constructor == Person); console.log(m.constructor == Male); console.log(m instanceof Male); console.log(m instanceof Person); console.log(p.getAge()); console.log(m.getAge()); // 将m转换为Male类型从而调用Male类的方法 m.\__proto\__ = Male.prototype; console.log(p.constructor == Person); console.log(m.constructor == Male); console.log(m instanceof Male); console.log(m instanceof Person); console.log(p.getAge()); console.log(m.getAge());
运行结果:
你们能够看到类型转换以后,调getAge()方法的不一样了吧。
【文献引用】 1.《Professional Javascript for Web Developers》 3rd. Edition 第六章