javascript内的继承分类——传统继承(classical inheritance)和现代继承(modern inheritance)
传统继承就是指类式的继承,思维方式较为贴近Java等类式面向对象语言。
1.类式继承模式#1——默认模式
默认模式就是最基本的原型链模式,通过给子构造函数的prototype添加父构造函数的实例化对象,这样就把父构造函数实例化的对象保存在prototype属性内
function Parent(name){ this.name=name||"xiaosi"; } Parent.prototype.say=function(){ return this.name; } function Child(name){} function inherit(C,P){ C.prototype=new P(); } inherit(Child,Parent); var kid=new Child(); kid.say();
单纯这种复用模式有很大的缺点:
1:同时继承了‘父类’的2个类对象属性,本身的属性和prototype上的属性,但是通常情况下我们往往需要的是prototype内的属性(这些才是共用的,对象内部属性在设计的时候就一般是给自己用的)
2:不能支持子构造函数传参到父构造函数,也就是说构造函数并没有被继承,明显手动的把父构造函数的代码粘贴到子构造函数内并不符合我们的代码复用思想
2.类式继承模式#2——借用构造函数
上面的默认继承问题2是不能继承到父构造函数,为了解决这个问题,我们可以直接使用父构造函数构造子对象,实现了“构造函数继承”
function Parent1(name){ this.name=name; } Parent1.prototype.sayName=function(){ alert(this.name); } function Parent2(age){ this.age=age; } Parent2.prototype.sayAge=function(){ alert(this.age); } function Child(name,age,sex){ Parent1.call(this,name); Parent2.call(this,age); this.sex=sex; } Child.prototype.saySex=function(){ alert(this.sex); } var son=new Child('xiaosi',20,'男'); son.sayName(); //报错,没有sayName方法 son.sayAge(); //报错,没有sayAge方法 son.saySex(); //男
这样就可以实现复用父构造函数,看起来更像Java的继承。
但是这种继承方式依旧有不小的问题
1:只能继承到父构造函数的内容(本质上是执行一次构造函数,所以其构造出的属性是只属于本身的)但是却不能继承到构造函数的属性(prototype也是属性之一),导致我们想要继承的方法没有被继承到。
3.类式继承模式#3——借用和设置原型
这种模式主要是结合前两种模式,即先借用构造函数,然后还设置子构造函数的原型指向一个构造函数的新实例。
function Parent(name){ this.name=name||'xiaosi'; } Parent.prototype.sayName=function(){ return this.name; } function Child(name,sex){ Parent.apply(this,arguments); this.sex=sex; } Child.prototype=new Parent(); Child.prototype.saySex=function(){ alert(this.sex); } var son=new Child('xiaosi2','男');
这种模式实现了构造函数继承和原型继承共存,但是也有自己的问题:
1:父构造函数被调用了2次,有可能进行了不必要的操作(影响到了全局变量)
2:子构造函数生成的对象在prototype和本身属性中有同名对象。如果需要delete属性的时候就可能出错
3:只能继承一个构造函数的prototype
4.类式继承模式#4——共享原型
这种模式的思想基础在于:可复用的成员应该转移到原型汇总而不是放在this中,因此,出于继承的目的,任何值得继承的东西都应该放置在原型中实现
function inherit(Child,Parent){ Child.prototype=Parent.prototype; }
但是这种模式依旧有自己的问题:
1:只能继承到prototype的属性,并不能继承到实例对象的属性(虽然我们一般情况下不打算继承到this内的东西)
2:子构造函数的prototype和父构造函数的prototype指向同一个位置,改变了任意一个子构造函数prototype就将影响父构造函数的prototype(多级继承将会一直向上延伸)
5.类式继承模式#5——临时构造函数(代理)
通过断开父对象于子对象的原型之间的之间联系,从而解决共享同一个原型带来的问题,同时依旧可以实现原型链继承好处
var inherit=(function(){ var F=function(){}; //改变函数内容,使得F只会初始化一次 return function(Child,Parent){ //先让F共享到Parent的prototype,使得F.prototype有Parent的“共享属性” F.prototype=Parent.prototype; //Child的prototype等于F的一个实例,在这里由于new了一个新的实例,从而断开了Child和Parent的直接连接 Child.prototype=new F(); //Child的super指向Parent的prototype,可以通过Child的super访问父构造函数的方法(子对象的同名方法可能已经更改) Child.super=Parent.prototype; //重定向Child的constructor属性,使其指向本身 Child.prototype.constructor=Child; } }()); function Parent(name,sex){ this.name=name; } Parent.prototype.sex='男'; Parent.prototype.saySex=function(){ alert(this.sex); } Parent.prototype.sayName=function(){ alert(this.name); } function Child(age){ this.age=age; } function Child2(love){ this.love=love; } inherit(Child,Parent); inherit(Child2,Parent); var xiaosi=new Child('xiaosi'); var xiaosi2=new Child2('cancan');
这种模式基本上解决了之前4中模式提到的问题,实现了一个相对而言更合适的继承方式,但是,值得注意的是,这种继承模式并没有完美模拟Java的继承,还是有一些自己的区别
1:需要被继承的属性和方法都要放在prototype内,在Java内,这种属性或者方法被认为是public或者是protect属性,但是每个实例的public和protect是独立的,而javascript内,放在prototype的属性是共享的,其表现形式更像java内的static属性
2:按照上面的模式继承,prototype属性内应该放置“可继承属性和方法”,而我们平时的习惯更倾向于“可复用方法和属性”(大多数时候还只有方法,我们希望每个对象都有自己不同的属性),这样按照平时习惯,几乎每一个方法我们都会放在prototype内,这样可以使得实例化对象的占用更少的空间。但是,一旦我们使用“临时代理”继承模式之后,这些方法都会被继承,而这些方法需要访问的对象属性却没有继承,也就不能被访问到,所以我们将会继承到很多“没用”的方法(类似上面例子中的sayName,因为子对象没有name属性)
6.类式继承模式#6——Klass
上述的“临时代理”模式继承已经有一些类式继承的不错的表现了,但是还是不够的,毕竟我们上面提到了这种“代理”模式没有解决属性继承问题,所以,我们有了新的改进:
var klass=function (Parent,props){ var Child,F,i; // 第一步,新的构造函数(在这里解决了属性继承问题,但是不完美) Child=function(){ //注意,实际的创建属性的“构造函数”是由_construct传入的,Child本身函数是这个2个if判断 Child={....} if(Child.uber&&Child.uber.hasOwnProperty('_construct')){ //执行“父类”的构造函数,Child.uber指向“父类”的原型,Child.uber._construct指向父类的构造函数 Child.uber._construct.apply(this,arguments); } if(Child.prototype.hasOwnProperty('_construct')){ //执行本身的构造函数 Child.prototype._construct.apply(this,arguments); } }; // 第二步,实现继承 Parent=Parent||Object; //如果没有继承的话就继承Object,必须要有prototype F=function(){}; //创建一个新的函数断开prototype的直接连接 F.prototype=Parent.prototype; //继承Parent构造函数的原型 Child.prototype=new F(); //创建一个新的F对象继承给Child Child.uber=Parent.prototype; //给Child一个uber属性,指向父类的方法,(可以通过这个访问父类的同名方法) Child.prototype.constructor=Child;//重新制定constructor属性,但是需要注意的是这个constructor并不是指向创建属性的函数 // 第三步,添加实现方法 for(i in props){ //添加自定义的方法(通常情况下)或者可以共用的属性到prototype内 if(props.hasOwnProperty(i)){ Child.prototype[i]=props[i]; } } // 返回这个类 return Child; //返回我们新生成的构造函数 }
如果我们需要使用Klass的话,就要像这样:
var Parent=klass(null, { _construct: function(name) { console.log("Parent的构造函数执行了"); this.name =name; }, sayName:function(){ alert(this.name); } }); var Child=klass(Parent,{ _construct:function(age){ this.age=age; }, sayAge:function(){ alert(this.age); } }); var xiaosi1=new Parent('xiaosi1'); console.log(xiaosi1); var xiaosi2=new Child('xiaosi2'); xiaosi2.sayName=function(){ alert(11); } xiaosi2.sayName(); xiaosi2.constructor.uber.sayName(xiaosi2);
我们需要通过执行Klass函数来改造我们的构造函数并且添加需要的属性,继承关系是在这个时候实现的,然后我们就可以有了“父类”的属性继承下来了,并且构造函数执行也没有重复(父类和子类都只有一次)。
但是,这还不是最右的方法,依旧有自己的问题(虽然和上面的方法比已经好了不少):
1:这种方法直接使用“父类”的构造函数,却没有另外传递参数,导致从“父类”继承下来的属性将会被初始化为错误的值(javascript对函数的参数完全没有限制,虽然我们后期可以改,但是这并不是我们所希望的)
2:访问父类的同名方法有些麻烦,xiaosi2.constructor.uber.sayName(xiaosi2);在上面例子上是这样访问的,因为uber是存储在Child上,并不是xiaosi2上的属性(也不是xiaosi2原型链上的属性),这会使得习惯Java模式的开发者并不是很习惯。毕竟调用一个函数需要重新指定对象不是一个好的体验
7.类式构造模式#7——Simple JavaScript Inheritance
这种方法是由Jquery之父John Resig写的,解决了Klass的不足,基本上完美模拟了Java的类式继承,原文链接:http://ejohn.org/blog/simple-javascript-inheritance/
实现源码:
/* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. */ // Inspired by base2 and Prototype (function(){ var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /_super/ : /.*/; // The base Class implementation (does nothing) this.Class = function(){}; // Create a new Class that inherits from this class Class.extend = function(prop) { var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; var prototype = new this(); initializing = false; // Copy the properties over onto the new prototype for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn){ return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } // The dummy class constructor function Class() { // All construction is actually done in the init method if ( !initializing && this.init ) this.init.apply(this, arguments); } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.prototype.constructor = Class; // And make this class extendable Class.extend = arguments.callee; return Class; }; })();
使用方法:
var ClassParent=Class.extend({ //包装ClassParent构造函数为模拟类 init:function(name){ this.name=name; }, sayName:function(){ alert(this.name); } }); var ClassSon= ClassParent.extend({ //在这里实现继承关系,并且包装ClassSon构造函数为模拟类 init:function(name,age){ this._super(name); //在这里调用父类的同名构造函数方法,传递的参数为字方法的 name this.age=age; }, sayName:function(){ this._super(); //在这里调用父类的同名方法 } }); var objSon=new ClassSon("xiaosi",20); objSon.sayName();
使用模式和Klass比较接近,而函数内部做了很多处理,并且可以实现,在构造函数内部可以通过_super()直接访问父类的同名方法(在对象上的_super()是undefined,这个_super()只在调用方法的时候才存在)。不过这样一来是对象不能访问到父类的同名方法的(不可能存在自己和父类有一个同名方法,却执行父类方法内的内容)
详细中文解释可以查看三生石大大博客:http://www.cnblogs.com/sanshi/archive/2009/07/14/1523523.html
原型继承也被成为现代继承,这种复用模式不依赖类思想,对象都是继承自其它对象
1.现代复用模式#1——初始原型继承
初始继承和之前的原始模式很像,但是不是类思维:
function object(o) { function F() {}; F.prototype = o; return new F(); } function Parent() { this.name = "xiaosi"; } Parent.prototype.getName = function() { return this.name; } var parent = new Parent(); var child = object(Parent.prototype); child.getName(); //undefinde,注意之前继承的是prototype
注意最后返回的值是undefined,因为继承不是基于“类”思想,所以可以直接继承与任何一个对象,这里继承的就是Parent.prototype对象,所以不会有name属性,自然就返回undefined。
ps,顺带一提,原型继承已经成为了ECMAScript5的一部分,使用方法是:var child= Object.create(parnet,{"sex":"男"});前面的参数和上面一样,后面可以再加一个对象参数,用于添加进新对象中
2.现代复用模式#2——通过复制属性实现继承
function clone(parent,child){ var i, toStr=Object.prototype.toString; astr="[object Array]"; child=child||{}; for(i in parent){ if(parent.hasOwnProperty(i)){ if (typeof parent[i] === "object"){ child[i]=(toStr.call(parent[i])===astr)?[]:{}; clone(parent[i],child[i]); }else{ child[i]=parent[i]; } } } return child; }
这个函数是用来深拷贝对象属性的,可以通过这个方法把一个对象属性继承到另一个上,这里没有设计任何原型,仅仅对象属性的拷贝
3.现代复用模式#3——借用和绑定函数
我们考虑到javascript内的函数式一个对象,而这个对象有一个特殊的方法,call和apply,可以指定借用方法的对象。
//函数借用 var one={ name:"object", say:function(greet){ console.log(greet+","+this.name); } } one.say("one"); //one , object var two={ name:"another object" } one.say.apply(two,['hello']); //hello , another object //全局情况下调用变量 var say=one.say; say('xiaosi'); //xiaosi, var xiaosi2={ name:"xiaosi2", method:function(callback){ return callback(this.name); } } xiaosi2.method(one.say); //xiaosi2
在这里,two借用了one的方法,当我们借用函数的时候this指针指向了two,这可能是我们想要的,但也可能不是,也许我们需要作为回调函数或者全局函数调用,这个时候绑定函数或许就是个好选择,以免我们出现this指针指向意外的地方(通常为window)的情况,在window下,明显我们需要的应该是要显示xiaosi,object,但是因为this指向了window,结果导致没有了对应的对象。只能显示xiaosi了,我们需要一个绑定函数的方法:
if(typeof Function.prototype.bind==="undefined"){ Function.prototype.bind=function(thisArg){ var fn=this, slice=Array.prototype.slice, args=slice.call(arguments,1); return function(){ return fn.apply(thisArg,args.concat(slice.call(arguments))); } } }
bind方法就可以实现对指定的this指向,类似apply的用法,从而不会将this指向其他位置
代码重用才是最终目的,继承只是实现重用的方法之一(在javascript内不一定使用继承,依旧可以实现代码复用)