标签:
在之前的文章中,我们借助构造函数实现了“类”,然后结合原型对象实现了“继承”,并了解了JavaScript中原型链的概念。
理解这些内容,有助于我们更深入地进行JavaScript面向对象编程。
由于JavaScript是一门基于对象和原型的弱语言,灵活度非常高,这使得JavaScript有各种套路去实现继承。本篇文章将逐一介绍实现继承的12种套路,它们可以适用于不同的场景,总一种套路适合你。
(亲:文章有点长,请点击右侧的「显示文章目录」按钮,以便导航和阅读哦。)
这是实现继承的经典方式,这种方式我就不再多做介绍了,具体请参考上一篇文章。
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { return ‘Hello, I am ‘ + this.name +‘!‘; } function Employee(name,email) { this.name = name; this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee(‘keepfool‘,‘keepfool@xxx.com‘);
以上的sayHello()方法是定义在Person.prototype上的,name属性在Employee()构造函数也定义了,所以我们可以只用继承副高早函数的原型。
/* * 继承方式02:共用父构造函数的原型对象 */ function Person(name) { this.name = name; } Person.prototype.sayHello = function() { return ‘Hello, I am ‘ + this.name +‘!‘; } function Employee(name,email) { this.name = name; this.email = email; } Employee.prototype = Person.prototype; Employee.prototype.constructor = Employee;
和01方式唯一的区别在于Employee.prototype = Person.prototype
这行代码。
Employee.prototype.sayHello = function(){ return ‘Hello, I am ‘ + this.name + ‘, my email is ‘ + this.email; } var person = new Person(‘Jack‘);
这段代码修改了Employee.prototype.sayHello方法,同时也影响了Person.prototype.sayHello方法。
在01和02两种方式中,Person()构造函数和Employee()构造函数都定义了name属性。
如果Person()和Employee()构造函数相同的属性很多,在Employee()构造函数中将会出现大量重复的this.xxx = xxx
赋值操作。
使用apply()方法借用Person()构造函数,可以减少这些重复的赋值操作。
function Person(name) { this.name = name; //调用Person.apply(this,[name])后,emp对象也会拥有favorites属性 this.favorits = [‘orange‘,‘apple‘]; } Person.prototype.sayHello = function() { return ‘Hello, I am ‘ + this.name +‘!‘; } function Employee(name,email) { Person.apply(this,[name]); this.email = email; } var emp = new Employee(‘keepfool‘, ‘keepfool@xxx.com‘);
注意:当未指定Employee.prototype = new Person()时,emp对象是可以访问favorites属性的。因为Person.apply(this,[name,age]);中的this是Employee()构造函数的实例,调用apply方法时,Person()构造函数中的属性和方法都会被分配给this对象,所以emp对象是可以访问favorites属性的。
/* * 继承方式03:借用构造函数 */ function Person(name) { this.name = name; //调用Person.apply(this,[name])后,emp对象也会拥有favorites属性 this.favorits = [‘orange‘,‘apple‘]; } Person.prototype.sayHello = function() { return ‘Hello, I am ‘ + this.name +‘!‘; } function Employee(name,email) { // 第2次调用Person()构造函数 Person.apply(this,[name]); this.email = email; } // 第1次调用Person()构造函数 Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee(‘keepfool‘, ‘keepfool@xxx.com‘);
Employee.prototype = new Person();
,第2次是调用Person.apply()
方法。/* * 继承方式04:使用临时构造函数 */ // Person function Person(name) { this.name = name; this.favorites = [‘orange‘,‘apple‘]; } Person.prototype.sayHello = function() { return ‘Hello, I am ‘ + this.name +‘!‘; } // Employee function Employee(name,email) { this.name = name; this.email = email; } // Developer function Developer(name, email, skills){ this.name = name; this.email = email; this.skills = skills; } /* 1.借助临时构造函数实现Employee()继承Person() */ var F = function() {}; F.prototype = Person.prototype; Employee.prototype = new F(); Employee.prototype.constructor = Employee; /* 2.借助临时构造函数实现Developer()继承Employee() */ var F = function(){}; F.prototype = Employee.prototype; Developer.prototype = new F(); Developer.prototype.constructor = Developer; var emp = new Employee(‘keepfool‘,‘keepfool@xxx.com‘); var dev = new Developer(‘Jack‘,‘Jack@xxx.com‘,[‘C#‘,‘JavaScript‘,‘HTML5‘])
注意:子构造函数只继承定义在父构造函数原型对象上的属性和方法,例如:Employee()只继承定义在Person.prototype上的属性和方法,Person()构造函数中定义的favorites属性不会被Employee()继承。
在介绍uber前,我们先看下面一则代码:
// Person function Person(name) { this.name = name; } Person.prototype.type = ‘Person‘; Person.prototype.toString = function(){ return this.type; } // Employee function Employee(name,email) { this.name = name; this.email = email; } // Developer function Developer(name, email, skills){ this.name = name; this.email = email; this.skills = skills; } var F = function() {}; F.prototype = Person.prototype; Employee.prototype = new F(); Employee.prototype.constructor = Employee; Employee.prototype.type = ‘Employee‘; var F = function(){}; F.prototype = Employee.prototype; Developer.prototype = new F(); Developer.prototype.constructor = Developer; Developer.prototype.type = ‘Developer‘; var emp = new Employee(‘keepfool‘,‘keepfool@xxx.com‘); var dev = new Developer(‘Jack‘,‘Jack@xxx.com‘,[‘C#‘,‘JavaScript‘,‘HTML5‘])
这则代码通过临时构造函数构建了Developer → Employee → Person继承关系。
调用emp.toString(),会输出"Employee"; 调用dev.toString(),则会输出"Developer"。
如果希望emp.toString()输出"Person,Employee",dev.toString()输出"Person,Employee,Developer",我们该如何做呢?
这意味着我们需要遍历原型链。
在构建继承关系时,可以为子构造函数引入uber属性,并将它指向父构造函数的原型对象(因为toString()方法是定义在Person.prototype上的)。
uber这个词表示“超级的”(不是优步哦),意指引用父类。为什么不用super呢?因为super是JavaScript的一个保留关键字。
// Person function Person(name) { this.name = name; } Person.prototype.type = ‘Person‘; Person.prototype.toString = function(){ return this.constructor.uber ? this.constructor.uber.toString() + ‘,‘ + this.type : this.type; } // Employee function Employee(name,email) { this.name = name; this.email = email; } // Developer function Developer(name, email, skills){ this.name = name; this.email = email; this.skills = skills; } var F = function() {}; F.prototype = Person.prototype; Employee.prototype = new F(); Employee.prototype.constructor = Employee; /* 1.引入uber属性,使它指向Person()构造函数的原型对象 */ Employee.uber = Person.prototype; Employee.prototype.type = ‘Employee‘; var F = function(){}; F.prototype = Employee.prototype; Developer.prototype = new F(); Developer.prototype.constructor = Developer; /* 2.引入uber属性,使它指向Person()构造函数的原型对象 */ Developer.uber = Employee.prototype; Developer.prototype.type = ‘Developer‘; var emp = new Employee(‘keepfool‘,‘keepfool@xxx.com‘); var dev = new Developer(‘Jack‘,‘Jack@xxx.com‘,[‘C#‘,‘JavaScript‘,‘HTML5‘])
再次调用emp.toString()和dev.toString()方法:
有些人可能不是明白这个过程,也可能会对下面这几行代码产生好奇:
Person.prototype.toString = function(){ return this.constructor.uber ? this.constructor.uber.toString() + ‘,‘ + this.type : this.type; }
我们以emp对象为例来解释为什么emp.toString()会输出"Person,Employee"。
在构建Devloper → Employee → Person的继承关系时,我们使用了2次临时构造函数,这些代码是重复的,我们可以把实现继承关系的代码提炼为一个函数。
/* * 继承方式04:使用临时构造函数-精简版 */ // Person function Person(name) { this.name = name; } Person.prototype.type = ‘Person‘; Person.prototype.toString = function(){ return this.constructor.uber ? this.constructor.uber.toString() + ‘,‘ + this.type : this.type; } // Employee function Employee(name,email) { this.name = name; this.email = email; } // Developer function Developer(name, email, skills){ this.name = name; this.email = email; this.skills = skills; } function extend(Child,Parent){ var F = function(){}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; Child.uber = Parent.prototype; } extend(Employee,Person); Employee.prototype.type = ‘Employee‘; extend(Developer,Employee); Developer.prototype.type = ‘Developer‘; var emp = new Employee(‘keepfool‘,‘keepfool@xxx.com‘); var dev = new Developer(‘Jack‘,‘Jack@xxx.com‘,[‘C#‘,‘JavaScript‘,‘HTML5‘])
这段代码引入了一个extend()函数,指定了2个参数,分别表示Child和Parent,并将临时构造函数封装在exntend()中。
当我们需要构建继承关系时,首先定义好构造函数,然后将构造函数传入extend()方法就可以实现继承了。 使用extend()方法,既能够让我们的代码保持整洁,又能够达到重用的目的。
由于继承是为了使代码能够重用,难道我们就不能简单地将一个对象的属性拷贝给另外一个吗?
这当然是可以的,沿用上面的代码,我们将extend()方法替换为extend2()方法:
function extend2(Child,Parent){ var p = Parent.prototype; var c = Child.prototype; for(var i in p){ c[i] = p[i]; } c.uber = p; }
通过for循环,我们将Parent.prototype中的属性拷贝到了Child.prototype。
extend2()方法和extend()方法有两个不同之处。
在extend()方法中,由于Child.prototype = new F()这行代码使得Child.prototype被覆盖了,所以extend()方法需要重写Child.prototype.constructor。
而在extend2()方法中,Child.prototype没有被覆盖,所以无需重写Child.prototype.constructor,Child.prototype.constructor本来就是指向Child的。
下面这段代码,定义了一个Person()和Employee()构造函数,Person.prototype提供了一个基础类型的type属性,以及一个复杂类型的toString()方法。
// Person function Person(name) { this.name = name; } Person.prototype.type = ‘Person‘; Person.prototype.toString = function(){ return this.constructor.uber ? this.constructor.uber.toString() + ‘,‘ + this.type : this.type; } // Employee function Employee(name,email) { this.name = name; this.email = email; }
如果使用extend()函数构建继承关系,则type属性既不是emp对象的属性,也不是Employee.prototype的属性(提示:Employee.prototype === emp.__proto__)。
如果使用extend2()函数构建继承关系,type属性会成为Employee.prototype的属性。
需要注意的是,toString()方法由于是复杂类型,extend2()方法只拷贝了toString()方法的引用。
也就是说Employee.prototype.toString和Person.prototype.toString是同一个引用。
通过以上示例,可以看到extend2()函数的效率是不如extend()函数的,因为extend2()在拷贝属性时,每一个定义于Parent.prototype中的属性,都要在Child.prototype中重建。
但这并不是很糟糕,因为extend2()方法只重建了基础类型,复杂类型则是拷贝了引用,重建基础类型属性造成的性能的损失是可以接受的。
extend2()方法有两个好处:
实际上,复杂类型(引用类型)都是通过拷贝引用来完成的,共用一个引用可能会导致一些预期之外的结果。
例如:在Person.prototype上定义一个favorites属性,它是数组类型的,当然也是引用类型。
Person.prototype.favorites = [‘orange‘,‘apple‘];
然后我们使用Employee()构造函数创建两个对象。
var emp1 = new Employee(‘Jack‘, ‘jack@xxx.com‘); var emp2 = new Employee(‘Rose‘,‘rose@xxx.com‘); emp2.favorites; // [‘orange‘,‘apple‘] emp1.favorites.push(‘banana‘); emp2.favorites; // [‘orange‘,‘apple‘,‘nanana‘]
emp2.favorites最开始是[‘orange‘,‘apple‘],然后通过数组的push方法更改了emp1.favorites,由于emp2.favorites和emp1.favorites指向同一个引用,所以emp2.favorites也变成了 [‘orange‘,‘apple‘,‘nanana‘]。
也就是说,emp1的改变影响了emp2,这不是我们预期的结果。
所以当使用extend2()实现继承时,对待引用类型应该谨慎,应该尽量避免对象修改类型为引用类型的属性。
借用构造函数会带来重复执行两次构造函数的问题,我们可以结合apply()和extend2()函数来修复这个问题。
使用apply()方法调用父构造函数,获取父构造函数自身的属性,然后使用extend2()函数继承父构造函数的原型属性。
/* * 继承方式06.借用构造函数并拷贝原型 */ function Person(name) { this.name = name; this.favorites = [‘orange‘,‘apple‘]; } Person.prototype.type = ‘Person‘; Person.prototype.sayHello = function() { return ‘Hello, I am ‘ + this.name +‘!‘; } function Employee(name,email) { // 调用Person()构造函数 Person.apply(this,[name]); this.email = email; } function extend2(Child,Parent){ var p = Parent.prototype; var c = Child.prototype; for(var i in p){ c[i] = p[i]; } c.uber = p; } // 继承Person()构造函数的原型对象的属性 extend2(Employee,Person); Employee.prototype.type = ‘Employee‘; var emp = new Employee(‘keepfool‘,‘keepfool@xxx.com‘);
这时,emp对象不仅继承了Person()构造函数中的属性,也继承了Person()构造函数原型对象上的属性。
另外,emp对象还可以通过uber属性访问父对象。
这种方式是03和05的结合,它使得我们可以在不重复调用父构造函数的情况下,同时继承父构造函数的自身属性和原型属性。
在这之前,我们使用构造函数来创建“类”,并使用new构造函数创建对象。 然后,我们通过Child.prototype = new Parent()
来构建继承关系,这里的new Parent()也是调用了构造函数。
在实现“类”、“继承”这些概念的过程中,构造函数充当了一个中间人的作用,继承的目的是对象的属性和方法可以被其他对象重用。
如果不使用构造函数,直接进行对象之间的拷贝难道不可行吗?
这当然是可行的,我们首先介绍一种方式——对象的浅拷贝。
/* * 继承方式07:对象的浅拷贝 */ function shallowCopy(p){ var c = {}; for(var i in p){ c[i] = p[i]; } c.uber = p; return c; } var person = { type : ‘Person‘, toString : function(){ return this.type; } } var emp = shallowCopy(person); emp.type = ‘Employee‘; emp.name = ‘keepfool‘; emp.email = ‘keepfool@xxx.com‘; // 在重写emp的toString()方法前,emp.toString === person.toString为true emp.toString = function(){ return this.uber.toString() + ‘, ‘ + this.type; }
注意:
1. c.uber = p
这行代码,表示子对象的uber属性指向父对象。
2. toString()方法是引用类型,拷贝时只拷贝引用。然后emp.toString = function() { ... }将重写了该方法,重写不会影响person.toString。
该方式不仅继承了父对象的属性,还可以通过uber属性来访问父对象。
在拷贝属性时,如果是引用类型的拷贝,由于共用一个对象,则可能存在一些隐患,深拷贝有助于解决这个问题。
浅拷贝和深拷贝最大的区别是:如果属性为复杂类型,浅拷贝是拷贝其引用,而深拷贝则会创建一个新的复杂类型。
/* * 继承方式08:对象的深拷贝 */ function deepCopy(p,c){ c = c || {}; for(var i in p){ // 属性i是否为p对象的自有属性 if(p.hasOwnProperty(i)){ // 属性i是否为复杂类型 if(typeof p[i] === ‘object‘){ // 如果p[i]是数组,则创建一个新数组 // 如果p[i]是普通对象,则创建一个新对象 c[i] = Array.isArray(p[i]) ? [] : {}; // 递归拷贝复杂类型的属性 deepCopy(p[i],c[i]); }else{ // 属性是基础类型时,直接拷贝 c[i] = p[i]; } } } return c; }
深拷贝的实现逻辑,已经很清晰地在注释中描述了,请注意这段代码是如何处理数组类型和对象类型的。
下面这段代码使用deepCopy()方法创建了一个child对象。
var parent = { name : ‘keepfool‘, age : 28, favorites : [‘orange‘,‘apple‘], experience : { limit : 7, skills : [‘C#‘,‘JavaScript‘,‘HTML5‘] } }; // 修改child对象的属性,不会影响parent对象 var child = deepCopy(parent);
如果修改child对象的复杂类型属性,不会对parent对象造成影响,因为child和parent是两个完全独立的个体,它们互不依赖。
基于对象继承对象的理念,Douglas Crockford提出了一个建议,使用object()函数,接收父对象,然后返回父对象的原型。
function object(o) { function F() {} F.prototype = o; return new F(); }
如果要访问父对象,则为子对象添加uber属性,并指向父对象。
/* * 继承方式09:使用object()函数 */ function object(o) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; return n; }
使用object()函数和使用shallowCopy()函数是一样的,复杂类型的拷贝仍然是引用拷贝。
var person = { type: ‘Person‘, favorites : [‘orange‘,‘apple‘], toString: function() { return this.type; } } var emp = object(person);
继承的目的之一在于重用已有对象的属性,然后在子对象上扩展一些额外的属性。
既然如此,我们可以将原型继承和属性拷贝混合起来使用。
通俗地讲,就是我们不仅使用现有对象(使用原型的属性),还要基于现有的对象扩展一些属性。
/* * 继承方式10:原型继承与属性拷贝的混合模式 */ function objectPlus(o, stuff) { var n; // 1. 从对象o继承原型 function F() {} F.prototype = o; n = new F(); n.uber = o; // 2. 从对象stuff拷贝属性 for (var i in stuff) { n[i] = stuff[i]; } return n; }
objectPlus()方法有两个参数,第1个参数用于继承原型,第2个参数用于拷贝属性。
下面这段代码:person对象是一个参数,用于继承原型;{}匿名对象中定义了一些属性,用于扩展子对象的属性。
var person = { type: ‘Person‘, favorites : [‘orange‘,‘apple‘], toString: function() { return this.type; } } var emp = objectPlus(person, { type : ‘Employee‘, name: ‘keepfool‘, email: ‘keepfool@xxx.com‘, toString : function(){ return this.uber + ‘,‘ + this.type; } });
这种方式使得我们一次性完成了对象的继承和扩展。
像C#,Java这样的面向对象编程语言,是不支持多重继承的(但是支持多重接口实现)。
但对于JavaScript这样的动态语言,实现多重继承就比较简单了。
下面这段代码定义了一个multi()函数,它没有显式地定义参数。但通过arguments可以获取调用函数时的参数,所以我们只需遍历arguments参数,然后拷贝每个参数对象的属性即可。
/* * 继承方式11:多重继承 */ function multi() { var n = {}, stuff, j = 0, len = arguments.length; for (j = 0; j < len; j++) { stuff = arguments[j]; for (var i in stuff) { n[i] = stuff[i]; } } return n; }
使用multi()函数:
var person = { type: ‘Person‘, toString: function() { return this.type; } }; var emp = { type: ‘Employee‘, name: ‘keepfool‘, email: ‘keepfool@xxx.com‘ }; var dev = multi(person, emp, { type: ‘Developer‘, age: 28, skills: [‘C#‘, ‘JavaScript‘] });
注意:如果arguments数组中的对象存在相同的属性,则后遍历的对象属性会覆盖先遍历的对象属性。
寄生继承是指:在创建对象的函数中,创建要返回的对象时,首先直接吸收其他对象的属性,然后再扩展自己的属性。
这个过程就好似一个对象的创建是寄生在另外一个对象上完成的。
下面这段代码,employee()是寄生继承函数,实现寄生函数时借用了09条的object()函数。
/* * 继承方式12:寄生继承 */ function object(o) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; return n; } var person = { type: ‘Person‘, toString: function() { return this.type; } }; // employee()是寄生函数 function employee(name, email) { // 寄生peson对象 var that = object(person); // 然后扩展自己的属性 that.type = ‘Employee‘; that.toString = function(){ return this.uber.type + ‘,‘ + this.type; } return that; } var emp = employee(‘keepfool‘,‘keepfoo@xxx.com‘);
注意:这段代码中的that不是一个关键字,它只是一个普通的对象。
由于这篇文章的篇幅较长,读到这儿的人,可能已经忘了我这篇文章开头讲的是什么内容了。但这并不要紧,咱们来个全篇的总结,能让你马上回忆起来。
编号 | 原型链 | 示例 |
---|---|---|
01 | 原型链(经典模式) | Child.prototype = new Parent(); |
02 | 仅继承父构造函数的原型 | Child.prototype = Parent.prototype; |
03 | 借用构造函数 | function Child() { Parent.apply(this, arguments); } |
04 | 临时构造函数 | function extend(Child,Parent) { var F = function() {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; Child.uber = Parent.prototype; } |
05 | 复制父构造函数的原型属性 | function extend2(Child, Parent) { var p = Parent.prototype; var c = Child.prototype; for (vari in p) { c[i] = p[i]; } c.uber = p; } |
06 | 借用构造函数并拷贝原型 | function Child() { Parent.apply(this, arguments); } extend2(Child,Parent); |
07 | 基于对象的浅拷贝 | function shallowCopy(p) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; } |
08 | 基于对象的深拷贝 | function shallowCopy(p) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; } |
09 | 原型继承 | function object(o) { function F() {} F.prototype = o; return new F(); } |
10 | 原型继承与属性拷贝的混合模式 | function objectPlus(o, stuff) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; for (var i in stuff) { n[i] = stuff[i]; } return n; } |
11 | 多重继承 | function multi() { var n = {},stuff, j = 0,len = arguments.length; for (j = 0; j < len; j++) { stuff = arguments[j]; for (var i in stuff) { n[i] = stuff[i]; } } return n; } |
12 | 寄生继承 | function parasite(victim) { var that = object(victim); that.more = 1; return that; } |
面对这么多的方法,你该如何选择呢?这取决于性能的需求、任务的目标以及设计风格等等。
如果你已经习惯了从“类”的角度去理解和分析问题,那么基于构造函数的继承实现比较适合你,01~06方式都是基于构造函数的。
如果你只是处理某些具体对象或实例,那么基于对象的继承实现比较适合你,07~12方式都是基于对象的。
《Object-Oriented JavaScript 2nd Edition》
玩转JavaScript OOP[4]——实现继承的12种套路
标签:
原文地址:http://www.cnblogs.com/keepfool/p/5592256.html