JavaScript 继承与拷贝
Date: 7th of Aug, 2015
Author: HaoyCn
本文讨论JavaScript中如何实现继承关系,以及如何拷贝对象。下面,我们分别探讨4种继承方法。谈完继承方法后,再谈论对象的拷贝。
需要提前说明的是,后文需要继承的构造器函数是:
var Animal = function(name){
this.name = name;
};
Animal.prototype.jump = function(){
console.log('jumped');
};
原型链继承法
本方法的特性在于,能够把可以重用的部分迁移到原型链中,而不可重用的则设置为对象的自身属性。
var Human = function(name){
this.name = name;
};
// 这一步会使得
// Human.prototype.constructor = Animal;
// 所以需要重新手动重定向
Human.prototype = new Animal;
// 如果不更改
// 通过`Human`构造器构造出来的对象的构造器会变成`Animal`而不是`Human`
Human.prototype.constructor = Human;
var man = new Human('HaoyCn');
man.jump();
现在,对象 man
可以使用 Animal.prototype.jump
方法,查找过程是:
-
man
自身没有jump
方法 - 查找
man.constructor.prototype
,即Human.prototype
,可Human.prototype
本身也没有jump
方法,而它又是一个由Animal
构造的对象,所以 - 查找
Animal.prototype
,在其中找到了jump
方法,执行之
仅从原型继承法
和“原型链继承法”相比,本方法的优点在于,提高了运行时的效率,没有创建新对象出来。
var Human = function(name){
this.name = name;
};
Human.prototype = Animal.prototype;
var man = new Human('HaoyCn');
man.jump();
这时候的查找过程是:
-
man
自身没有jump
方法 - 查找
man.constructor.prototype
,即Human.prototype
,Human.prototype
是对Animal.prototype
的引用,在其中找到了jump
方法,执行之
减少了一步。然而代价则是:对 Human.prototype
的修改都会影响到 Animal.prototype
,因为前者是对后者的引用。
一个致命缺点就是,无法修正子类构造的对象的 constructor
。
测试一下:
man.constructor === Animal;//true
我们来回顾一下 new
的过程:
var newProcess = function(){
var ret;
// 构造一个新对象
var obj = {};
// 构造函数
var Constructor = Array.prototype.shift.call(arguments);
// 记录原型
obj.__proto__ = Constructor.prototype;
// 运用构造函数给新对象设置属性
ret = Constructor.apply(obj,arguments);
// 始终返回一个对象
return 'object' === typeof ret ? ret : obj;
};
我们以此来回顾下“仅从原型继承法”是如何创建出 man
的。
// var man = newProcess(Human,'HaoyCn');
// 还原如下
var ret;
var man = {};
// var Constructor = Array.prototype.shift.call(arguments);
// 即是
//var Constructor = Human;
man.__proto__ = Human.prototype;
// ret = Human.apply(obj,arguments);
// `Human`构造器执行的是
man.name = 'HaoyCn';
// `Human`构造器返回的是 undefined,即 ret = undefined;
// 所以最后`newProcess`返回`man`
因此,就不难理解了:
man.constructor ===
man.__proto__.constructor ===
Human.prototype.constructor ===
Animal.prototype.constructor ===
Animal
临时构造器继承法
“仅从原型继承法”的问题暴露出来了:Animal.prototype
会因对 Human.prototype
的修改而改变。如果被改变了,由 Animal
构造出来的对象也会发生改变。我们来举个例子:
var monkey = new Animal('monkey');
var woman = new Human('woman');
monkey.jump();// jumped
woman.jump();// jumped
// 下面的修改会影响`Animal.prototype`
Human.prototype.jump = function(){
console.log('I refuse');
};
// 原本构造好的对象也会被影响
monkey.jump();// I refuse
woman.jump();// I refuse
那么,我们如何规避这个问题呢?
“临时构造器继承法”使用一个中介函数,如下
var F = function(){};
F.prototype = Animal.prototype;
var Human = function(name){
this.name = name;
};
Human.prototype = new F;
Human.prototype.constructor = Human;
Human.prototype.sing = function(){
console.log('Mayday');
};
var man = new Human('HaoyCn');
man.jump();
man.sing();
我们对 Human.prototype
的任何改变都变成了对一个由中介构造器创建的对象的属性的修改。jump
查找过程是:
-
man
自身没有jump
方法 - 查找
man.constructor.prototype
,即Human.prototype
,可Human.prototype
本身也没有jump
方法,而它又是一个由F
构造的对象,所以 - 查找
F.prototype
,即Animal.prototype
,在其中找到了jump
方法,执行之
那这个方法同最开始的“仅从原型继承法”相比,又有什么进步呢?
先看“仅从原型继承法”中的操作:
Human.prototype = new Animal;
// 这将造成:
// Human.prototype.name = undefined;// 没有给`Animal`传入参数之故
也就是说,Human.prototype
会多出不必要的属性来,而中介器则避免了这种不必要的属性。
构造器借用法
以上继承法共通的一个缺点在于,Human
构造器构造的对象虽然可以共用 Animal.prototype
,但对于 name
属性而言,Human
构造器只能自己再写一遍构造 name
属性,为什么不把初始化属性的方法也共(借)用呢?
构造器借用法应运而生。现在我们把 name
属性的创建还是交给 Animal
,然后再为 Human
增加 country
属性。我们在“临时构造器法”基础上进一步完善之。
var F = function(){};
F.prototype = Animal.prototype;
var Human = function(){
Animal.apply(this,arguments);
this.country = arguments[1];
}
Human.prototype = new F;
Human.prototype.constructor = Human;
var man = new Human('HaoyCn','China');
console.log(man.country);// China
这样,我们就轻轻松松地完成了偷懒。这让我想到了PHP中覆盖构造函数的办法,如下
// PHP
class Human{
public $name;
public $country;
function __construct($name,$country){
parent::__construct($name);
$this->country = $country;
}
}
关于继承的话题到此结束。接下来谈拷贝。
原型属性拷贝法
利用了原型机制。在高级浏览器中,有 Object.create
方法来完成对对象的拷贝,我们现在就简单地还原之:
Object.create = Object.create || function(obj){
var F = function(){};
F.prototype = obj;
return new F;
}
可以看到,这是一种浅拷贝。如果我们对被拷贝对象进行修改,也会影响到新对象。举例如下:
var man = {
name: 'HaoyCn',
jump: function(){
console.log('jumped');
}
};
var monkey = Object.create(man);
monkey.jump();// jumped
man.jump = function(){
console.log('I refuse');
};
monkey.jump();// I refuse
浅拷贝与深拷贝
问题摆在面前,如何深拷贝?
我们拷贝对象除了“原型属性拷贝法”之外,还可以通过遍历来完成。如浅拷贝遍历:
var man = {
name: 'HaoyCn',
jump: function(){
console.log('jumped');
}
};
var monkey = {};
for(var i in man){
monkey[i] = man[i];
}
monkey.jump();// jumped
而深拷贝要做的就是,如果属性还是个对象,就递归拷贝。
function deepCopy(origin,copy){
copy = copy || {};
for(var i in origin){
if('object' === typeof origin[i]){
// 判断是否为数组还有更好办法,这里从简
copy[i] = ('Array' === origin[i].constructor) ? [] : {};
deepCopy(origin[i],copy[i]);
}else{
copy[i] = origin[i];
}
}
}
以上是深拷贝的一个扼要原理代码。更复杂的检验过程,可以参考 jQuery.extend
。但是,这样的拷贝(包括jQuery.extend
的深拷贝)只能完成对纯粹对象的深拷贝,而函数、RegExp、Date等都无法深拷贝。
以上。关于对非纯粹对象的深拷贝的方法我还在探索中,比如调用 toString()
后再构造对象的方式,但都不够完善,如果您在此方面有心得,敬请指教!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。