Posts JS拷贝
Post
Cancel

JS拷贝

浅拷贝 / 深拷贝:

  • clone-deep
  • JSON.stringify
  • 尤雨溪的 circular-json-es6
  • @jsmini/clone

浅拷贝

浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响

  • Object.assign
  • 展开运算符(…)

clone-deep

深拷贝需要考虑:

  • JSON 克隆不支持函数、引用、undefined、Date、RegExp 等
  • 递归克隆要考虑环
  • 要考虑 Date、RegExp、Function 等特殊对象的克隆方式
  • 要不要克隆 proto,如果要克隆,就非常浪费内存;如果不克隆,就不是深克隆

所以一般说的的深拷贝都是浅的,自己实现是很复杂可以考虑用个库 clone-deep

cloneDeep.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const cloneDeep = (obj) => {
  if (!obj) {
    return obj;
  }

  let res;
  if (Array.isArray(obj)) {
    res = [];
  } else if (typeof obj === "object") {
    res = {};
  }
  for (const item in obj) {
    const val = obj[item];
    if (typeof val === "object") {
      res[item] = cloneDeep(val);
    } else {
      res[item] = val;
    }
  }
  return res;
};

JSON.stringify

  • 对象如果引用了自身的话是不能够直接 JSON.stringify 的,可以传入第二个参数 getCircularReplacer 函数解决
  • 当值为函数、undefined、或 symbol 时,无法拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
function getCircularReplacer() {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return;
      }
      seen.add(value);
    }
    return value;
  };
}

window.postMessage(
  JSON.stringify(
    {
      type: BlockErrorActions.ON_BLOCK_ERROR,
      payload: {
        error_entity: origin_error_entity_id,
        block_id: error_block_id,
        heart_error: e,
      },
    },
    getCircularReplacer()
  ),
  "*"
);

circular-json-es6

尤雨溪在知乎上有个回答,说了一下这个问题:

  • 你任意对象的深度克隆,edge case 非常多,比如原生 DOM/BOM 对象怎么处理,RegExp 怎么处理,函数怎么处理,原型链怎么处理… 并不是一个简单的问题。
  • 大部分时候 deep clone 的用例都是在数据结构的持久化上,换句话说应该是可以被序列化/反序列化的数据。数据类型只包含 JSON 支持的类型的话就好办了,加上循环引用支持就行了。

他写了一个方案 circular-json-es6,代码十分的优雅。(https://github.com/yyx990803/circular-json-es6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
function encode(data, replacer, list, seen) {
  var stored, key, value, i, l;
  var seenIndex = seen.get(data);
  if (seenIndex != null) {
    return seenIndex;
  }
  var index = list.length;
  if (isPlainObject(data)) {
    stored = {};
    seen.set(data, index);
    list.push(stored);
    var keys = Object.keys(data);
    for (i = 0, l = keys.length; i < l; i++) {
      key = keys[i];
      value = data[key];
      if (replacer) {
        value = replacer.call(data, key, value);
      }
      stored[key] = encode(value, replacer, list, seen);
    }
  } else if (Array.isArray(data)) {
    stored = [];
    seen.set(data, index);
    list.push(stored);
    for (i = 0, l = data.length; i < l; i++) {
      value = data[i];
      if (replacer) {
        value = replacer.call(data, i, value);
      }
      stored[i] = encode(value, replacer, list, seen);
    }
  } else {
    index = list.length;
    list.push(data);
  }
  return index;
}

function decode(list, reviver) {
  var i = list.length;
  var j, k, data, key, value;
  while (i--) {
    var data = list[i];
    if (isPlainObject(data)) {
      var keys = Object.keys(data);
      for (j = 0, k = keys.length; j < k; j++) {
        key = keys[j];
        value = list[data[key]];
        if (reviver) value = reviver.call(data, key, value);
        data[key] = value;
      }
    } else if (Array.isArray(data)) {
      for (j = 0, k = data.length; j < k; j++) {
        value = list[data[j]];
        if (reviver) value = reviver.call(data, j, value);
        data[j] = value;
      }
    }
  }
}

function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === "[object Object]";
}

exports.stringify = function stringify(data, replacer, space) {
  try {
    return arguments.length === 1
      ? JSON.stringify(data)
      : JSON.stringify(data, replacer, space);
  } catch (e) {
    return exports.stringifyStrict(data, replacer, space);
  }
};

exports.parse = function parse(data, reviver) {
  var hasCircular = /^\s/.test(data);
  if (!hasCircular) {
    return arguments.length === 1
      ? JSON.parse(data)
      : JSON.parse(data, reviver);
  } else {
    var list = JSON.parse(data);
    decode(list, reviver);
    return list[0];
  }
};

exports.stringifyStrict = function (data, replacer, space) {
  var list = [];
  encode(data, replacer, list, new Map());
  return space
    ? " " + JSON.stringify(list, null, space)
    : " " + JSON.stringify(list);
};

@jsmini/clone

原文地址:https://yanhaijing.com/javascript/2018/10/10/clone-deep/

可以生成指定深度和每层广度的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createData(deep, breadth) {
  const data = {};
  const temp = data;

  for (let i = 0; i < deep; i++) {
    temp = temp["data"] = {};
    for (let j = 0; j < breadth; j++) {
      temp[j] = j;
    }
  }

  return data;
}

createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}

简易深拷贝:

1
2
3
function cloneJSON(source) {
  return JSON.parse(JSON.stringify(source));
}

爆栈、循环引用

1
2
3
4
5
6
7
// 爆栈
cloneJSON(createData(10000)); // Maximum call stack size exceeded

// 循环引用
const a = {};
a.a = a;
cloneJSON(a); // Uncaught TypeError: Converting circular structure to JSON

cloneLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const a = {
  a1: 1,
  a2: {
    b1: 1,
    b2: {
      c1: 1
    }
  }
}

    a
  /   \
 a1   a2
 |    / \
 1   b1 b2
     |   |
     1  c1
         |
         1

用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function cloneLoop(x) {
  const root = {};

  // 栈
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x,
    },
  ];

  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== "undefined") {
      res = parent[key] = {};
    }

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === "object") {
          // 下一次循环
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}

cloneForce

假如一个对象 a,a 下面的两个键值都引用同一个对象 b,经过深拷贝后,a 的两个键值会丢失引用关系,从而变成两个不同的对象

1
2
3
4
5
6
7
const b = {};
const a = { a1: b, a2: b };

a.a1 === a.a2; // true

var c = clone(a);
c.a1 === c.a2; // false

引入一个数组 uniqueList 用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在 uniqueList 中了,如果在的话就不执行拷贝逻辑了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function cloneForce(x) {
  const uniqueList = []; // 用来去重

  let root = {};

  // 循环数组
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x,
    },
  ];

  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== "undefined") {
      res = parent[key] = {};
    }

    // 数据已经存在
    let uniqueData = find(uniqueList, data);
    if (uniqueData) {
      parent[key] = uniqueData.target;
      continue; // 中断本次循环
    }

    // 数据不存在,保存源数据,在拷贝数据中对应的引用
    uniqueList.push({
      source: data,
      target: res,
    });

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === "object") {
          // 下一次循环
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}

function find(arr, item) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i].source === item) {
      return arr[i];
    }
  }

  return null;
}

runTime

可以通过一个 runTime 函数检测:

1
2
3
4
5
6
7
8
9
10
function runTime(fn, time) {
  const stime = Date.now();
  let count = 0;
  while (Date.now() - stime < time) {
    fn();
    count++;
  }

  return count;
}

一般性能优化是在遇到瓶颈的时候才去进行,有句话叫做:“先抗住,再优化”。如果只是少量数据的数据持久化的话,可能直接 JSON.stringify 就完事了。

This post is licensed under CC BY 4.0 by the author.
Trending Tags
Contents

Trending Tags