数组的浅拷贝

利用 sliceconcat 函数。

  1. // concat
  2. const arr = ["old", 1, true, null, undefined];
  3. const new_arr = arr.concat();
  4. console.log(arr); // ["old", 1, true, null, undefined]
  5. console.log(new_arr); // ["old", 1, true, null, undefined]
  6. // slice
  7. const arr = ["old", 1, true, null, undefined];
  8. const new_arr = arr.concat();
  9. console.log(arr); // ["old", 1, true, null, undefined]
  10. console.log(new_arr); // ["old", 1, true, null, undefined]

这两种方法只能对基本类型起作用,但对于引用类型就只是复制引用的地址。

JSON 序列化

  1. var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}];
  2. var new_arr = JSON.parse( JSON.stringify(arr) );
  3. console.log(new_arr); // ['old', 1, true, ['old1', 'old2'], {old: 1}]

这是个好方法但是不能拷贝函数,不信你看。

  1. const arr = [
  2. function() {
  3. console.log(a);
  4. },
  5. {
  6. b: function() {
  7. console.log(b);
  8. }
  9. }
  10. ];
  11. const new_arr = JSON.parse(JSON.stringify(arr));
  12. console.log(new_arr);

image.png

函数直接就变成 null 了,对于嵌套在对象里面的函数直接就不拷贝了。

以上三种方法都非常的 hack,那有没有正常一点的方法了?有的看下面。

遍历实现浅拷贝

  1. const shallowCopy = function(obj) {
  2. // 只拷贝对象
  3. if (typeof obj !== "object") return;
  4. // 根据obj的类型判断是新建一个数组还是对象
  5. var newObj = obj instanceof Array ? [] : {};
  6. // 遍历obj,并且判断是obj的属性才拷贝
  7. for (var key in obj) {
  8. if (obj.hasOwnProperty(key)) {
  9. newObj[key] = obj[key];
  10. }
  11. }
  12. return newObj;
  13. };

那怎样实现深拷贝了?很简单判断拷贝的属性值是对象就递归遍历。看下面。

递归遍历实现深拷贝

  1. const deepCopy = function(obj) {
  2. if (typeof obj !== "object") return;
  3. var newObj = obj instanceof Array ? [] : {};
  4. for (var key in obj) {
  5. if (obj.hasOwnProperty(key)) {
  6. newObj[key] = typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key];
  7. }
  8. }
  9. return newObj;
  10. };

实现 extend 函数

jq 有一个 extend 函数用于拷贝。看看 jq 是怎么描述这个函数的。

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

_
意思是:合并第二个参数以上的对象到第一个参数对象上(讲得怪怪的)。

基本用法

  1. const obj1 = {
  2. a: 1,
  3. b: { b1: 1, b2: 2 }
  4. };
  5. const obj2 = {
  6. b: { b1: 3, b3: 4 },
  7. c: 3
  8. };
  9. const obj3 = {
  10. d: 4
  11. }
  12. console.log($.extend(obj1, obj2, obj3));

image.png

可以看到第二个参数以上的对象的属性会覆盖第一个参数对象的属性。

那我们来实现一下吧。

  1. function extend() {
  2. var name, options, copy;
  3. var length = arguments.length;
  4. var i = 1;
  5. // 目标对象
  6. var target = arguments[0];
  7. for (; i < length; i++) {
  8. // 需要遍历的对象
  9. options = arguments[i];
  10. // 要求不能为空 避免 extend(a,,b) 这种情况
  11. if (options != null) {
  12. for (name in options) {
  13. copy = options[name];
  14. // 排除用属性名却没有值的情况
  15. if (copy !== undefined) {
  16. target[name] = copy;
  17. }
  18. }
  19. }
  20. }
  21. return target;
  22. }

以上是浅拷贝那怎样实现深拷贝了?

在 jq 的 1.1.4 版里,在 extend 函数添加了一个 deep 参数用于表示是否深拷贝。

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

还是举个例子

  1. const obj1 = {
  2. a: 1,
  3. b: { b1: 1, b2: 2 }
  4. };
  5. const obj2 = {
  6. b: { b1: 3, b3: 4 },
  7. c: 3
  8. };
  9. const obj3 = {
  10. d: 4
  11. }
  12. console.log($.extend(true, obj1, obj2, obj3));

image.png

可以看到因为 deep 参数为 true,所以用更深层次的遍历和覆盖。

那我们实现一下这个用 deep 参数的版本吧。

  1. function extend() {
  2. // 默认不进行深拷贝
  3. var deep = false;
  4. var name, options, src, copy;
  5. var length = arguments.length;
  6. // 记录要复制的对象的下标
  7. var i = 1;
  8. var target = arguments[0];
  9. // 根据 target 的类型不同分别处理。
  10. if (typeof target === "boolean") {
  11. deep = target;
  12. // 如果 target 不是对象类型,就赋值一个空对象。
  13. if (typeof arguments[i] !== "object") {
  14. target = {};
  15. } else {
  16. target = arguments[i];
  17. }
  18. i++;
  19. } else {
  20. // 如果 target 不是对象类型,就赋值一个空对象。
  21. if (typeof arguments[0] !== "object") {
  22. target = {};
  23. } else {
  24. target = arguments[0];
  25. }
  26. }
  27. // 循环遍历要复制的对象们
  28. for (; i < length; i++) {
  29. // 获取当前对象
  30. options = arguments[i];
  31. // 要求不能为空 避免extend(a,,b)这种情况
  32. if (options != null) {
  33. for (name in options) {
  34. // 目标属性值
  35. src = target[name];
  36. // 要复制的对象的属性值
  37. copy = options[name];
  38. if (deep && copy && typeof copy == "object") {
  39. // 递归调用
  40. target[name] = extend(deep, src, copy);
  41. } else if (copy !== undefined) {
  42. target[name] = copy;
  43. }
  44. }
  45. }
  46. }
  47. return target;
  48. }

核心还是递归遍历,但 target 也可能是函数啊。那怎样兼容这种情况了。看下面。

我们可以利用在 《javascript 类型判断(基本类型)》讲的 isFunction 函数来判断 target 是函数的情况的。

修改一下代码

  1. if (typeof target === "boolean") {
  2. // ...
  3. if (typeof arguments[i] !== "object" && !isFunction(arguments[i])) {
  4. // ...
  5. } else {
  6. // ...
  7. }
  8. i++;
  9. } else {
  10. if (typeof arguments[0] !== "object" && !isFunction(arguments[i])) {
  11. // ...
  12. } else {
  13. // ...
  14. }
  15. }

还用 bug,写个例子复现一下。

  1. var obj1 = {
  2. a: 1,
  3. b: {
  4. c: 2
  5. }
  6. };
  7. var obj2 = {
  8. b: {
  9. c: [5]
  10. }
  11. };
  12. var d = extend(true, obj1, obj2);
  13. 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. }

为什么会这样的了?我们在 extend 函数开头写一个 console.log(1) 函数,看看 extend 执行了几次。

打出 3 个 1,说明执行了 3 次。我们来仔细地分析一下每次的传入参数:

第一次

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

第二次

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

第三次执行,由于 src 是一个基本类型,我们默认会赋值一个空对象作为 target 值,所以最终的结果就变成了对象的属性。

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

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

我们利用在 《javascript 类型判断(plainObject…)》讲到的 isPlainObject 函数来判断。

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

还用一个问题没有解决,那就是循环引用。看例子。

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

可以看到

image.png

解决办法也很简单啊。加入以下代码。

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

最终代码

  1. function extend() {
  2. // 默认不进行深拷贝
  3. let deep = false;
  4. let name, options, src, copy;
  5. let length = arguments.length;
  6. let clone, copyIsArray;
  7. // 记录要复制的对象的下标
  8. let i = 1;
  9. let target = arguments[0];
  10. // 根据 target 的类型不同分别处理。
  11. if (typeof target === "boolean") {
  12. deep = target;
  13. // 如果 target 不是对象类型,而且也不是函数类型,就赋值一个空对象。
  14. if (typeof arguments[i] !== "object" && !isFunction(arguments[i])) {
  15. target = {};
  16. } else {
  17. target = arguments[i];
  18. }
  19. i++;
  20. } else {
  21. // 如果 target 不是对象类型,而且也不是函数类型,就赋值一个空对象。
  22. if (typeof arguments[0] !== "object" && !isFunction(arguments[0])) {
  23. target = {};
  24. } else {
  25. target = arguments[0];
  26. }
  27. }
  28. // 循环遍历要复制的对象们
  29. for (; i < length; i++) {
  30. // 获取当前对象
  31. options = arguments[i];
  32. // 要求不能为空 避免 extend(a,,b) 这种情况
  33. if (options !== null) {
  34. for (name in options) {
  35. // 目标属性值
  36. src = target[name];
  37. // 要复制的对象的属性值
  38. copy = options[name];
  39. if (target === copy) {
  40. continue;
  41. }
  42. // !! 将赋值表达式的副作用转化为 boolean
  43. if (deep && copy && (isPlainObject(copy) || !!(copyIsArray = Array.isArray(copy)))) {
  44. if (copyIsArray) {
  45. copyIsArray = false;
  46. clone = src && Array.isArray(src) ? src : [];
  47. } else {
  48. clone = src && isPlainObject(src) ? src : {};
  49. }
  50. // 递归调用
  51. target[name] = extend(deep, clone, copy);
  52. } else if (copy !== undefined) {
  53. target[name] = copy;
  54. }
  55. }
  56. }
  57. }
  58. return target;
  59. }

参考:

[1] JavaScript专题之深浅拷贝
[2] JavaScript专题之从零实现jQuery的extend