类与继承在javascript的出现,说明javascript已经达到大规模开发的门槛了,在以前是ECMAScript4,就试图引入类,模块等东西,但因为过度引入太多的特性,搞得javascript乌烟瘴气,致使被否决。不过只是把类延时到ES6.到目前为止,javascript尚未正真意义上的类。不过咱们能够模拟类,曾近一段时间,类工厂是框架的标配,本章会介绍各类类实现,方便你们在本身的框架中或选择时本身喜欢的那一类风格。javascript
1.javascript对类的支持css
在其它语言中 ,类的实例都要经过构造函数new出来。做为一个刻意模仿java的语言。javascript存在new操做符,而且全部函数均可以做为构造器。构造函数与普通的方法没有什么区别。浏览器为了构建它繁花似锦的生态圈,好比Node,Element,HTMLElement,HTMLParagraphElement,显然使用继承关系方便一些方法或属性的共享,因而javascript从其它语言借鉴了原型这种机制。Prototype做为一个特殊的对象属性存在于每个函数上。当一个函数经过new操做符new出其“孩子”——“实例”,这个名为实例的对象就拥有这个函数的Prototype对象全部的一切成员,从而实现实现全部实例对象都共享一组方法或属性。而javascript所谓的“类”就是经过修改这个Prototype对象,以区别原生对象及其其它定义的“类”。在浏览器中,node这个类基于Object修改而来的,而Element则是基于Node,而HTMLElement又基于Element....相对咱们的工做业务,咱们能够建立本身的类来实现重用与共享。html
function A(){ } A.prototype = { aa:"aa", method:function(){ } }; var a = new A; var b = new A; console.log(a.aa === b.aa); console.log(a.method === b.method)
通常地,我们把定义在原型上的方法叫原型方法,它为全部的实例所共享,这有好也有很差,为了实现差别化,javascript容许咱们直接在构造器内指定其方法,这叫特权方法。若是是属性,就叫特权属性。它们每个实例一个副本,各不影响。所以,咱们一般把共享用于操做数据的方法放在原型,把私有的属性放在特权属性中。但放于this上,仍是让人任意访问到,那就放在函数体内的做用域内吧。这时它就成为名副其实的私有属性。java
function A() { var count = 0; this.aa = "aa"; this.method = function() { return count; } this.obj = {} } A.prototype = { aa:"aa", method:function(){ } }; var a = new A; var b = new A; console.log(a.aa === b.aa);//true 因为aa的值为基本类型,比较值 console.log(a.obj === b.obj) //false 引用类型,每次进入函数体都要从新建立,所以都不同。 console.log(a.method === b.method); //false
特权方法或属性只是只是遮住原型的方法或属性,所以只要删掉特权方法,就能方法到同名的原型方法或属性。node
delete a.method; delete b.method; console.log(a.method === A.prototype.method);//true console.log(a.method === b.method); //true
用java的语言来讲,原型方法与特权方法都属性实例方法,在java中还有一种叫类方法与类属性的东西。它们用javascript来模拟也很是简单,直接定义在函数上就好了。git
A.method2 = function(){} //类方法 var c = new A; console.log(c.method2); //undefined
接下来,咱们看下继承的实现,上面说过,Prototype上有什么东西,它的实例就有什么东西,不论这个属性是后来添加的,仍是整个Prototype都置换上去的。若是咱们将这个prototype对象置换为另外一个类的原型,那么它就垂手可得的得到那个类的全部原型成员。es6
function A() {}; A.prototype = { aaa : 1 } function B() {}; B.prototype = A.prototype; var b = new B; console.log(b.aaa); //=> 1; A.prototype.bb = 2; console.log(b.bb) //=> 2;
因为是引用着同一个对象,这意味这,咱们修改A类的原型,也等同于修该了B类的原型。所以,咱们不能把一个对象赋值给两个类。这有两种办法,github
方法1:经过for in 把父类的原型成员逐一赋给子类的原型
方法2是:子类的原型不是直接由父类得到,先将父类的原型赋值给一个函数,而后将这个函数的实例做为子类的原型。数组
方法一,咱们一般要实现mixin这样的方法,有的书称之为拷贝继承,好处就是简单直接,坏处就是没法经过instanceof验证。Prototype.js的extend方法就用来干这事。浏览器
function extend (des, source) { //des = destination for (var property in source) des[property] = source[property]; return des; }
方法二,就在原型上动脑筋,所以称之为原型继承。下面是个范本
function A() {}; A.prototype = { aa:function(){ alert(1) } } function bridge() { }; bridge.prototype = A.prototype; function B() {} B.prototype = new bridge(); var a = new A; var b = new B; console.log(a == b) //false 证实成功分开原型 console.log(A.prototype == B.prototype) //true 子类共享父类的原型方法 console.log(a.aa === b.aa); //为父类动态添加新的方法 A.prototype.bb = function () { alert(2) } //true,继承父类的方法 B.prototype.cc = function (){ alert(3) } //false 父类未必有子类的new实例 console.log(a.cc === b.cc) //而且它可以正常经过javascript自带的验证机制instanceof console.log(b instanceof A) ;//true console.log(b instanceof B) ; //true
方法二能经过instanceof验证,es5就内置了这种方法来实现原型继承,它就是Object.create,若是不考虑第二个参数,它约等于下面的代码。
Object.create = function (o) { function F() {} F.prototype = o; return new F(); }
上面的方法,要求传入一个父类的原型做为参数,而后返回子类的原型
不过,咱们这样仍是遗漏了一点东西——子类不仅是继承父类的遗产,还应该有本身的东西,此外,原型继承并无让子类继承父类的成员与特权成员。这些咱们都得手动添加,如类成员,咱们能够经过上面的extend方法,特权成员咱们能够在子类构造器中,经过apply实现。
function inherit(init, Parent, proto){ function Son(){ Parent.apply(this, argument); //先继承父类的特权成员 init.apply(this, argument); //在执行本身的构造器 } } //因为Object.create是咱们伪造的,所以避免使用第二个参数 Son.prototype = Object.create(Parent.prototype,{}); Son.prototype.toString = Parent.prototype.toString; //处理IEbug Son.prototype.valueOf = Parent.prototype.valueOf; //处理IEbug Son.prototype.constructor = Son; //确保构造器正常指向,而不是Object extend(Son, proto) ;//添加子类的特有的原型成员 return Son;
下面,作一组实验,测试下实例的回溯机制。当咱们访问对象的一个属性,那么他先寻找其特权成员,若是有同名就返回,没有就找原型,再没有,就找父类的原型...咱们尝试将它的原型临时修改下,看它的属性会变成那个。
function A(){ } A.prototype = { aa:1 } var a = new A; console.log(a.aa) ; //=>1 //将它的全部原型都替换掉 A.prototype = { aa:2 } console.log(a.aa); //=>1 //因而咱们想到每一个实例都有一个constructor方法,指向其构造器 //而构造器上面正好有咱们的原型,javascript引擎是否是经过该路线回溯属性呢 function B(){ } B.prototype = { aa:3 } a.constructor = B; console.log(a.aa) //1 表示不受影响
所以类的实例确定经过另外一条通道进行回溯,翻看ecma规范可知每个对象都有一个内部属性[[prototype]],它保存这咱们new它时的构造器所引用的Prototype对象。在标准浏览器与IE11里,它暴露了一个叫__proto__属性来访问它。所以,只要不动__proto__上面的代码怎么动,a.aa始终坚决不毅的返回1.
再看一下,new时操做发生了什么。
1.建立了一个空对象 instance
2.instance.__proto__ = intanceClass.prototype
3.将构造函数里面的this = instance
4.执行构造函数里的代码
5.断定有没有返回值,没有返回值就返回默认值为undefined,若是返回值为复合数据类型,则直接返回,不然返回this
因而有了下面的结果。
function A(){ console.log(this.__proto__.aa); //1 this.aa = 2 } A.prototype = {aa:1} var a = new A; console.log(a.aa) a.__proto__ = { aa:3 } console.log(a.aa) //=>2 delete a. aa; //删除特权属性,暴露原型链上的同名属性 console.log(a.aa) //=>3
有了__proto__,咱们能够将原型设计继承设计得更简单,咱们仍是拿上面的例子改一改,进行试验
function A() {} A.prototype = { aa:1 } function bridge() {} bridge.prototype = A.prototype; function B(){} B.prototype = new bridge(); B.prototype.constructor = B; var b = new B; B.prototype.cc = function(){ alert(3) } //String.prototype === new String().__proto__ => true console.log(B.prototype.__proto__ === A.prototype) //true console.log(b.__proto__ == B.prototype); //true console.log(b.__proto__.__proto__ === A.prototype); //true 获得父类的原型对象
由于b.__proto__.constructor为B,而B的原型是从bridge中得来的,而bride.prototype = A.prototype,反过来,咱们在定义时,B.prototype.__proto__ = A.prototype,就能轻松实现两个类的继承.
__proto__属性已经加入es6,所以能够经过防止大胆的使用
2.各类类工厂的实现。
上节咱们演示了各类继承方式的实现,但都很凌乱。咱们但愿提供一个专门的方法,只要用户传入相应的参数,或按照必定简单格式就能建立一个类。特别是子类。
因为主流框架的类工厂太依赖他们庞杂的工具函数,而一个精巧的类工厂也不过百行左右
至关精巧的库,P.js
https://github.com/jiayi2/pjs
使用版:https://github.com/jiayi2/factoryjs
这是一个至关精巧的库,尤为调用父类的同名方法时,它直接将父类的原型抛在你面前,连_super也省了。
var P = (function(prototype, ownProperty, undefined) { return function P(_superclass /* = Object */, definition) { // handle the case where no superclass is given if (definition === undefined) { definition = _superclass; _superclass = Object; } // C is the class to be returned. // // When called, creates and initializes an instance of C, unless // `this` is already an instance of C, then just initializes `this`; // either way, returns the instance of C that was initialized. // // TODO: the Chrome inspector shows all created objects as `C` // rather than `Object`. Setting the .name property seems to // have no effect. Is there a way to override this behavior? function C() { var self = this instanceof C ? this : new Bare; self.init.apply(self, arguments); return self; } // C.Bare is a class with a noop constructor. Its prototype will be // the same as C, so that instances of C.Bare are instances of C. // `new MyClass.Bare` then creates new instances of C without // calling .init(). function Bare() {} C.Bare = Bare; // Extend the prototype chain: first use Bare to create an // uninitialized instance of the superclass, then set up Bare // to create instances of this class. var _super = Bare[prototype] = _superclass[prototype]; var proto = Bare[prototype] = C[prototype] = C.p = new Bare; // pre-declaring the iteration variable for the loop below to save // a `var` keyword after minification var key; // set the constructor property on the prototype, for convenience proto.constructor = C; C.extend = function(def) { return P(C, def); } return (C.open = function(def) { if (typeof def === 'function') { // call the defining function with all the arguments you need // extensions captures the return value. def = def.call(C, proto, _super, C, _superclass); } // ...and extend it if (typeof def === 'object') { for (key in def) { if (ownProperty.call(def, key)) { proto[key] = def[key]; } } } // if no init, assume we're inheriting from a non-Pjs class, so // default to using the superclass constructor. if (!('init' in proto)) proto.init = _superclass; return C; })(definition); } // as a minifier optimization, we've closured in a few helper functions // and the string 'prototype' (C[p] is much shorter than C.prototype) })('prototype', ({}).hasOwnProperty);
咱们尝试建立一个类:
var Dog = P (function(proto, superProto){ proto.init = function(name) { //构造函数 this.name = name; } proto.move = function(meters){ //原型方法 console.log(this.name + " moved " + meters + " m.") } }); var a = new Dog("aaa") var b = new Dog("bbb"); //无实例变化 a.move(1); b.move(2);
咱们在如今的状况下,能够尝试建立更简洁的定义方式
var Animal = P (function(proto, superProto){ proto.init = function(name) { //构造函数 this.name = name; } proto.move = function(meters){ //原型方法 console.log(this.name + " moved " + meters + " m.") } }); var a = new Animal("aaa") var b = new Animal("bbb"); //无实例变化 a.move(1); b.move(2); //............... var Snake = P (Animal, function(snake, animal){ snake.init = function(name, eyes){ animal.init.call(this, arguments); //调运父类构造器 this.eyes = 2; } snake.move = function() { console.log('slithering...'); animal.move.call(this, 5); //调运父类同名方法 } }); var s = new Snake("snake", 1); s.move(); console.log(s.name); console.log(s.eyes);
私有属性演示,因为放在函数体内集中定义,所以安全可靠!
var Cobra = P (Snake, function(cobra){ var age = 1;//私有属性 //这里还能够编写私有方法 cobra.glow = function(){ //长大 return age++; } }); var c = new Cobra("cobra"); console.log(c.glow()); //1 console.log(c.glow()); //2 console.log(c.glow()); //3
JS.Class
从它的设计来看,让是继承Base2,类似的类工厂还有mootools。
Base2的base2.__prototyping, mootools的klass.$protyping。它建立子类时页不经过中间的函数断开双方的原型链,而是使用父类的实例来作子类的原型,这点实现的很是精巧。
simple-inheritance
做者为john Resig ,特色是方法链实现的十分优雅,节俭。
体现javascript的灵活性的库 def.js
若是有什么库能体现javascript的灵活性,此库确定名列前茅。它试图在形式上模拟Ruby的继承形式。让使用过ruby的人一眼看出,那个是父类,那个是子类。
下面就是Ruby的继承示例:
class child < Father # 略 end
def.js能作到这个程度
def("Animal")({ init:function(name){ this.name = name; }, speak:function(text){ console.log('this is a' + this.name) } }); var animal = new Animal("Animal"); console.log(animal.name) def('Dog') < Animal({ init:function(name,age){ this._super();//魔术般的调运了父类 this.age = age; }, run:function(s){ console.log(s) } }); var dog = new Dog('wangwang'); console.log(dog.name); //wangwang //在命名空间上建立子类 var namespace = {}; def(namespace,"Shepherd") < Dog({ init:function(){ this._super(); } }); var shepherd = new namespace.Shepherd("Shepherd") console.log(shepherd.name);
3.es5属性描述符对oo库的冲击
es5最受人瞩目的升级是为对象引入属性描述符,让咱们对属性有了更精细的控制,如这个属性是否能够修改,是否能够在for in中循环出来,是否能够删除。这些新增的API都集中定义在Object下,基本上除了Object.keys这个方法外,其它新API,旧版本的IE都没法模拟。因而新的API,基本不多有讲解的,咱们在这里稍微解读下:
Obejct提供如下几种新方法。
Object.keys
Object.getOwnPropertyNames
Object.getPrototypeOf
Object.defineProperty
Object.defineProperties
Object.getOwnPropertyDescriptor
Object.create
Object.seal
Object.freeze
Object.preventExtensions
Object.isSealed
Object.isFrozen
Object.isExtensible
其中,除了Object.keys外,旧版本的IE都没法模拟这些新API。旧版式的标准浏览器,能够用__peototype__实现Object.getPrototypeOf,结合__defineGetter__与defineSetter__来模拟Object.defineProperty。
Obejct.keys用于收集当前对象的可遍历属性(不包括原型链上的)以数组形式返回。
Object.getOwnPropertyNames用于收集当前对象不可遍历属性与可遍历属性,以数组形式返回。
var obj = { aa : 1, toString : function() { return "1" } } if (Object.defineProperty && Object.seal) { Object.defineProperty(obj,"name",{ value:2 }) } console.log(Object.getOwnPropertyNames(obj)); //=> ["aa", "toString", "name"] console.log(Object.keys(obj));//=> ["aa", "toString"] function fn(aa, bb){}; console.log(Object.getOwnPropertyNames(fn));// => ["length", "name", "arguments", "caller", "prototype"] console.log(Object.keys(fn));//[] var reg = /\w{2,}/i; console.log(Object.getOwnPropertyNames(reg)); //=> ["source", "global", "ignoreCase", "multiline", "lastIndex"] console.log(Object.keys(reg));//[]
Object.prototypeOf返回参数对象内部属性[[Prototype]],它在标准浏览器中一直使用一个私有属性__proto__获取(IE9 10,opera都没有)。须要补充一下,Object的新API(除了Object.create外)有一个统一的规定,要求第一个参数不能为数字 ,字符串,布尔,null,undefeind这五种字面量,不然抛出TypeError异常。
console.log(Object.getPrototypeOf(function(){}) == Function.prototype ); //=>true console.log(Object.getPrototypeOf({}) === Object.prototype);//=>true
Object.definePrototype暴露了属性描述的接口,以前许多内建属性都是由JavaScript引擎在属下操做。如,for in循环为什么不能遍历出函数的arguments、length、name等属性名,delete window.a为什么返回false. 这些现象终究有个解释。它一共涉及六个可组合的配置项。
是否可重写writable,当前值value,
读取时内部调用的函数set,
写入时内部调用函数get,
是否可遍历enumerable,
是否能够再次改动这些配置项configurable.
好比咱们随便写个对象
var obj = {x:1}
有了属性描述符,咱们就清楚它在底下作的更多细节,它至关于es5的这个建立对象的式子:
var obj = Object.create(Object.prototype,{ x : { value : 1, writable : true, enumerable : true, configurable : true } })
效果对比es3和es5,就很快明白,曾经的[[ReadOnly]] , [[DontEnum]], [[DontDlelete]]改为[[writable]], [[enumerable]],[[Configurable]]了。所以,configurable还有兼顾可否删除的职能。
这六个配置项将原有的本地属性拆分为两组。数据属性与访问器属性。咱们以前的方法能够像数据属性那样定义。
es3时代,咱们的自定义类的属性能够通通看作是数据属性。
像DOM中的元素节点的 innerHTML innerText cssText 数组的length则可归为访问器属性,对它们赋值不是单纯的赋值,还会引起元素其它功能的触发,而取值不必定直接返回咱们以前给予的值。
数据的属性有一、二、五、6这四个配置项,访问器有三、四、五、6这四个配置项、若是你设置了value与writable,就不能设置set,get,反之亦然。若是没有设置。2,3,4默认为false。第1,5,6项默认为false.
关于对象属性特征,更多请参阅http://www.cnblogs.com/ahthw/p/4272663.html 第7小节:7.属性的特征
var obj = {}; Object.defineProperty(obj,"a",{ value: 37, writable :true, enumerable :true, configurable: true }); console.log(obj.a) //=> 37; obj.a = 40; console.log(obj.a) //=>40 var name = "xxx"; for(var i in obj){ name = i } console.log(name);//=> a Object.defineProperty(obj,"a",{ value:55, writable:false, enumerable:false, configurable:true }) console.obj(obj.a);//=>55 obj.a = 50; console.log(obj.a);//55 name = "b"; for (var i in obj){ name = i } console.log(name); //b
Object.defineProperties就是Object.defineProperty的增强版,它能一会儿处理多个属性。所以,若是你能模拟Object.defineProperty,它就不是问题。
if (typeof Object.defineProperties !== 'function'){ Object.defineProperties = function(obj, descs){ for(var prop in descs) { if (descs.hasOwnProperty(porop)){ Object.defineProperty(obj, prop, descs[prop]); } } return obj; } }
使用示例
var obj = {}; Object.defineProperties(obj, { "value":{ value :true, writable:false, }, "name":{ value:"John", writable:false } }); var a = 1; for (var p in obj){ a = p }; console.log(a);// 1
Object.getOwnPropertDescriptor用于得到某对象的本地属性的配置对象。其中,configurable,enumerable确定包含其中。视状况再包括value,wirtable或set,get.
....
Object.preventExtensions,它是三个封锁对象修改的API中程度最轻的,就是阻止添加本地属性,不过若是本地属性都被删除了,也没法再加回来。之前javascript对象的属性都是任意添加的,删除,修改其值。若是它原型改动。咱们访问它还会有意外的惊喜。
var a = { aa: "aa" } Object.preventExtensions(a) a.bb = 2; console.log(a.bb);//=> undefined a.aa = 3; console.log(a.aa); //=>3 容许修改原有的属性 delete a.aa; console.log(a.aa); //=> undefined 但容许它删除已有的属性 Object.prototype.ccc = 4; console.log(a.ccc); //4 不能阻止添加原型属性 a.aa = 5; console.log(a.aa); //=> dundeined 不吃回头草,估计里边是以白名单的方式实现的
Object.seal比Object.preventExtensions更严格,它不许删除已有的本地属性,内部实现就是遍历一下,把本地属性的configurable改成false
var a = { a : "aa" } Object.seal(a) a.bb = 2; console.log(a.bb); // =>undefined添加本地属性失败 a.aa = 3; console.log(a.aa); //3 容许修改已有的属性 delete a.aa; console.log(a.aa) ;//=>3 但不容许删除已有的属性
Object.freeze无疑是最专制的(所以有人说过程式程序很专制,OO程序则自由些,显然道格拉斯的ecma262v5想把javascript引向前者),它连原有的本地属性也不让修改了。内部的实现就是遍历一下,把每一个本地属性的writable也改为false.
var a = { aa : "aa" }; Object.freeze(a); a.bb = 2; console.log(a.bb) //undefined 添加本地属性失败 a.aa =3; console.log(a.aa) //aa 不容许它修改已有的属性 delete a.aa; console.log(a.aa); //aa 不容许删除已经有的属性
(isPainObject用于断定目标是否是纯净的javascript对象,且不是其它自定义类的实例。用法与prototype.js的class.create同样,并参照 jQuery UI提供了完美的方法链与静态成员的继承。)
总结:es5对javascript对象产生深入的影响,Object.create让原型继承更方便了,但在增添的字类的专有原型成员或类成员时,若是它们的属性enumerable为false,单纯的for in循环已经无论用了,完美就要用到Object.getOwnPropertyNames。另外,访问器属性的复制只有经过Object.getOwnPropertyDescriptpor与Object.defineProperty才能完成。
(本章完结)
上一章:第五章:浏览器的嗅探和特征侦测 下一章: 第7章:选择器引擎