引言
上一节《javascript基础–对象(Object)封装》讲了如何造人,这一节将会总结怎么理清人与人之间的血缘关系,即继承关系。对象继承简单地分为两类,构造函数继承和非构造函数继承。所谓构造函数继承就是利用this绑定对象的模式创建的对象之间的继承关系;而非构造函数继承就是利用对象字面量创建的对象之间的继承关系。上代码:
//Person父对象利用this绑定,子对象Tom,Linda也同样采用这种模式创建
function Person(name,sex){
this.name = name;
this.sex = sex;
}
Person.prototype.say = function(){
console.log(this.name +' is ' + this.sex);
}
//创建的Tom,Linda去继承Person父对象,此种继承成为构造函数继承
//Person采用对象字面量创建,子对象Tom,Linda也同样采用这种模式创建
var Person = {
Pname: 'Person',
Psex: 'Person sex is male or female',
say: function(){
console.log('haha');
}
}
//创建的Tom,Linda去继承Person父对象,此种继承成为非构造函数继承
1.构造函数继承
首先确定父对象,即引言内的父对象,copy下来
function Person(name,sex){
this.name = name;
this.sex = sex;
}
Person.prototype.say = function(){
console.log(this.name +' is ' + this.sex);
}
我们的任务是创建Tom和Linda去继承Person,我们先创建子对象模型
//Tom构造函数
function Tom(name,sex){}
Tom.prototype = {}
var T = new Tom('Tom','male');
//Linda构造函数
function Linda(name,sex){}
Linda.prototype = {}
var L = new Linda('Linda','female');
分析:父对象Person中属性和方法分为两部分,name和sex属性是在构造函数Person上,而say方法是在Person.prototype上。所以要继承Person所有属性和方法,分为两步,即继承构造函数上的属性和继承原型对象上的方法。
2.构造函数继承–对象冒充
对象冒充,是利用call或者apply方法,将父对象的属性和方法绑定在子对象上,从而完成构造函数上属性和方法的继承,但是person原型对象上的say方法不会继承。这里简单介绍一下call和apply
//call定义
A.call(B,Object);
//释义:调用A对象的方法,以B对象替换当前的A对象,其中参数是Object
//apply定义
A.apply(B,array);
//释义:调用A对象的方法,以B对象替换当前的A对象,其中参数必须是Array类型
//so call和apply区别就在参数的形式上不同
现在来完善Tom和Linda构造函数
//Tom构造函数
function Tom(name,sex){
Person.apply(this,arguments);
}
Tom.prototype = {}
var T = new Tom('Tom','male');
//Linda构造函数
function Linda(name,sex){
Person.apply(this,arguments);
}
Linda.prototype = {}
var L = new Linda('Linda','female');
测试:
//可见Tom和Linda成功继承了Person构造函数上的属性,Person原型对象属性怎么继承呢?
T.name;//'Tom'
T.sex;//'male'
T.say;//undefined
L.name;//'Linda'
L.sex;//'female'
L.say;//undefined
3.构造函数继承–原型继承
原型继承,顾名思义肯定要在子对象原型上做文章,首先想到的是直接继承Person.protype,就像这样
//Tom构造函数
function Tom(name,sex){
Person.apply(this,arguments);
}
Tom.prototype = Person.prototype;
var T = new Tom('Tom','male');
//Linda构造函数
function Linda(name,sex){
Person.apply(this,arguments);
}
Linda.prototype = Person.prototype;
var L = new Linda('Linda','female');
测试:
//say被继承了,完了??没完
T.say();//Tom is male
L.say();//Linda is female
我们为Tom原型对象上添加一个run方法,Linda原型对象上不添加,期望结果是T.run返回function,L.run返回undefined
Tom.prototype.run = function(){
console.log('Tom running');
}
//测试
T.run();//'Tom running'
L.run();//'Tom running'
Person.prototype.run();//'Tom running'
//可见修改Tom的原型对象也同样修改了Person.prototype
原因是,prototype是一个地址,子对象原型继承的是地址,而非数值。在C语言中,指针就是这样一个概念。那怎么避免呢?既然地址不行,那就用值,不就ok了。所以让子对象原型继承父对象实例是一种方式。
//Tom构造函数
function Tom(name,sex){
Person.apply(this,arguments);
}
//想想封装函数的原型链,任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Tom.prototype = new Person();"这一行,Tom.prototype.constructor是指向Tom的;加了这一行以后,Tom.prototype.constructor指向Person。所以我们必须手动纠正
Tom.prototype = new Person();
Tom.prototype.constructor = Tom;
var T = new Tom('Tom','male');
//Linda构造函数
function Linda(name,sex){
Person.apply(this,arguments);
}
//同理
Linda.prototype = new Person();
Linda.prototype.constructor = Linda;
var L = new Linda('Linda','female');
Tom.prototype.run = function(){
console.log('Tom running');
}
测试:
//测试结果表明,我们成功了
T.say();//'Tom is male'
L.say();//'Linda is female'
T.run();//'Tom running'
L.run();//undefined
Person.prototype.run();//undefined
除了利用继承值以外,还有别的方法吗?肯定有,那就是利用空对象作为中介,让子对象的原型继承空对象,空对象继承父对象原型,这样间接实现继承,而且避免了直接继承prototype的缺点,给子对象添加自身额外的方法时只会改变空对象,而不会改变父对象,这样就达到和继承值一样的目的。
var Empty = function(){};
Empty.prototype = Person.prototype;
Tom.prototype = new Empty();
Tom.prototype.constructor = Tom;
将此方法进行封装成extend方法
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
//为了保证继承函数的完整性,增添一个通向父对象原型的接口
Child.uber = Parent.prototype;
}
现在将Tom和Linda构造函数改变
//Tom构造函数
function Tom(name,sex){
Person.apply(this,arguments);
}
//调用extend函数
extend(Tom,Person);
var T = new Tom('Tom','male');
//Linda构造函数
function Linda(name,sex){
Person.apply(this,arguments);
}
//同理
extend(Linda,Person);
var L = new Linda('Linda','female');
Tom.prototype.run = function(){
console.log('Tom running');
}
测试:
//成功了
T.name;//'Tom'
T.say();//'Tom is male'
T.run;//function
L.name;//'Linda'
L.say();//'Linda is female'
L.run;//undefined
Person.prototype.run;//undefined
4.构造函数继承–拷贝继承
上面讲了为了继承原型上的属性和方法,我们使用了原型继承。这里有一个新思路,就是拷贝继承,即将父对象原型上的属性和方法直接拷贝到子对象上,从而实现继承。这样也避免了改变子对象原型的时候不会改变父对象原型,因为拷贝的是值而不是地址。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
//为了保证继承函数的完整性,增添一个通向父对象原型的接口
c.uber = p;
}
现在改变Tom,Linda构造函数
//Tom构造函数
function Tom(name,sex){
Person.apply(this,arguments);
}
//调用extend2函数
extend2(Tom,Person);
var T = new Tom('Tom','male');
//Linda构造函数
function Linda(name,sex){
Person.apply(this,arguments);
}
//同理
extend2(Linda,Person);
var L = new Linda('Linda','female');
Tom.prototype.run = function(){
console.log('Tom running');
}
测试,不出意外,结果一样
//意料之中,成功了
T.name;//'Tom'
T.say();//'Tom is male'
T.run;//function
L.name;//'Linda'
L.say();//'Linda is female'
L.run;//undefined
Person.prototype.run;//undefined
5.非构造函数继承
还是先确定父对象,即引言内的父对象,copy下来
var Person = {
Pname: 'Person',
Psex: 'Person sex is male or female',
say: function(){
console.log('haha');
}
}
我们的任务是创建Tom和Linda去继承Person,我们先创建子对象模型
var Tom = {};
var Linda = {};
分析:这里要注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现”继承”。
6.非构造函数继承–object()方法
json格式的发明人Douglas Crockford,提出了一个object()函数
function object(o){
function F(){};
F.prototype = o;
return new F();
}
添加Tom和Linda对象私有方法
Tom = object(Person);
Tom.name = 'Tom';
Linda = object(Person);
Linda.name = 'Linda';
测试:
Tom.Pname;//'Person'
Tom.Psex;//'Person sex is male or female'
Tom.say;//function
Tom.name;//'Tom'
Linda.Pname;//'Person'
Linda.Psex;//'Person sex is male or female'
Linda.say;//function
Linda.name;//'Linda'
7.非构造函数继承–浅拷贝
和构造函数的实现思路一样,拷贝也是实现继承的一种方式
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
//为了保证继承函数的完整性,增添一个通向父对象的接口
c.uber = p;
return c;
}
改变Tom, Linda对象
Tom = extendCopy(Person);
Tom.name = 'Tom';
Linda = extendCopy(Person);
Linda.name = 'Linda';
测试:
//同样成功了
Tom.Pname;//'Person'
Tom.Psex;//'Person sex is male or female'
Tom.say;//function
Tom.name;//'Tom'
Linda.Pname;//'Person'
Linda.Psex;//'Person sex is male or female'
Linda.say;//function
Linda.name;//'Linda'
为什么叫做浅拷贝呢,现在假设父对象中有一个属性是数组或者对象,为父对象增加一个属性color数组
Person.color = [yellow,white,black];
//Tom继承Person
Tom = extendCopy(Person);
//测试
Tom.color;//[yellow,white,black]
//现在我们为Tom.color
Tom.color.push('blue');
//测试
Tom.color;//[yellow,white,black,blue]
Person.color;//[yellow,white,black,blue]
//???为什么改变子对象,父对象也跟着改变了呢?
//原因是数组或对象是地址,相当于拷贝了地址,所以会出现这种情况。为了避免这种情况,也就有了深拷贝
8.非构造函数继承–深拷贝
深拷贝是在前拷贝的基础上增添了对父对象属性的判断,判断是否为地址,如果是,则递归拷贝函数
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
改变Tom和Linda对象
Tom = deepCopy(Person,Tom);
Tom.name = 'Tom';
Linda = deepCopy(Person,Tom);
Linda.name = 'Linda';
测试:
Person.color = [yellow,white,black];
//Tom继承Person
Tom = extendCopy(Person);
//测试
Tom.color;//[yellow,white,black]
//现在我们为Tom.color
Tom.color.push('blue');
//测试
Tom.color;//[yellow,white,black,blue]
//父对象没有改变
Person.color;//[yellow,white,black]
jQuery使用的深拷贝
结束语
对象的继承到这里就结束了,其实也蛮简单的。针对两种情况,构造函数继承其实就是对象冒充,原型,拷贝三种方式;非构造函数就是object()和浅深拷贝。只要深入理解,记忆起来还是蛮easy