《JavaScript 模式》读书笔记(6)— 代码复用模式3

我们之前聊了聊基本的继承的概念,也聊了很多在JavaScript中模拟类的方法。这篇文章,我们主要来学习一下现代继承的一些方法。九、原型继承下面我们开始讨论一种称之为原型继承(prototype

大家好,又见面了,我是你们的朋友全栈君。

  我们之前聊了聊基本的继承的概念,也聊了很多在JavaScript中模拟类的方法。这篇文章,我们主要来学习一下现代继承的一些方法。

 

九、原型继承

  下面我们开始讨论一种称之为原型继承(prototype inheritance)的“现代”无类继承模式。在本模式中并不涉及类,这里的对象都是继承自其他对象。以这种方式考虑:有一个想要复用的对象,并且想创建的第二个对象需要从第一个对象中获取其功能。

  下面的代码展示了该如何开始着手实现这种模式:

// 要继承的对象
var parent = {
    name:"Papa"
};
// 新对象
var child = object(parent);

// 测试
alert(child.name) //"Papa"

  在前面的代码片段中,存在一个以对象字面量(object literal)创建的名为parent的现有对象,并且要创建另外一个与parent具有相同属性和方法的名为child的对象。child对象是由一个名为object()的函数所创建。JavaScript中并不存在该函数(不要与构造函数object()弄混淆),为此,让我们看看该如何定义该函数。

  与类似继承模式的圣杯版本相似,首先,可以使用空的临时构造函数F()。然后,将F()的原型属性设置为父对象。最后,返回一个临时构造函数的新实例:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

  下图展示了在使用原型继承模式时的原型链。图中的child最初是一个空对象,他没有自身的属性,但是同时他又通过受益于__proto__链接而具有其父对象的全部功能。

<span role="heading" aria-level="2">《JavaScript 模式》读书笔记(6)— 代码复用模式3

 

讨论

  在原型继承模式中,并不需要使用字面量符合(literal notation)来创建父对象(尽管这可能是一种比较常见的方式)。如下代码所示,可以使用构造函数创建父对象,请注意,如果这样做的话,“自身”属性和构造函数的原型的属性都将被继承:

// 父构造函数
function Person() {
    // an "own" property
    this.name = "Adam"
}

// 添加到原型的属性
Person.prototype.getName = function () {
    return this.name;
};

// 创建一个新的Person类对象
var papa = new Person();

// 继承
var kid = object(papa);

// 测试自身的属性
// 和继承的原型属性
console.log(kid.getName());

  在本模式的另外一个变化中,可以选择仅继承现有构造函数的原型对象。请记住,对象继承自对象,而不论父对象是如何创建的。下面使用了前面的例子演示该变化,仅需稍加修改代码即可:

// 父构造函数
function Person() {
    // an "own" property
    this.name = "Adam"
}

// 添加到原型的属性
Person.prototype.getName = function () {
    return this.name;
};

// 继承
var kid = object(Person.prototype);

console.log(typeof kid.getName);
console.log(typeof kid.name);

 

增加到ECMAScript5中

  在ECMAScript5中,原型继承模式已经正式成为该语言的一部分。这种模式是通过方法Object.create()来实现的。也就是说,不需要推出与object()类似的函数,它已经内嵌在语言中:

var child = Object.create(parent);

  Object.create()接受一个额外的参数,即一个对象。这个额外对象的属性将会被添加到新对象中,以此作为新对象自身的属性,然后Object.create()返回该新对象。这提供了很大的方便,使您可以仅采用一个方法调用即可实现继承并在此基础上构建子对象。比如:

var child = Object.create(parent,{
   age : { value : 2}      
});
child.hasOwnProperty("age");

  可能还会发现一些JavaScript库中已经实现了原型继承模式。例如,在YUI3中是Y.Object()方法。

 

十、通过复制属性实现继承

  让我们看另一种继承模式,即通过复制属性实现继承。在这种模式中,对象将从另一个对象中获取功能,其方法是仅需将其复制即可。下面是一个示例函数extend()实现复制继承的例子:

function extend(parent, child){
    var i;
    child = child || {};
    for(i in parent) {
        if(parent.hasOwnProperty(i)) {
            child[i] = parent[i]
        }
    }
    return child
}

  上面点的代码是一个简单的实现,它仅遍历父对象的成员并将其复制出来。在本示例实现中,child对象是可选的。如果不传递需要扩展的已有对象,那么他会创建并返回一个全新的对象。

var dad = {name : "Adam"};
var kid = extend(dad);
console.log(kid.name)

  上面给出的是一种所谓浅复制的对象。另一方面,深度复制意味着属性检查,如果即将复制的属性是一个对象或者一个数组,这样的话,它将会递归遍历该属性并且还会将属性中的元素复制出来。在使用前复制(由于JavaScript中的对象是通过引用而传递的)的时候,如果改变了子对象的属性,并且该属性恰好是一个对象,那么这种操作表示也正在修改父对象。其实,这也是更可取的方法,但是当处理其他对象和数组时,这种前复制也可能导致意外发生。考虑下列情况:

var dad = {
    counts:[1,2,3],
    reads:{paper:true}
}

var kid = extend(dad);
kid.counts.push(4);
console.log(dad.counts.toString());
console.log(dad.reads === kid.reads)

  现在让我们修改extend()函数以实现深度复制。所有需要做的事情就是检查某个属性的类型是否为对象,如果是这样的话,需要递归复制出该对象的属性。另外,还需要检查该对象是否为一个真实对象或者一个数组,我们可以使用第三章中讨论的方法检查其数组性质。因此,深度复制版本的extend()函数看起来是这样的:

function extendDeep(parent,child) {
    var i,
        toStr = Object.prototype.toString,
        astr = "[object Array]";
    
    child = child || {};

    for(i in parent) {
        if(parent.hasOwnProperty(i)) {
            if(typeof parent[i] === 'object'){
                child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
                extendDeep(parent[i],child[i])
            } else {
                child[i] = parent[i]
            }
        }
    }
    return child;
}

  现在开始测试这种新的实现方式,由于它能够为我们创建对象的真实副本,因此子对象的修改并不会影响其父对象。

var kid = extendDeep(dad);
kid.counts.push(4);
console.log(kid.counts.toString());
console.log(dad.counts.toString());

console.log(dad.reads === kid.reads);
kid.reads.paper = false;

kid.reads.web = true;
console.log(dad.reads.paper)

  这种属性复制模式比较简单且得到了广泛的应用。值得注意的是,本模式中根本没有涉及到任何原型,本模式仅与对象以及它们自身的属性相关。

 

混入

  可以针对这种通过属性复制实现继承的思想作进一步的扩展,现在让我们思考一种“mix-in”混入模式。mix-in模式并不是复制一个完整的对象,而是从多个对象中复制出任意的成员并将这些成员组合成一个新的对象。

  mix-in实现比较简单,只需遍历每个参数,并且复制出传递给该函数的每个对象中的每个属性。

function mix() {
    var arg,prop,child = {};
    for(arg = 0;arg < arguments.length; arg += 1) {
        for(prop in arguments[arg]) {
            if(arguments[arg].hasOwnProperty(prop)) {
                child[prop] = arguments[arg][prop];
            }
        }
    }
    return child;
}

  现在,您有一个通用的mix-in函数,可以向他传递任意数量的对象,其结果将获得一个具有所有源对象属性的新对象。下面是一个使用示例:

var cake = mix(
    {eggs:2,large:true},
    {butter:2,salted:true},
    {flour:'3 cups'},
    {sugar:'sure!'}
)

console.dir(cake)

  注意:如果已经学习过那些正式包含mix-in概念的语言,并且习惯于mix-in的概念,那么可能希望修改一个或多个父对象时可以影响其子对象,但是在本节给定的实现中并不是这样的。子啊这里我们仅简单循环、复制自身的属性,以及断开与父对象之间的链接。

 

十一、借用方法

  有时候,可能恰好仅需要现有对象其中的一个或两个方法。在想要重用这些方法的同时,但是又不希望与源对象形成父-子继承关系。也就是说,指向使用所需要的方法,而不希望继承那些永远都不会用到的其他方法。在这种情况下,可以通过使用借用方法模式来实现,而这时受益于call()和apply()函数方法。您已经在本书中见到过这种模式,比如,甚至于在本章中extendDeep()函数的实现内部都见到过。

  如您所知,JavaScript中的函数也是对象,并且它们自身也附带着一些有趣的方法,比如apply()和call()方法。这两者之间的唯一区别在于其中一个可以接受传递给将被调用方法的参数数组,而另一个仅逐个接受参数。可以使用这些方法以借用现有对象的功能。

//call()例子
notmyobj.doStuff.call(myobj,param1,p2,p3);
// apply()例子
notmyobj.doStuff.apply(myobj,[param1,p2,p3]);

  在以上代码中,存在一个名为MyObj的对象,并且还知道其他名为notmyobj的对象中有一个名为doStuff()的有用方法。您无需经历继承所带来的麻烦,也无需继承myobj对象永远都不会用到的一些方法,可以仅临时性的借用方法doStuff()即可。

  可以传递对象、任意参数以及借用方法,并将它们绑定到您的对象中以作为this本身的成员。从根本上说,您的对象将在一小段时间内伪装成其他对象,从而借用其所需的方法。这就像得到了继承的好处,但是却无需支付遗产税(这里指其他您不需要的属性或方法)。

 

例子:借用数组方法

  本模式的一个常见实现方法是借用数组方法。

  数组具有一些有用的放啊,而形如arguments的类似数组的对象并不具有这些方法。因此,arguments可以借用数组的方法,比如slice()方法:

function f() {
    var args = [].slice.call(arguments,1,3);
    return args;
}
console.log(f(1,2,3,4,5,6));

  在这个例子中,创建一个空数组的原因只是为了使用数组的方法。此外,能够实现同样功能但是语句稍微长一点的方式是直接从Array的原型中借用方法,即使用Array.prototype.slice.call()方法。这种方式需要输入更长一点的字符,但是却可以节省创建一个空数组的工作。

 

借用和绑定

  考虑到借用方法不是通过调用call()/apply()就是通过简单的赋值,在借用方法的内部,this所指向的对象是基于调用表达式而确定的。但是有时候,最好能够“锁定”this的值,或者将其绑定到特定对象并预先确定该对象。

  让我们看下面这个例子,其中存在一个名为one的对象,且具有say()方法:

var one = {
    name:"object",
    say: function(greet) {
        return greet + ', ' + this.name; 
    }
};

// 测试
console.log(one.say('hi')); //结果为“hi,object”

  现在,另一个对象two中并没有say()方法,但是可以从对象one中借用该方法,如下所示:

var two = {
    name:"another object"
};

console.log(one.say.apply(two,["hello"]));

  在上述例子中,借用的say()方法内部的this指向了two对象,因而this.name的值为“another object”。但是在什么样的场景中,应该将函数指针赋值给一个全局变量,或者将该函数作为回调函数来传递?在客户端编程中有许多事件和回调函数,因此确实发生了很多这样混淆的事情。

// 给变量赋值
// this将指向全局变量
var say = one.say;
console.log(say('hoho'));
// 作为回调函数传递
var yetanother = {
    name:"Yet another object",
    method:function(callback) {
        return callback("Hola");
    }
};
console.log(yetanother.method(one.say));

  在以上两种情况下,say()方法内部的this指向了全局对象,并且整个代码段都无法按照预期正常运行。为了修复(也就是说,绑定)对象与方法之间的关系,我们可以使用如下的一个简单函数:

function bind(o,m) {
    return function () {
        return m.apply(o,[].slice.call(arguments))
    }
}

  这个bind()函数接受了一个对象o和一个方法m,并且将两者绑定起来,然后返回另一个函数。其中,返回的函数可以通过闭包来访问o和m。因此,即时在bind()返回后,内部函数热盎然可以访问o和m,并且总是指向原始对象和方法。下面,让我们使用bind()创建一个新的函数:

var twosay = bind(two,one.say);
console.log(twosay('yo'))

  正如您上面所看到的,即时twosay()以全局函数方式而创建,但是say()方法内部的this并没有指向全局对象,实际上它指向了传递给bind()的对象two。无论您如何调用twosay(),该方法永远是绑定到对象two上。

  奢侈的拥有绑定所需要付出的代价就是额外的必报的开销。

 

Function.prototype.bind()

  ECMAScript5中将bind()方法添加到了Function.prototype中,使得bind()就像apply()和call()一样简单易用。因此,可以执行如下表达式:

var newFunc = obj.someFunc.bind(myobj,1,2,3);

  上述表达式的含义是将someFunc()和myobj绑定在一起,并且预填充someFunc()期望的前三个参数。这也是第四章中所讨论的部分函数应用的一个例子。

  当程序在ES5之前的环境运行时,让我们看看应该如何实现Function.prototype.bind():

if(typeof Function.prototype.bind === 'undefined') {
    Function.prototype.bind = function(thisArg) {
        var fn = this,
            slice = Array.prototype.slice,
            args = slice.call(arguments,1);
        return function () {
            return fn.apply(thisArg,args.concat(slice.call(arguments)));
        }
    }
}

  这个实现看起来可能有点熟悉,它使用了部分应用并拼接了参数列表,即那些传递给bind()的参数(除了第一个以外),以及那些传递给由bind()所返回的新函数的参数,其中该新函数将在以后被调用。下面是一个使用示例:

var twosay2 = one.say.bind(two);
console.log(twosay2("Bonjour"));

  在前面的例子中,除了提供了将被绑定的对象以外,并没有向bind()传递任何参数。在下面的例子,让我们传递一个参数以实现部分应用:

var twosay3 = one.say.bind(two,"Enchante");
console.log(twosay3())

 


小结

  当在JavaScript中涉及到继承时,有很多可供选择的方法。这些方法对于学习和理解多种不同的模式大有裨益,因为它们有助于提高您对语言的掌握程度。在本章中,您了解了几种类式继承模式以及集中现代继承模式,从而可以解决继承相关的问题。

  然而,在开发过程中经常面临的继承可能并不是一个问题。其中一部分的原因在于,事实上使用的JavaScript库可能以这样或那样的方式解决了该问题,而另一个方面的原因在于很少需要在JavaScript中建立长而且复杂的继承链。在静态强类型的语言中,继承可能是唯一复用代码的方法。在JavaScript中,经常有更简洁且优美的方法,其中包括借用方法、绑定、复制属性以及从多个对象中混入属性等多种方法。

  最后,请记住,代码重用才是最终目的,而继承只是实现这一目标的方法之一。

 

  到这里,这一篇就结束了,后面,我们开始学习设计模式!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/124382.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 算法的时间与空间复杂度(一看就懂)

    算法的时间与空间复杂度(一看就懂)算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。那么我们应该如何去衡量不同算法之间的优劣呢?主要还是从算法所占用的「时间」和「空间」两个维度去考量。 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。 空间维度:是指执行当前算…

  • centos7网络设置ipv4_centos7连接wifi详细步骤

    centos7网络设置ipv4_centos7连接wifi详细步骤系统版本centos7.71、ip配置(配置后局域网内可互ping)同网段内设置,不用网关即可通信1、临时设置#设置接口ens33的地址为192.168.59.27ipaddradd192.168.56.27/24devens33#查看接口ens33地址ipaddrshowens332、永久设置进入目录,/etc/sysconfig/network-scripts,修改文件ifcfg-ens33,修改或添加下列几项BOOTPROTO=”none”#

  • OpenCV 如何保存图片「建议收藏」

    OpenCV 如何保存图片「建议收藏」里主要说明两种图片格式cv::Mat以及IplImage如果图片是以Mat类型的格式表示的话,那么保存图片则用imwrite()函数举例如下:constchar*path;path=”E:\\Data\\right\\right.bmp”imwrite(path,riFrame);//riFrame为当前帧如果图片是以IplImage类型的格式表示的话,

  • iphone android换机助手下载,腾讯换机助手手机最新版 目前最好用的安卓/苹果一键换机工具…

    iphone android换机助手下载,腾讯换机助手手机最新版 目前最好用的安卓/苹果一键换机工具…换机助手软件介绍换机助手是腾讯开发的一款跨平台手机资料迁移工具,它可以在安卓与安卓,苹果与苹果,安卓与苹果手机之间进行数据迁移,安卓手机可以直接在下面下载APP,而苹果手机则需要在自带的APPSTORE中搜索“换机助手”下载安装,这也是非常实用的一款程序了!换机助手软件功能:该软件可以通过调用手机创建热点,进行两部手机匹配互联,零消耗网络流量传输手机资料。目前已支持安卓Android与苹果iOS…

  • hdu 1520Anniversary party(简单树形dp)

    hdu 1520Anniversary party(简单树形dp)

  • c/c++常见面试题

    1.C中static有什么作用(1)隐藏。当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,故使用static在不同的文件中定义同名函数和同名变量,而不必担心命

    2021年12月27日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号