前言
继承,代码复用的一种模式。和其它高级程序语言相比,javascript有点点不一样,它是一门纯面向对象的语言,在JS中,没有类的概念,但也可以通过原型(prototype)来模拟对象的继承和多态。根据javascript对象的特点,JS中的继承又可以分类为引用对象继承和实例对象继承。
引用对象继承,子引用类型继承父引用类型,然后通过子引用类型生成的实例对象,具有父引用类型的特性。 而实例对象继承,继承得到的对象都具有父实例对象的所有属性和方法,其实就是指对象的复制和克隆。
- 默认继承模式
- 借用构造函数模式
- 借用构造函数和设置原型模式
- 共享原型模式
- 临时原型模式
- 借用构造函数+临时原型模式
实例对象继承也有以下几种实现模式:
- 临时-原型继承
- 借用和绑定实现继承(其实是复用)
引用对象继承-默认继承模式
默认继承模式很简单,如果要C继承P,实现代码:
function Parent(name) {
this.name = name || 'Adam';
}
Parent.prototype.say = function() { return this.name;};
function Child(name) { };
function inherit(C , P) {
C.prototype = new P();
}
var c = new C ('xiaoxin' );
alert(c.say()); // 'Adam'
模式特点:
- C不仅继承了P的所有属性,同时还继承了P的原型(P.prototype)的所有属性,但有时候,我们只想继承P的原型的属性;
- C的构造函数不能传递参数,也就是说无法通过给C构造方法传递参数,C将参数传递到P的构造方法(个人认为这个问题不大大,实现方式二 可以解决这个问题)。
引用对象继承-借用构造函数
借用构造函数模式,其实就是指采用Apply或Call来让子对象继承父对象的属性,具体看下面例子:
function Article(name) {
this.name = name;
this.tags =['js','css'];
}
var article = new Article('parent');
function BlogPost() {}
BlogPost.prototype = article;
var blog = new BlogPost();
function Parent2 (name) { //父对象2
this.name2 = name;
}
function StaticPage(name) {
Article.apply(this,arguments); // 子对象可以继承多个父对象
Parent2(this,arguments); // 子对象可以继承多个父对象
}
var page = new StaticPage("page");
alert(article.hasOwnProperty('tags')); //true
alert(blog.hasOwnProperty('tags')); // false 说明:子对象不能继承父对象的prototype中的属性
alert(page.hasOwnProperty('tags')); //true 说明:子对象继承了父对象的属性
blog.tags.push('html');
page.tags.push('php');
alert(article.tags.join(',')); // js,css,html 说明:默认继承模式可以修改父对象的属性; 而借用构造函数模式中,对象继承的属性仅仅是一个副本
模式特点:
- 通过这种方式继承,每个子对象都具有父对象的属性,且该属性是父对象属性的副本(非引用),所以它不会出现子对象通过修改继承的属性而导致父对象属性被覆盖的风险;
- 通过这种方式实现的继承,一个子对象可以继承多个父对象;
- 子对象只能继承父对象的属性,但是不能继承父对象的prototype对象的属性。
引用对象继承-借用构造函数和设置原型
该模式其实就是默认继承模式和借用构造函数的组合所实现的一种模式,该模式可以解决先前两个模式存在的一些问题,具体见下例:
function StaticPage(name) {
Article.apply(this,arguments); // 子对象可以继承多个父对象
// Parent2(this,arguments); // 此时 子对象可以继承多个父对象
}
StaticPage.prototype = new Article(); //设置原型
模式特点:
- 这种模式会导致父对象的属性会被继承两次,导致效率低下。
引用对象继承-共享原型原型
共享原型模式的原理就是:子类对象和父类对象共享一个原型,即父类对象的原型,具体见下面例子:
StaticPage.prototype = Article.prototype; //共享原型
模式特点:
- 这种模式几乎近完美了,它基本上没有以上模式存在的问题。只是有一点需要注意,那就是由于子对象和父对象共享一个原型,如果某个子对象对原型里的属性进行了修改,会导致父对象的原型也发生变化,从而导致所有的子对象实例都会发生变化。
引用对象继承-临时原型模式
前面说了,默认继承模式可能会继承父类对象一些你不想继承的属性,而共享原型模式有存在父类对象属性被修改的风险,怎么办呢?临时构造函数模式则很好的解决了以上存在的问题。临时构造函数模式的原理是: 只继承父类对象的原型对象; 不将父类对象原型直接赋值给子类对象的prototype属性,而是引用一个第三方函数对象,将父类函数对象的prototype赋值给这个第三方函数对象的prototype,然后将这个第三方函数对象的实例对象作为子对象的prototype。
说得有点拗口,具体看下面例子吧:
function inherit(C,P) { //可通过Object.create()方法简化下面代码(ES5)
var F = function() {}; //构造一个第三方临时函数
F.prototype = P.prototype // 原型赋值 (要继承的东西都放到protype对象里)
C.prototype = new F(); //如此,则解决了默认继承和原型共享继承所带来的问题
C.uber = P.prototype ; //可加 可不加 加上后,可以让在对象可以访问父类(超类)
C.prototype.constructor = C; // 重置constructor属性
引用对象继承-用构造函数+临时原型模式
这种方法,就是为了解决借用构造函数和设置原型存在的父对象的属性被继承两次导致效率低下的问题,它是怎么实现的呢?看下面代码:
function Child() {//继承Parent对象里的属性
Parent.call(this);
}
//inherit可参照上面章节代码
inherit(Child, Parent);
这种方法是最合适,也是用得最广的继承模式,在nodejs中,推荐采用这种方式来实现继承。比如,我想继承EventEmitter引用类型,代码如下:
function MyEvent() {
events.EventEmitter.call(this);
}
util.inherits(MyEvent, events.EventEmitter);//使这个类继承EventEmitter
实例对象继承-原型继承
现代继承和传统继承不同,它指得不是类的继承,而是指对象的继承,其实质就是:创造出一个对象,和父对象具有相同的属性 。 示例:
function object(parent) { //对象继承的公共方法
var F = function() {}; //创建一个临时函数对象
F.prototype = parent;
return new F(); //返回一个新的实例对象
}
function Person() {
this.name = 'xiaoxin';
}
Person.prototype.getName = function() { return this.name; }
var pa = new Person();
var kid = object(pa);
alert(pa.name); // xiaoxin 继承了父对象属性
alert(kid.getName()); //xiaoxin 继承了父对象的prototype属性
实例对象继承-对象复制(克隆)
对象的复制也是一种对象继承的方式(注意JS中现代继承的意义)。下面是一个简单的浅克隆方式,浅克隆相对简单,如果需要进行精确的对象复制,建议采用深克隆。Jquery中就应用了大量的深度克隆来实现 jquery 功能和插件的扩展。
function extend(parent,child) { //浅克隆的简单实现
var att ;
child = child || {};
for(i in parent) {
if(parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}
实例对象继承-借用和绑定
前面说了对象复制这种模式,它对父对象所有属性进行克隆复制,生成一个子对象,从而实现了父对象中属性代码的复用。但是,有时,我们并不需要复用父对象的所有属性,而是仅仅只需要使用某个属性而已,此时我们可以考虑采用借用模式来实现。借用模式的原理:采用apply或call来实现对象中方法的复用。
具体看下面一个例子:
//假如 我们需要复用数组对象的slice()方法,用来截取类数组arguments中的一段
function f() {
var args = [].slice.call(arguments,1,3);// 只复用Array中的slice方法。
// var args = Array.prototype.slice.call(arguments,1,3); //需要输入更长的字符,但是节省了创建一个数组的开销
return args;
}
f(1,2,3,4,5,6); //返回 [2,3]
面上面的模式用来普通的方法(方法类不含this关键字)是没有问题的,但是如果复用的方法内包含有this关键字,且该方法又作为全局方法或回调函数传递,则会出现一点点小问题,具体看下面示例:
var one = {
name : "object",
say: function(greet) { return greet+","+this.name; } //被复用的方法 里面包含this关键字
}
one.say('hi'); // hi,object
var two = { name: 'another object' };
one.say.apply(two, ['hello']) ; // hello,another object //这是没问题的,相当于已经绑定对象到 two了
var say = one.say;
say('hello'); //hello,underfined 未绑定对象,导致 this为全局对象,所以取不到name值
var yetanother = {
name: 'yet another object',
method: function(callback) {
return callback('Hola');
}
}
yetanother.method(one.say); // Hola,underfined 未绑定对象,导致 this为全局对象,所以取不到name值
面以上例子表明:如果要复用方法,一定要将复用的对象和被复用的方法绑定,而全局对象或回调函数的形式是未绑定的,如果使用前要绑定呢?要和two对象进行绑定呢?
function bind(method,obj) {
//注意这个红色的return 放回的是一个function方法
return function() {
//绑定过程
return method.apply(obj,[].slice.call(arguments));
};
}
//相当于 var say = function() { return one.say.apply(two,[].slice.call(arguments));}
var say = bind(one.say,two);
say('hello'); //hello,another object