jQuery 中 extend 的实现
侧边栏壁纸
  • 累计撰写 1,061 篇文章
  • 累计收到 0 条评论

jQuery 中 extend 的实现

加速器之家
2024-10-21 / 0 评论 / 3 阅读 / 正在检测是否收录...

通常我们使用 jquery 的 extend 时,大都是为了实现默认字段的覆盖,即若传入某个字段的值,则使用传入值,否则使用默认值。如下面的代码:

function getOpt(option) {
  var _default = {
    name: "wenzi",
    age: "25",
    sex: "male",
  };
  $.extend(_default, option);
  return _default;
}
getOpt(); // {name: "wenzi", age: "25", sex: "male"}
getOpt({ name: "bing" }); // {name: "bing", age: "25", sex: "male"}
getOpt({ name: "bing", age: 36, sex: "female" }); // {name: "bing", age: 36, sex: "female"}

那现在我们就得需要知道这个 extend 具体是怎么实现的了,除了实现上面的功能,还有其他作用么?那肯定是有的啦,否则我也不会问那句话了((⊙﹏⊙)b)。我们先来看看 extend 主要有哪些功能,然后再看实现这些功能的原理。

1. extend 能实现的功能 #

其实从 extend 的含义里,我们就能知道 extend 是做什么的了。extend 翻译成汉语后就是:延伸、扩展、推广。

1.1 将两个或更多对象的内容合并到第一个对象 #

我们来看看$.extend()提供的参数:jQuery.extend( target [, object1 ] [, objectN ] ),extend 方法需要至少传入一个参数,第一个必需,后面的都是可选参数。若传给 extend 是两个或两个以上的参数都是对象类型,那么就会把后面所有对象的内容合并给 target(第一个对象)上。

我们再来看看上面的例子:

function getOpt(option) {
  var _default = {
    name: "wenzi",
    age: "25",
    sex: "male",
  };
  $.extend(_default, option);

  return _default;
}

$.extend()中接收了两个参数 _defaultoption,那么 extend 方法执行时,就会把 option 对象上字段的值全给了 _default。于是 _default 上设置的默认值就会被 option 上的值覆盖。当然,若 option 上没有这个字段,就不会覆盖 _default 上字段的值。

上面函数中的 extend,只是传入了两个参数,那传的参数再更多一些呢:

function getOpt(target, obj1, obj2, obj3) {
  $.extend(target, obj1, obj2, obj3);
  return target;
}

var _default = {
  name: "wenzi",
  age: "25",
  sex: "male",
};
var obj1 = {
  name: "obj1",
};
var obj2 = {
  name: "obj2",
  age: "36",
};
var obj3 = {
  age: "67",
  sex: { error: "sorry, I dont't kown" },
};

// {name: "obj2", age: "67", sex: {error: "sorry, I dont't kown"}}
getOpt(_default, obj1, obj2, obj3);

这里我们传入了 4 个参数,然后 getOpt()返回第一个参数的值。从运行的得到结果我们可以看到,属性值永远是最后一个属性的值。

还有很重要的一点,$.extend()其实是有返回值的,返回的就是修改后的第一个参数的值。如我们可以把上面的函数修改成这样:

function getOpt(target, obj1, obj2, obj3) {
  var result = $.extend(target, obj1, obj2, obj3);
  return result; // // result即修改后的target值
}

若我们传入的参数不想被修改,我们可以用一个空对象来作为第一个参数,然后获取$.extend()的返回值:

function getOpt(target, obj1, obj2, obj3) {
  var result = $.extend({}, target, obj1, obj2, obj3);
  return result; // // result即为{}修改后的值
}

1.2 为 jQuery 扩展方法或属性 #

刚才我们在 1.1 中讲的$.extend()的例子都是传了两个或两个以上的参数,但其实只有一个参数是必须的。若只传一个参数会怎样呢。

如果只有一个参数提供给$.extend(),这意味着目标参数被省略。在这种情况下,jQuery 对象本身被默认为目标对象。这样,我们可以在 jQuery 的命名空间下添加新的功能。这对于插件开发者希望向 jQuery 中添加新函数时是很有用的。

举个例子:

$.extend({
  _name: "wenzi",
  _getName: function () {
    return this._name;
  },
});

$._name; // wenzi
$._getName(); // wenzi

这样我们就为 jQuery 扩展了_name属性和_getName方法。

1.3 深度拷贝和浅度拷贝 #

针对什么是深度拷贝,什么是浅度拷贝,我们先来看一个简单的例子。

var obj = { name: "wenzi", sex: "male" };
var obj1 = obj; // 赋值
obj1.name = "bing";
console.log(obj.name); // bing

我们修改了 obj1 中的 name 值,结果 obj 中的值也跟着发生了变化,这是为什么呢。其实这就是 浅度拷贝 :这仅仅是将 obj 对象的引用地址简单的复制了一份给予变量 obj1,而并不是将真正的对象克隆了一份,因此 obj 和 obj1 指向的都是同一个地址。当修改 obj1 的属性或给 obj1 添加新属性时,obj 都会受到影响。

可是如果变量的值不是对象和数组,修改后面的变量是不会影响到前面的变量:

var s = "hello";
var t = s;
t = "world";
console.log(s); // hello

那么深度拷贝就不是拷贝引用地址,而是实实在在的复制一份新对象给新的变量。 在上面使用$.extend()中,都是使用的浅度拷贝,因此若后面的参数值是 object 类型或 array 类型,修改_default(target)的值,就会影响后面参数的值。

如我们使用getOpt(_default, obj1, obj2, obj3);得到的_default 值是{name: "obj2", age: "67", sex: {error: "sorry, I dont't kown"}},可是若:

_default.sex.error = "hello world";

那么 obj3.sex.error 也会跟着修改,因为 obj3.sex 是一个 object 类型。

不过$.extend()也提供了深度拷贝的方法:jQuery.extend( [deep ], target, object1 [, objectN ] )。若第一个参数是 boolean 类型,且值是 true,那么就会把第二个参数作为目标参数进行合并。

var obj = { name: "wenzi", score: 80 };
var obj1 = { score: { english: 80, math: 90 } };
$.extend(true, obj, obj1);
obj.score.english = 10;
console.log(obj.score.english); // 10
console.log(obj1.score.english); // 80

执行后我们发现,无论怎么修改 obj.score 里的值,都不会影响到 obj1.score 了。

2. jQuery 中 extend 实现原理 #

其实不看源码,对 extend 大致的过程应该也是了解的:对后一个参数进行循环,然后把后面参数上所有的字段都给了第一个字段,若第一个参数里有相同的字段,则进行覆盖操作,否则就添加一个新的字段。

下面是 jQuery 中关于 extend 的源码,我就在源码上进行注释讲解了,随后再在后面进行总结:

// 为与源码的下标对应上,我们把第一个参数称为`第0个参数`,依次类推
jQuery.extend = jQuery.fn.extend = function () {
  var options,
    name,
    src,
    copy,
    copyIsArray,
    clone,
    target = arguments[0] || {}, // 默认第0个参数为目标参数
    i = 1, // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并
    length = arguments.length,
    deep = false; // 默认为浅度拷贝

  // 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true还是false
  // 同时将第1个参数作为目标参数,i从当前目标参数的下一个
  // Handle a deep copy situation
  if (typeof target === "boolean") {
    deep = target;

    // Skip the boolean and the target
    target = arguments[i] || {};
    i++;
  }

  // 	判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数重新赋值
  // Handle case when target is a string or something (possible in deep copy)
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'})
  // 则目标参数即为jQuery本身,而target表示的参数不再为目标参数
  // Extend jQuery itself if only one argument is passed
  if (i === length) {
    target = this;
    i--;
  }

  // 从第i个参数开始
  for (; i < length; i++) {
    // 获取第i个参数,且该参数不为null和undefind,在js中null和undefined,如果不区分类型,是相等的,null==undefined为true,
    // 因此可以用null来同时过滤掉null和undefind
    // 比如$.extend(target, {}, null);中的第2个参数null是不参与合并的
    // Only deal with non-null/undefined values
    if ((options = arguments[i]) != null) {
      // 使用for~in获取该参数中所有的字段
      // Extend the base object
      for (name in options) {
        src = target[name]; // 目标参数中name字段的值
        copy = options[name]; // 当前参数中name字段的值

        // 若参数中字段的值就是目标参数,停止赋值,进行下一个字段的赋值
        // 这是为了防止无限的循环嵌套,我们把这个称为,在下面进行比较详细的讲解
        // Prevent never-ending loop
        if (target === copy) {
          continue;
        }

        // 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值
        // Recurse if we're merging plain objects or arrays
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          // 若当前参数中name字段的值为Array类型
          // 判断目标参数中name字段的值是否存在,若存在则使用原来的,否则进行初始化
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src : [];
          } else {
            // 若原对象存在,则直接进行使用,而不是创建
            clone = src && jQuery.isPlainObject(src) ? src : {};
          }

          // 递归处理,此处为2.2
          // Never move original objects, clone them
          target[name] = jQuery.extend(deep, clone, copy);

          // deep为false,则表示浅度拷贝,直接进行赋值
          // 若copy是简单的类型且存在值,则直接进行赋值
          // Don't bring in undefined values
        } else if (copy !== undefined) {
          // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性
          target[name] = copy;
        }
      }
    }
  }

  // 返回修改后的目标参数
  // Return the modified object
  return target;
};

源码分析完了,下面我们来讲解下源码中存在的几个难点和重点。

2.1 若参数中字段的值就是目标参数,停止赋值 #

在源码中进行了一下这样的判断:

// Prevent never-ending loop
if ( target === copy ) {
	continue;
}

为什么要有这样的判断,我们来看一个简单的例子,如果没有这个判断会怎么样:

var _default = { name: "wenzi" };
var obj = { name: _default };
$.extend(_default, obj);
console.log(_default);

输出的_default 是什么呢:

_default = { name: _default };

_default 是 object 类型,里面有个字段 name,值是_default,而_default 是 object 类型,里面有个字段 name,值是_default......,无限的循环下去。于是 jQuery 中直接不进行操作,跳过这个字段,进行下一个字段的操作。

2.2 深度拷贝时进行递归处理 #

我们在前面稍微的讲解了一下,变量值为简单类型(如 number, string, boolean)进行赋值时是不会影响上一个变量的值的,因此,如果当前字段的值为ObjectArray类型,需要对其进行拆分,直到字段的值为简单类型(如 number, string, boolean)时才进行赋值操作。

3. $.extend()与$.fn.extend() #

上面讲解的全都是$.extend(),根本就没讲$.fn.extend()。可是,你有没有发现一个细节,在这段代码的第一行是怎么写的:

jQuery.extend = jQuery.fn.extend = function () {};

也就是说$.extend()与$.fn.extend()共用的是同一个函数体,所有的操作都是一样的,只不过两个 extend 使用的对象不同罢了:$.extend()是在 jQuery($)上进行操作的;而$.fn.extend()是在 jQuery 对象上进行操作的,如$('div').extend().

4. 总结 #

这就是 jQuery 中 extend 的实现,以后若我们需要用到上面的功能时,除了使用$.extend(),我们也可以在不引入 jQuery 框架的情况下,自己写一个简单的 extend()来实现上面的功能。

0

评论

博主关闭了当前页面的评论