资讯专栏INFORMATION COLUMN

JavaScript > 继承与拷贝

tylin / 1667人阅读

摘要:继承与拷贝本文讨论中如何实现继承关系,以及如何拷贝对象。谈完继承方法后,再谈论对象的拷贝。我们在临时构造器法基础上进一步完善之。这让我想到了中覆盖构造函数的办法,如下关于继承的话题到此结束。

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.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 查找过程是:

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() 后再构造对象的方式,但都不够完善,如果您在此方面有心得,敬请指教!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85857.html

相关文章

  • <<编写可维护的javascript&gt;&gt; 笔记8(避免'空比较&#

    摘要:中常常会看到这种代码变量与的比较这种用法很有问题用来判断变量是否被赋予了一个合理的值比如不好的写法执行一些逻辑这段代码中方法显然是希望是一个数组因为我们看到的拥有和这段代码的意图非常明显如果参数不是一个数组则停止接下来的操作这种写法的问题在 js中, 常常会看到这种代码: 变量与null的比较(这种用法很有问题), 用来判断变量是否被赋予了一个合理的值. 比如: const Contr...

    young.li 评论0 收藏0
  • JavaScript设计模式开发实践》 —— <阅读小札·一&gt;

    摘要:阅读小札一阅读前自大学课上,就开始接触设计模式,但对设计模式却鲜有研究与实践。第二部分是核心部分,由浅到深讲解个设计模式。设计模式遵循的原则所有设计模式罪训的一条原则就是找出程序中变化的地方,并将变化封装起来。 阅读小札 · 阅读前 自大学Java课上,就开始接触设计模式,但对设计模式却鲜有研究与实践。最近向公司反映和游说技术提升,得以获得公司提供购书机会,借此认真学习前端学习之路的...

    Yangder 评论0 收藏0
  • 《你不知道的javascript》笔记_对象&原型

    摘要:上一篇你不知道的笔记写在前面这是年第一篇博客,回顾去年年初列的学习清单,发现仅有部分完成了。当然,这并不影响年是向上的一年在新的城市稳定连续坚持健身三个月早睡早起游戏时间大大缩减,学会生活。 上一篇:《你不知道的javascript》笔记_this 写在前面 这是2019年第一篇博客,回顾去年年初列的学习清单,发现仅有部分完成了。当然,这并不影响2018年是向上的一年:在新的城市稳定、...

    seasonley 评论0 收藏0
  • Javascript面向对象编程(三):非构造函数的继承

    摘要:原文链接一什么是非构造函数的继承比如,现在有一个对象,叫做中国人。通过函数,继承了。中国北京上海香港厦门北京上海香港厦门北京上海香港这时,父对象就不会受到影响了。目前,库使用的就是这种继承方法。 原文链接 一、什么是非构造函数的继承? 比如,现在有一个对象,叫做中国人。 var Chinese = { nation: 中国 } 还有一个对象,叫做医生。 var Doctor = {...

    jaysun 评论0 收藏0
  • JavaScript =&gt; TypeScript 类入门

    摘要:静态属性静态方法目前支持静态方法表示,类属性及静态属性目前作为提案还未正式成为标准。在中,抽象类不能用来实例化对象,主要做为其它派生类的基类使用。不同于接口,抽象类可以包含成员的实现细节。中也是这样规定的抽象类不允许直接被实例化。 尝试重写 在此之前,通过《JavaScript => TypeScript 入门》已经掌握了类型声明的写法。原以为凭着那一条无往不利的规则,就可以开开心心的...

    geekidentity 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<