JavaScript merge 函数解析与实例详解

这篇文章将对一段借鉴自 jQuery 3.6.0 中 jQuery.extend 并经过修改的 JavaScript 代码做出详细解析。文章将对每一行代码进行说明、讨论其设计思想以及运行机制,并提供能够直接运行的代码实例和运行结果说明。文章中涉及的所有英文单词与中文字符混排时将确保二者之间存在一个空格符号,并且所有成对匹配的英文双引号 " 均替换为反引号 ` 。下文将依托严谨的逻辑推理和详细的源码注释来介绍每一部分的代码功能。

本段代码定义了一个名为 fnMerge 的函数,主要用于合并多个对象,其设计思路参照 jQuery 中用于扩展对象的机制,因此具有深拷贝和浅拷贝两种操作模式。代码整体遵循模块化编程思想,首先对必要的局部变量进行了声明,其后根据函数参数选择不同的合并策略,再通过循环遍历多个待合并对象的属性逐个合并。最后将合并后的结果对象返回。这种方式不仅保证了原有对象数据的不被破坏,同时也通过递归调用实现了嵌套对象和数组的深层合并操作。

起初,代码使用 var fnMerge = function() { … } 来声明一个匿名函数并赋值给变量 fnMerge。注释部分提到,该函数从 jQuery JavaScript Library v3.6.0 中的 jQuery.extend 获取灵感,并经过修改,故而继承了原实现中高效且安全的对象扩展模式。注释中详细列出了版权信息、许可证以及项目官网链接,这表明原始代码具有开源许可并在大规模框架中使用过,值得信赖并具有良好维护经验。

接下来的代码块中声明了多个局部变量:

var src, copyIsArray, copy, name, options, clone,
    target = arguments[2] || {},
    i = 3,
    length = arguments.length,
    deep = arguments[0] || false,
    skipToken = arguments[1] ? undefined : oToken;

此处各变量的用途逐一说明:

  • 变量 src 用于保存目标对象中对应属性的原始值;
  • 变量 copyIsArray 作为布尔值标志,用于指示当前复制的属性值是否为数组;
  • 变量 copy 保存从扩展对象中取出的属性值;
  • 变量 name 用作 for … in 循环中遍历属性名;
  • 变量 options 用于保存当前正被处理的扩展对象;
  • 变量 clone 则用于存放数组或者对象在需要递归复制时的克隆副本;
  • 变量 target 初始化为传递进来的第三个参数(arguments[2]),如果未提供则默认为空对象 {}
  • 变量 i 被初始化为 3,表示从第 4 个参数开始依次进行对象合并;
  • 变量 length 表示传入参数的总个数;
  • 变量 deep 则从第一个参数中取值,若为真则启用深拷贝模式,否则执行浅拷贝;
  • 变量 skipToken 根据第二个参数是否为真来决定其值。若第二个参数为真,则 skipToken 的值设为 undefined,否则为一个特殊值 oToken,用于之后在属性复制时判断是否排除某些值。

这一部分代码展示了通过函数 arguments 数组实现灵活参数传递的技巧,此种方式允许用户传入不同数量的参数,同时通过索引标识确定各参数的功能。对于深拷贝参数 deep 和 skipToken 的判断确保了合并操作能够根据需求进行精细控制。

紧接着,代码判断 target 的数据类型,保证它是一个对象或函数类型:

if ( typeof target !== `object` && typeof target !== `function` ) {
    target = {};
}

这段代码通过 typeof 运算符检测 target,如果传入的 target 既不是 object 又不是 function,则重置 target 为一个空对象 {}。这种防御性编程手段能够规避错误的数据类型传入情况下的异常行为,确保接下来的对象属性操作安全可靠。对于深拷贝过程中可能出现的字符串等原始类型来说,这一验证尤为关键。

接下来,代码进入了一个 for 循环,遍历从第 4 个参数开始的所有参数对象:

for ( ; i < length; i++ ) {
    if ( ( options = arguments[ i ] ) != null ) {

循环条件保证 i 从 3 开始一直遍历到传入参数列表结束。对于每个参数,代码首先检查参数是否为 null 或 undefined,确保仅对有效的对象进行操作。此处通过 if 判断不仅排除了 null,还避免了可能存在 undefined 值的情况。

在循环体内,代码对当前传入的对象 options 使用 for … in 循环进行遍历:

for ( name in options ) {
    src = target[ name ];
    copy = options[ name ];

此处 name 为 options 中的每个属性名,src 取 target 中对应属性的值,而 copy 则是扩展对象 options 中的当前属性值。通过这种方式,可以将多个对象的属性依次合并到 target 中,同时保留原对象中可能存在的同名属性,以便判断是否需要进行递归合并。

接着,代码对属性名进行安全校验:

if ( name === `__proto__` || target === copy ) {
    continue;
}

此处通过 if 判断若属性名为 __proto__ 或目标对象 target 与当前复制的值 copy 是相同值,则使用 continue 跳过本轮循环。通过这种方式可以防止原型污染问题,避免利用属性扩展机制对目标对象的原型链进行篡改,同时也防止死循环情况的发生(例如拷贝过程中意外引用自身)。

随后,代码对当前属性值是否需要进行深度合并进行判断:

if ( deep && copy && ( isPlainObject( copy ) ||
    ( copyIsArray = Array.isArray( copy ) ) ) ) {

在此判断条件中,各个因素协同作用:

  • deep 参数为真,表明用户希望进行深度合并;
  • copy 必须存在(即不为 null 或 undefined),以确保复制的对象或数组实际具有内容;
  • 函数 isPlainObject 用于判断 copy 是否为一个纯粹的对象;
  • 同时,也会用 Array.isArray 判断 copy 是否为数组,并将结果赋值给 copyIsArray。如果其中任一条件为真,则认为当前属性值需要进行深拷贝操作。
    利用递归原理,将深层结构(数组或对象)合并时不会简单复制引用,而是通过递归调用 fnMerge 生成新的副本,防止数据混用或引用冲突情况的发生。

内部判断中根据当前属性值是否为数组进行进一步分支判断:

if ( copyIsArray ) {
    copyIsArray = false;
    clone = src && Array.isArray( src ) ? src : [];
} else {
    clone = src && isPlainObject( src ) ? src : {};
}

这段代码根据 copyIsArray 的真假分别执行不同的操作。若 copyIsArray 为真,说明当前待复制的属性值 copy 为数组,此时将 copyIsArray 重置为 false(以便后续可能的复制操作不受上一次判断影响)。接着,判断 target 中对应的 src 是否已存在且为数组,若是,则直接将 clone 指定为 src,否则将 clone 初始化为空数组 []。在当前属性值为对象的分支中,若 target 中对应属性存在且也是一个纯粹对象,则 clone 指向 src,否则以空对象 {} 作为基础。这样的策略确保当目标对象中已经存在合适类型的副本时,直接在其基础上进行扩展而不重新创建。

在确定好 clone 后,代码采用递归调用的方式对当前属性值 copy 进行深度合并操作:

target[ name ] = fnMerge( deep, arguments[1], clone, copy );

此处调用 fnMerge 并传入 deep 参数保持深拷贝状态,同时传递第二个参数(skipToken 相关参数),再传入已有的 clone 和当前的扩展对象 copy。递归调用的结果覆盖 target 中的 name 属性值,从而把数组或对象的所有内部属性通过递归逐层合并到目标对象中。递归策略确保即使嵌套层级很深,也能够完整并精确地将所有子属性复制过去。

对于不满足深度合并条件的属性,代码采用直接赋值的简单操作:

} else if ( copy !== skipToken ) {
    target[ name ] = copy;
}

这里使用条件判断 copy !== skipToken 来排除某些特殊值不进行复制,从而避免将无效或不应复制的值覆盖目标对象中的原有属性。直接赋值的方式较为简单高效,适用于基本数据类型或非复杂对象的赋值操作。

循环遍历结束后,代码将修改好的 target 返回:

return target;

这一返回语句意味着经过逐个合并所有对象中的属性操作后,最终获得了一个经过安全防护、可选深拷贝、递归合并后的对象。函数 fnMerge 也因此具备灵活性和高扩展性,可广泛应用于各类需要对象合并的场景。

文章最后通过 return fnMerge 将整个 fnMerge 函数导出或暴露给外部使用。这种写法一般出现在模块化或闭包环境中,能够保证 fnMerge 作为一个独立函数被正确返回并在其他部分代码中调用。

下面给出一段完整且能够运行的代码示例,示例中演示了使用 fnMerge 函数对两个对象进行合并,并采用深拷贝模式处理嵌套属性结构。代码实例中所有成对匹配的双引号均替换为反引号 ` ,同时英文单词与中文之间均保留空格分隔:

// 定义辅助函数 isPlainObject 用于判断是否为纯对象
function isPlainObject( obj ) {
    if ( typeof obj !== `object` || obj === null ) {
        return false;
    }
    var proto = Object.getPrototypeOf( obj );
    return proto === Object.prototype || proto === null;
}

// 定义全局变量 oToken 作为跳过标识符
var oToken = {};

// 定义 fnMerge 函数
var fnMerge = function() {
    /*
     * 此处代码借鉴自 jQuery 3.6.0 中 `jQuery.extend` 方法并经过部分修改
     *
     * jQuery JavaScript Library v3.6.0
     * https://jquery.com/
     *
     * Copyright OpenJS Foundation and other contributors
     * Released under the MIT license
     * https://jquery.org/license
     */
    var src, copyIsArray, copy, name, options, clone,
        target = arguments[2] || {},
        i = 3,
        length = arguments.length,
        deep = arguments[0] || false,
        skipToken = arguments[1] ? undefined : oToken;

    if ( typeof target !== `object` && typeof target !== `function` ) {
        target = {};
    }

    for ( ; i < length; i++ ) {
        if ( ( options = arguments[ i ] ) != null ) {
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                if ( name === `__proto__` || target === copy ) {
                    continue;
                }

                if ( deep && copy && ( isPlainObject( copy ) ||
                    ( copyIsArray = Array.isArray( copy ) ) ) ) {

                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && Array.isArray( src ) ? src : [];
                    } else {
                        clone = src && isPlainObject( src ) ? src : {};
                    }

                    target[ name ] = fnMerge( deep, arguments[1], clone, copy );
                } else if ( copy !== skipToken ) {
                    target[ name ] = copy;
                }
            }
        }
    }
    return target;
};

// 示例代码:使用 fnMerge 对两个对象进行合并
// 示例对象 obj1 包含基本属性与嵌套对象属性
var obj1 = {
    a: 1,
    b: {
        c: 2
    },
    d: [ 1, 2, 3 ]
};

// 示例对象 obj2 包含与 obj1 部分重复的属性以及新的属性
var obj2 = {
    b: {
        d: 4,
        c: 3
    },
    d: [ 4, 5 ],
    e: 5
};

// 调用 fnMerge 进行深拷贝合并操作,传入参数分别为深拷贝标志、跳过标识(此处设置为 false)、目标对象 obj1 及源对象 obj2
var mergedResult = fnMerge( true, false, obj1, obj2 );
console.log( mergedResult );
// 输出结果为:
// {
//     a: 1,
//     b: { c: 3, d: 4 },
//     d: [ 4, 5 ],
//     e: 5
// }

此实例展示了当启用深拷贝模式时,嵌套对象属性 b 中的子属性被正确递归处理,并将 obj2 中相应的属性值复制到目标对象中。数组属性 d 则被视作需特殊处理的数据类型,若目标中已有数组则直接采用现有的数组作为起点,再将源数组覆盖合并。在该示例中,属性 e 为新属性,因此直接拷贝到目标对象中。

对代码整体而言,设计思想中融入了防止原型污染、避免引用自循环以及灵活深度复制三大重要原则。通过对传入参数的灵活解析,fnMerge 能够适应不同场景下的对象扩展需求。在实际应用中,该函数可用于配置对象的扩展、组件属性的混合以及复杂数据结构的合并操作,具有极高的实用价值。函数中的递归调用机制有效地保证了对于多层嵌套结构的复制过程中每一层数据均可保持独立性和完整性,避免出现因直接引用同一内存地址而产生的数据覆盖错误。

代码中对跳过属性的判断(例如属性名为 __proto__)体现了作者对于安全性的考虑,防范了可能利用对象扩展机制进行原型污染的安全风险。对于深拷贝与浅拷贝模式的区分,则使得用户可以根据需求选择性地进行资源密集型操作或性能优先的快速合并。通过这种设计,fnMerge 既兼顾了性能问题,也满足了多样化场景下的应用需求。

此外,示例代码中自定义的辅助函数 isPlainObject 与全局变量 oToken 均起到了辅助作用,其中 isPlainObject 能够精准判断目标值是否为普通对象,而 oToken 则作为一种标记,用于在复制过程中避免将特定的无效值错误地纳入合并结果。整段代码充分体现了高水平 JavaScript 编程中的细节把控与异常安全处理,为开发者提供了一个可靠且灵活的对象扩展工具。

通过上面代码的讲解与示例演示,各位开发者可以全面理解该函数如何从多个角度检测、处理并整合不同的数据结构,同时体会到递归调用与安全校验在合并操作中的实际意义。这种代码结构适用于在复杂项目中对配置对象的构造、插件配置合并以及数据处理过程中的多层复制场景,提供了一种既高效又安全的解决方案。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪子熙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值