资讯专栏INFORMATION COLUMN

YUI中对象合并的方法与原理

bitkylin / 1113人阅读

摘要:更细致的为了应对更为复杂的对象合并要求,提供了方法。注意到,当两者都为时,得到的正是在配置对象合并中的一个理想结果。它们都类似于对象合并,而且有趣的是,其源码中都调用了方法。其深合并的实现方法依然是递归。

起因:配置选项

“配置”(Config | Options | Settings)对大家来说一定是非常熟悉的词。就以一般玩的游戏为例,很多游戏的主界面,搭配的菜单会是"start","load","config","exit"这样的搭配。在"config"中,我们可以调整游戏参数,比如音量、控制按键等,以更符合自己的游戏习惯。不过,也有游戏并没有"config",这些游戏其实就是只有一个不允许修改的“默认”(Defaults)配置,而即使如此,这些游戏至少也会给你一份指南,告诉你应该如何按照预设进行游戏。

应用的配置这一环节的设计,其实就是要求要将配置数据从代码中分离出来。也就是说,那些可以被修改,或者将来可能会被修改的内容(这些内容也是代码),应该和指令类的代码区别开来,多带带放在一个地方。这样,源码修改可以减少很多意外的错误。

javascript编写的封装的功能模块,比如常见的jQuery插件,一般都会这样分离出配置数据。下面是[slidesjs][]的使用范例:

$("#slides").slidesjs({
    width: 940,
    height: 528
});

这里的{width: 940, height: 528}即是配置数据。不过,只是这些数据是不能独立支撑起完整的功能的。在封装的代码之内的,还隐藏着完整的初始配置数据,这就是默认配置。而像上面这样在实际使用时传入的配置数据,一般叫做用户配置。它们之间的关系是,如果同时包含对某一选项的配置值,用户配置会覆盖掉默认配置。

比如在Sublime Text中,就是这种设计(Default和User):

可以想到,实际功能运行时,使用的配置数据既不是默认配置,也不是用户配置,而是这两个配置的结合。在javascript中,配置数据都是对象(Object)的形式,因此,我们需要做对象合并

在YUI的核心包(yui-core)中,就提供了对象合并有关的方法。

YUI中的对象合并方法 浅合并的Y.merge()

YUI提供了合并一个或更多对象到一个新的合并对象上的方法Y.merge(),在这里先使用它。假如有一个封装的功能模块(简单的游戏?),然后内部通过Y.merge()进行配置对象的合并:

var myModule = function(userConfig){
    var defaults = {
        volume: 0,
        quality: "normal",
        control: {
            left: "left",
            right: "right",
            attack: "space"
        }  
    };

    var config = Y.merge(defaults, userConfig);

    // use the merged config to run this module...
};

对应上面已有的默认配置的形式,给出如下的用户配置:

var userConfig = {
    quality: "high",
    control: {
        left: "a",
        right: "d",
        extra: "z"
    }
};

这里通过Y.merge()合并得到的config,输出结果是:

{
    volume: 0,
    quality: "high",
    control: {
        left: "a",
        right: "d",
        extra: "z"
    }
}

可以看到,volumequality的属性合并都达到了预想的要求。但是,属性control却像是忽略了默认值的部分,并不符合预想结果。这是因为,属性control的值并不是基本数据类型,而是引用数据类型(例如对象、数组或函数),因此这里的配置对象是包含多层次的复杂配置对象。Y.merge()并没有处理这样的情况,它的源码如下:

Y.merge = function () {
    var i      = 0,
        len    = arguments.length,
        result = {},
        key,
        obj;
 
    for (; i < len; ++i) {
        obj = arguments[i];
 
        for (key in obj) {
            if (hasOwn.call(obj, key)) {
                result[key] = obj[key];
            }
        }
    }
 
    return result;
};

其中hasOwnObject.prototype.hasOwnProperty。从上面的源码可以看出,Y.merge()只对直接属性(层级数为1)进行赋值,并没有分析属性的值类别。因此,在前面的对象合并中,configcontrol属性,实际上就和userConfigcontrol是同一个引用,如果在config上修改control对象,则也会改变userConfigcontrol对象。

不过,Y.merge()仍然是很有价值的对象合并方法。如果是单层次的配置对象,它足以实现预想的要求,这是它很有用的一个模式。此外,请注意这个方法创建了一个空对象result用作最终返回,而不是直接对参数进行操作,所以,Y.merge()方法不会影响作为参数的任一对象。

更细致的Y.mix()

为了应对更为复杂的对象合并要求,YUI提供了方法Y.mix()Y.mix()不能像Y.merge()那样同时合并2个以上的对象,它的合并对象数目限定为2个,对应前2个参数,此外的参数都用作合并模式、方法的设置。Y.mix()搭配其多种模式和配置的控制,可以实现相当精细的两个对象的合并(方法叫做"mix",所以也称为混合吧)。

Y.mix()的源码较多,不直接贴在本文中。YUI官方的注释写得很详细,不过在这里我也说一些我自己的理解。它的源码形式是:

Y.mix = function(receiver, supplier, overwrite, whitelist, mode, merge) {
    // Y.mix() detail
);

Y.mix()有很多参数。前2个参数receiversupplier如字面意思,分别对应合并操作的接收者和提供者。其余4个参数的意义分别是:

overwrite,默认为false。如果为true则会开启属性覆盖。也就是说,默认情况下,提供者的属性是不会覆盖接受者的同名属性的。

whitelist,白名单。可以指定一个数组,只有当提供者的属性名包含于白名单数组内时,属性才会被拷贝到接受者。

mode,混合模式。可选值0、1、2、3、4分别对应5个模式。默认值为0,是object to object的合并方式。

merge,默认为false。如果为true,对象上的值为引用数据类型的属性(也就是,当这是一个多层次的复杂对象时),每一个层级上的对象都会依次被合并,而不是被忽略(overwritefalse)或被直接覆盖(overwritetrue)。

上面的参数中,overwritemerge的意义比较重要,其他的则一般使用null或默认值。为了理解overwritemerge,直接进行不同搭配的合并测试(配置变量和前文一样)。测试代码和结果如下:

// overwrite = false , merge = false | Y.mix()的最简单形式,默认值
var config = Y.mix(defaults, userConfig);
// 结果输出:
// {volume => 0, quality => normal, control => {left => left, right => right, attack => space}}

// overwrite = true , merge = false
var config = Y.mix(defaults, userConfig, true, null, 0, false);
// 结果输出:
// {volume => 0, quality => high, control => {left => a, right => d, extra => e}}

// overwrite = false , merge = true
var config = Y.mix(defaults, userConfig, false, null, 0, true);
// 结果输出:
// {volume => 0, quality => normal, control => {left => left, right => right, attack => space, extra => e}}

// overwrite = true , merge = true
var config = Y.mix(defaults, userConfig, true, null, 0, true);
// 结果输出:
// {volume => 0, quality => high, control => {left => a, right => d, attack => space, extra => e}}

可以看出,4个结果各不相同。从这些结果的具体情况,可以分析体会overwritemerge的意义。注意到,当两者都为true时,得到的正是在配置对象合并中的一个理想结果。而当overwrite = true, merge = false时,得到的结果和Y.merge()相同。

阅读Y.mix()的源码,可以了解到,merge决定是否进行深合并。深合并的实现原理是递归,也就是对那些需要多层次合并的属性值(对象、数组),再次调用Y.mix()overwrite决定是否进行覆盖,这就类似于电脑里做复制操作时,会提示同名文件是否覆盖的选项。

对于用作配置数据的对象而言,预期的最佳的合并方式就是允许覆盖,而且进行深合并。因此,Y.mix(defaults, userConfig, true, null, 0, true)即可以得到最适当的合并后的配置数据。

需要注意的是,Y.mix()直接对接收者进行操作,并返回接收者。所以,和Y.merge()不同,它一定会影响到作为第一个参数的对象。

如果可以确定配置数据是单层次的简单配置对象,那么使用Y.merge()要更简单方便。

更多的工具

YUI除了核心包内的上面两个方法外,还在oop模块中提供了一些处理对象的附加工具。其中包括了Y.extend()(单词意义:扩展),Y.augment()(单词意义:增强),Y.aggregate()(单词意义:聚合)。它们都类似于对象合并,而且有趣的是,其源码中都调用了Y.mix()方法。

这几个方法和前面的有哪些不同呢?本文限于篇幅,只简单说一些。Y.aggregate()是开启深合并的Y.mix()的一个方法糖,Y.extend()则要求参数是构造函数,它操作的是传入对象的prototype,并通过prototype建立继承关系(原型链),Y.augment()是从提供者拷贝方法到接收者,但它巧妙地隔离了所有拷贝的方法并延迟调用提供者的构造函数,直到你第一次调用被拷贝上去的新方法。

有些关系的jQuery.extend()

因为比较近似,所以在这里也提一下jQuery。

如果你写过jQuery插件,你肯定知道jQuery.extend()这个方法。事实证明,不同的javascript库,在命名理念上也是各有想法,在此请注意,jQuery.extend()Y.extend()是完全不同的两个方法。阅读jQuery.extend()的源码可知,它仍然是在做传统的对象合并,而且同时允许声明一个可选的逻辑参数deep,来指定是否进行深合并。其深合并的实现方法依然是递归。

此外,类似于Y.mix()jQuery.extend()也会影响到作为接收者的对象。官方文档对此还给了明确说明,如果希望不影响原来的对象,就使用var object = $.extend({}, object1, object2);这样以空对象作为接收者的写法。

结语

之所以想到写这个话题,大概就是因为YUI里有关对象合并很有几个不同的方法,我已经相当被它们弄晕了。所以,果断读源码,才能理清这其中的区别,也算是体会一下javascript库设立这些不同的方法的本因吧。

默认配置 + 用户配置 → 实际配置,结果来看也是一个不简单的转化计算式。

(重新编辑自我的博客,原文地址:http://acgtofe.com/posts/2014/07/object-toolkit-in-yui)

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

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

相关文章

  • JavaScript模块化发展

    摘要:所有依赖这个模块的语句,都定义在一个回调函数中,等到所有依赖加载完成之后前置依赖,这个回调函数才会运行。如果将前面的代码改写成形式,就是下面这样定义了一个文件,该文件依赖模块,当模块加载完毕之后执行回调函数,这里并没有暴露任何变量。 模块化是我们日常开发都要用到的基本技能,使用简单且方便,但是很少人能说出来但是的原因及发展过程。现在通过对比不同时期的js的发展,将JavaScript模...

    mengbo 评论0 收藏0
  • JavaScript模块化开发演进历程

    摘要:参考精读模块化发展模块化七日谈前端模块化开发那点历史本文先发布于我的个人博客模块化开发的演进历程,后续如有更新,可以查看原文。 Brendan Eich用了10天就创造了JavaScript,因为当时的需求定位,导致了在设计之初,在语言层就不包含很多高级语言的特性,其中就包括模块这个特性,但是经过了这么多年的发展,如今对JavaScript的需求已经远远超出了Brendan Eich的...

    anonymoussf 评论0 收藏0
  • <<编写可维护javascript>> 笔记6(避免使用全局变量)

    摘要:执行环境在很多方面都有其独特之处全局变量和函数便是其中之一事实上的初始执行环境是由多种多样的全局变量所定义的这写全局变量在脚本环境创建之初就已经存在了我们说这些都是挂载在全局对象上的全局对象是一个神秘的对象它表示了脚本最外层上下文在浏览器中 JavaScript执行环境在很多方面都有其独特之处. 全局变量和函数便是其中之一. 事实上, js的初始执行环境是由多种多样的全局变量所定义的,...

    MoAir 评论0 收藏0
  • 高性能JavaScript(文档)

    摘要:最近在全力整理高性能的文档,并重新学习一遍,放在这里方便大家查看并找到自己需要的知识点。 最近在全力整理《高性能JavaScript》的文档,并重新学习一遍,放在这里方便大家查看并找到自己需要的知识点。 前端开发文档 高性能JavaScript 第1章:加载和执行 脚本位置 阻止脚本 无阻塞的脚本 延迟的脚本 动态脚本元素 XMLHTTPRequest脚本注入 推荐的无阻塞模式...

    RayKr 评论0 收藏0
  • 精读《js 模块化发展》

    摘要:我是这一期的主持人黄子毅本期精读的文章是。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。精读本次提出独到观点的同学有流形,黄子毅,苏里约,,杨森,淡苍,留影,精读由此归纳。 这次是前端精读期刊与大家第一次正式碰面,我们每周会精读并分析若干篇精品好文,试图讨论出结论性观点。没错,我们试图通过观点的碰撞,争做无主观精品好文的意见领袖。 我是这一期的主持人 ——...

    Freelander 评论0 收藏0

发表评论

0条评论

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