前言

jQuery 的 extend 是 jQuery 中应用非常多的一个函数,今天我们一边看 jQuery 的 extend 的特性,一边实现一个 extend!

extend 基本用法

先来看看 extend 的功能,引用 jQuery 官网:

Merge the contents of two or more objects together into the first object.

翻译过来就是,合并两个或者更多的对象的内容到第一个对象中。

让我们看看 extend 的用法:

  1. jQuery.extend( target [, object1 ] [, objectN ] )

第一个参数 target,表示要拓展的目标,我们就称它为目标对象吧。

后面的参数,都传入对象,内容都会复制到目标对象中,我们就称它们为待复制对象吧。

举个例子:

  1. var obj1 = {
  2. a: 1,
  3. b: { b1: 1, b2: 2 }
  4. };
  5.  
  6. var obj2 = {
  7. b: { b1: 3, b3: 4 },
  8. c: 3
  9. };
  10.  
  11. var obj3 = {
  12. d: 4
  13. }
  14.  
  15. console.log($.extend(obj1, obj2, obj3));
  16.  
  17. // {
  18. // a: 1,
  19. // b: { b1: 3, b3: 4 },
  20. // c: 3,
  21. // d: 4
  22. // }

当两个对象出现相同字段的时候,后者会覆盖前者,而不会进行深层次的覆盖。

extend 第一版

结合着上篇写得 《JavaScript专题之深浅拷贝》,我们尝试着自己写一个 extend 函数:

  1. // 第一版
  2. function extend() {
  3. var name, options, copy;
  4. var length = arguments.length;
  5. var i = 1;
  6. var target = arguments[0];
  7.  
  8. for (; i < length; i++) {
  9. options = arguments[i];
  10. if (options != null) {
  11. for (name in options) {
  12. copy = options[name];
  13. if (copy !== undefined){
  14. target[name] = copy;
  15. }
  16. }
  17. }
  18. }
  19.  
  20. return target;
  21. };

extend 深拷贝

那如何进行深层次的复制呢?jQuery v1.1.4 加入了一个新的用法:

  1. jQuery.extend( [deep], target, object1 [, objectN ] )

也就是说,函数的第一个参数可以传一个布尔值,如果为 true,我们就会进行深拷贝,false 依然当做浅拷贝,这个时候,target 就往后移动到第二个参数。

还是举这个例子:

  1. var obj1 = {
  2. a: 1,
  3. b: { b1: 1, b2: 2 }
  4. };
  5.  
  6. var obj2 = {
  7. b: { b1: 3, b3: 4 },
  8. c: 3
  9. };
  10.  
  11. var obj3 = {
  12. d: 4
  13. }
  14.  
  15. console.log($.extend(true, obj1, obj2, obj3));
  16.  
  17. // {
  18. // a: 1,
  19. // b: { b1: 3, b2: 2, b3: 4 },
  20. // c: 3,
  21. // d: 4
  22. // }

因为采用了深拷贝,会遍历到更深的层次进行添加和覆盖。

extend 第二版

我们来实现深拷贝的功能,值得注意的是:

  • 需要根据第一个参数的类型,确定 target 和要合并的对象的下标起始值。
  • 如果是深拷贝,根据 copy 的类型递归 extend。
  1. // 第二版
  2. function extend() {
  3. // 默认不进行深拷贝
  4. var deep = false;
  5. var name, options, src, copy;
  6. var length = arguments.length;
  7. // 记录要复制的对象的下标
  8. var i = 1;
  9. // 第一个参数不传布尔值的情况下,target默认是第一个参数
  10. var target = arguments[0] || {};
  11. // 如果第一个参数是布尔值,第二个参数是才是target
  12. if (typeof target == 'boolean') {
  13. deep = target;
  14. target = arguments[i] || {};
  15. i++;
  16. }
  17. // 如果target不是对象,我们是无法进行复制的,所以设为{}
  18. if (typeof target !== 'object') {
  19. target = {}
  20. }
  21.  
  22. // 循环遍历要复制的对象们
  23. for (; i < length; i++) {
  24. // 获取当前对象
  25. options = arguments[i];
  26. // 要求不能为空 避免extend(a,,b)这种情况
  27. if (options != null) {
  28. for (name in options) {
  29. // 目标属性值
  30. src = target[name];
  31. // 要复制的对象的属性值
  32. copy = options[name];
  33.  
  34. if (deep && copy && typeof copy == 'object') {
  35. // 递归调用
  36. target[name] = extend(deep, src, copy);
  37. }
  38. else if (copy !== undefined){
  39. target[name] = copy;
  40. }
  41. }
  42. }
  43. }
  44.  
  45. return target;
  46. };

在实现上,核心的部分还是跟上篇实现的深浅拷贝函数一致,如果要复制的对象的属性值是一个对象,就递归调用 extend。不过 extend 的实现中,多了很多细节上的判断,比如第一个参数是否是布尔值,target 是否是一个对象,不传参数时的默认值等。

接下来,我们看几个 jQuery 的 extend 使用效果:

target 是函数

在我们的实现中,typeof target 必须等于 object,我们才会在这个 target 基础上进行拓展,然而我们用 typeof 判断一个函数时,会返回function,也就是说,我们无法在一个函数上进行拓展!

什么,我们还能在一个函数上进行拓展!!

当然啦,毕竟函数也是一种对象嘛,让我们看个例子:

  1. function a() {}
  2.  
  3. a.target = 'b';
  4.  
  5. console.log(a.target); // b

实际上,在 underscore 的实现中,underscore 的各种方法便是挂在了函数上!

所以在这里我们还要判断是不是函数,这时候我们便可以使用《JavaScript专题之类型判断(上)》中写得 isFunction 函数

我们这样修改:

  1. if (typeof target !== "object" && !isFunction(target)) {
  2. target = {};
  3. }

类型不一致

其实我们实现的方法有个小 bug ,不信我们写个 demo:

  1. var obj1 = {
  2. a: 1,
  3. b: {
  4. c: 2
  5. }
  6. }
  7.  
  8. var obj2 = {
  9. b: {
  10. c: [5],
  11.  
  12. }
  13. }
  14.  
  15. var d = extend(true, obj1, obj2)
  16. console.log(d);

我们预期会返回这样一个对象:

  1. {
  2. a: 1,
  3. b: {
  4. c: [5]
  5. }
  6. }

然而返回了这样一个对象:

  1. {
  2. a: 1,
  3. b: {
  4. c: {
  5. 0: 5
  6. }
  7. }
  8. }

让我们细细分析为什么会导致这种情况:

首先我们在函数的开始写一个 console 函数比如:console.log(1),然后以上面这个 demo 为例,执行一下,我们会发现 1 打印了三次,这就是说 extend 函数执行了三遍,让我们捋一捋这三遍传入的参数:

第一遍执行到递归调用时:

  1. var src = { c: 2 };
  2. var copy = { c: [5]};
  3.  
  4. target[name] = extend(true, src, copy);

第二遍执行到递归调用时:

  1. var src = 2;
  2. var copy = [5];
  3.  
  4. target[name] = extend(true, src, copy);

第三遍进行最终的赋值,因为 src 是一个基本类型,我们默认使用一个空对象作为目标值,所以最终的结果就变成了对象的属性!

为了解决这个问题,我们需要对目标属性值和待复制对象的属性值进行判断:

判断目标属性值跟要复制的对象的属性值类型是否一致:

  • 如果待复制对象属性值类型为数组,目标属性值类型不为数组的话,目标属性值就设为 []

  • 如果待复制对象属性值类型为对象,目标属性值类型不为对象的话,目标属性值就设为 {}

结合着《JavaScript专题之类型判断(下)》中的 isPlainObject 函数,我们可以对类型进行更细致的划分:

  1. var clone, copyIsArray;
  2.  
  3. ...
  4.  
  5. if (deep && copy && (isPlainObject(copy) ||
  6. (copyIsArray = Array.isArray(copy)))) {
  7.  
  8. if (copyIsArray) {
  9. copyIsArray = false;
  10. clone = src && Array.isArray(src) ? src : [];
  11.  
  12. } else {
  13. clone = src && isPlainObject(src) ? src : {};
  14. }
  15.  
  16. target[name] = extend(deep, clone, copy);
  17.  
  18. } else if (copy !== undefined) {
  19. target[name] = copy;
  20. }

循环引用

实际上,我们还可能遇到一个循环引用的问题,举个例子:

  1. var a = {name : b};
  2. var b = {name : a}
  3. var c = extend(a, b);
  4. console.log(c);

我们会得到一个可以无限展开的对象,类似于这样:

循环引用对象

为了避免这个问题,我们需要判断要复制的对象属性是否等于 target,如果等于,我们就跳过:

  1. ...
  2. src = target[name];
  3. copy = options[name];
  4.  
  5. if (target === copy) {
  6. continue;
  7. }
  8. ...

如果加上这句,结果就会是:

  1. {name: undefined}

最终代码

  1. // isPlainObject 函数来自于 [JavaScript专题之类型判断(下) ](https://github.com/mqyqingfeng/Blog/issues/30)
  2. var class2type = {};
  3. var toString = class2type.toString;
  4. var hasOwn = class2type.hasOwnProperty;
  5.  
  6. function isPlainObject(obj) {
  7. var proto, Ctor;
  8. if (!obj || toString.call(obj) !== "[object Object]") {
  9. return false;
  10. }
  11. proto = Object.getPrototypeOf(obj);
  12. if (!proto) {
  13. return true;
  14. }
  15. Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
  16. return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
  17. }
  18.  
  19.  
  20. function extend() {
  21. // 默认不进行深拷贝
  22. var deep = false;
  23. var name, options, src, copy, clone, copyIsArray;
  24. var length = arguments.length;
  25. // 记录要复制的对象的下标
  26. var i = 1;
  27. // 第一个参数不传布尔值的情况下,target 默认是第一个参数
  28. var target = arguments[0] || {};
  29. // 如果第一个参数是布尔值,第二个参数是 target
  30. if (typeof target == 'boolean') {
  31. deep = target;
  32. target = arguments[i] || {};
  33. i++;
  34. }
  35. // 如果target不是对象,我们是无法进行复制的,所以设为 {}
  36. if (typeof target !== "object" && !isFunction(target)) {
  37. target = {};
  38. }
  39.  
  40. // 循环遍历要复制的对象们
  41. for (; i < length; i++) {
  42. // 获取当前对象
  43. options = arguments[i];
  44. // 要求不能为空 避免 extend(a,,b) 这种情况
  45. if (options != null) {
  46. for (name in options) {
  47. // 目标属性值
  48. src = target[name];
  49. // 要复制的对象的属性值
  50. copy = options[name];
  51.  
  52. // 解决循环引用
  53. if (target === copy) {
  54. continue;
  55. }
  56.  
  57. // 要递归的对象必须是 plainObject 或者数组
  58. if (deep && copy && (isPlainObject(copy) ||
  59. (copyIsArray = Array.isArray(copy)))) {
  60. // 要复制的对象属性值类型需要与目标属性值相同
  61. if (copyIsArray) {
  62. copyIsArray = false;
  63. clone = src && Array.isArray(src) ? src : [];
  64.  
  65. } else {
  66. clone = src && isPlainObject(src) ? src : {};
  67. }
  68.  
  69. target[name] = extend(deep, clone, copy);
  70.  
  71. } else if (copy !== undefined) {
  72. target[name] = copy;
  73. }
  74. }
  75. }
  76. }
  77.  
  78. return target;
  79. };

思考题

如果觉得看明白了上面的代码,想想下面两个 demo 的结果:

  1. var a = extend(true, [4, 5, 6, 7, 8, 9], [1, 2, 3]);
  2. console.log(a) // ???
  1. var obj1 = {
  2. value: {
  3. 3: 1
  4. }
  5. }
  6.  
  7. var obj2 = {
  8. value: [5, 6, 7],
  9.  
  10. }
  11.  
  12. var b = extend(true, obj1, obj2) // ???
  13. var c = extend(true, obj2, obj1) // ???

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。