1

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 方法,查找过程是:

  1. man 自身没有jump方法
  2. 查找 man.constructor.prototype,即Human.prototype,可Human.prototype本身也没有 jump 方法,而它又是一个由 Animal 构造的对象,所以
  3. 查找 Animal.prototype,在其中找到了 jump 方法,执行之

仅从原型继承法

和“原型链继承法”相比,本方法的优点在于,提高了运行时的效率,没有创建新对象出来。

var Human = function(name){
    this.name = name;
};
Human.prototype = Animal.prototype;
var man = new Human('HaoyCn');
man.jump();

这时候的查找过程是:

  1. man 自身没有jump方法
  2. 查找 man.constructor.prototype,即Human.prototypeHuman.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 查找过程是:

  1. man 自身没有jump方法
  2. 查找 man.constructor.prototype,即Human.prototype,可Human.prototype本身也没有 jump 方法,而它又是一个由 F 构造的对象,所以
  3. 查找 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() 后再构造对象的方式,但都不够完善,如果您在此方面有心得,敬请指教!


残阳映枫红
6.1k 声望638 粉丝