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

  我们之前聊了聊基本的继续的观点,也聊了许多在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__链接而具有其父工具的所有功效。

《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中,经常有更简练且优美的方式,其中包罗借用方式、绑定、复制属性以及从多个工具中混入属性等多种方式。

  最后,请记着,代码重用才是最终目的,而继续只是实现这一目的的方式之一。

 

  到这里,这一篇就竣事了,后面,我们最先学习设计模式!

原创文章,作者:admin,如若转载,请注明出处:https://www.2lxm.com/archives/7100.html