【问题标题】:How to force JavaScript to deep copy a string?如何强制 JavaScript 深度复制字符串?
【发布时间】:2015-10-21 03:55:29
【问题描述】:

我有一些 javascript 代码,如下所示:

var myClass = {
  ids: {}
  myFunc: function(huge_string) {
     var id = huge_string.substr(0,2);
     ids[id] = true;
  }
}

稍后,该函数被一些大字符串 (100 MB+) 调用。我只想保存在每个字符串中找到的短 id。然而,谷歌浏览器的子字符串函数(实际上是我的代码中的正则表达式)只返回一个“切片字符串”对象,它引用了原始对象。因此,在对myFunc 进行了一系列调用后,我的 chrome 选项卡内存不足,因为临时的 huge_string 对象无法被垃圾回收。

如何复制字符串id,以便不维护对huge_string 的引用,并且可以对huge_string 进行垃圾回收?

【问题讨论】:

  • “子字符串函数(实际上是我代码中的正则表达式)只返回一个“切片字符串”对象,它引用了原始对象” - 嗯? .substr().substring().slice() 和相关的正则表达式函数都返回一个 new 字符串。调用myClass.myFunc() 的其他代码是否保留了对您的巨大字符串的引用?如果您的真实代码更复杂,是否会不小心将巨大的字符串保留在闭包中?
  • @nnnnnn 无法判断它是否是来自 JavaScript 的“新”字符串 data;实现可以共享底层数据而不违反 ECMAScript 的任何部分。 Firefox 有六个different string implementations(特别是参见 JSDependentString),如果 Chrome 有类似的优化(在某些边缘情况下可能表现不佳),我并不感到惊讶。话虽这么说..如果这是一条红鲱鱼,我不会感到非常惊讶。
  • 这个bug report #2869 包含一个工作:(' ' + src).slice(1)。没有正式的决议。
  • 我在将脚本转换为“use strict;”时遇到了这个问题我们正在写入一个现在只读的字符串文字并获得“无法分配给字符串的只读属性'0'”。

标签: javascript google-chrome memory-management garbage-collection


【解决方案1】:

JavaScript 的 ECMAScript 实现因浏览器而异,但对于 Chrome,许多字符串操作(substr、slice、regex 等)只是保留对原始字符串的引用,而不是复制字符串。这是 Chrome (Bug #2869) 中的一个已知问题。要强制复制字符串,可以使用以下代码:

var string_copy = (' ' + original_string).slice(1);

此代码通过在字符串前面附加一个空格来工作。这种连接会在 Chrome 的实现中产生一个字符串副本。那么空格后面的子串就可以被引用了。

解决方案的这个问题已经在这里重新创建:http://jsfiddle.net/ouvv4kbs/1/

警告:加载需要很长时间,打开 Chrome 调试控制台查看进度打印输出。

// We would expect this program to use ~1 MB of memory, however taking
// a Heap Snapshot will show that this program uses ~100 MB of memory.
// If the processed data size is increased to ~1 GB, the Chrome tab
// will crash due to running out of memory.

function randomString(length) {
  var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  var result = '';
  for (var i = 0; i < length; i++) {
    result +=
        alphabet[Math.round(Math.random() * (alphabet.length - 1))];
  }
  return result;
};

var substrings = [];
var extractSubstring = function(huge_string) {
  var substring = huge_string.substr(0, 100 * 1000 /* 100 KB */);
  // Uncommenting this line will force a copy of the string and allow
  // the unused memory to be garbage collected
  // substring = (' ' + substring).slice(1);
  substrings.push(substring);
};

// Process 100 MB of data, but only keep 1 MB.
for (var i =  0; i < 10; i++) {
  console.log(10 * (i + 1) + 'MB processed');
  var huge_string = randomString(10 * 1000 * 1000 /* 10 MB */);
  extractSubstring(huge_string);
}

// Do something which will keep a reference to substrings around and
// prevent it from being garbage collected.
setInterval(function() {
  var i = Math.round(Math.random() * (substrings.length - 1));
  document.body.innerHTML = substrings[i].substr(0, 10);
}, 2000);

【讨论】:

  • var string_copy = original_string.slice(0);
  • @WesleyStam 我认为 AffluentOwl 的帖子有效的原因是他在字符串前面添加了一个字符,这会导致字符串被复制,因为切片运算符实际上并没有像它那样复制字符串应该。
  • 谢谢你 - var string_copy = (' ' + original_string).slice(1);我正在从 html 编辑器复制文本并将其写在它旁边,然后在循环中自动保存它。我想知道为什么复制文本然后更改副本会改变原件 - 然后我想到 - 这是一个参考!
  • 我尝试使用您的代码并生成一个 JS Benchmark 来比较这里建议的各种操作。 jsben.ch/aYDBc 看来这可能是最好的解决方案。根据基准,这里提出的其他解决方案(插值/重复/等)似乎不太可能真正复制到所有浏览器。
  • 不错的基准测试,但在我的测试中,插值和重复(1)方法实际上并没有释放保留的内存。
【解决方案2】:

不确定如何测试,但使用字符串插值创建新的字符串变量是否有效?

newString = `${oldString}`

【讨论】:

  • @Qwertiy 你为什么说这行不通?似乎对我有用。从上面运行命令后,我更改了oldString,它并没有更改newString。此外,typeof 为两者返回了原始字符串类型。
  • 这绝对有效并且性能非常好。在 4K 长度的字符串上进行测试,平均性能约为 0.004 毫秒。很多时候,执行大约需要 0.001 毫秒。这是我在控制台中运行的测试:!function () { const outputArr = []; const chars = 'ABC'; while(outputArr.length &lt; 4000) { outputArr.push( chars[Math.floor(Math.random() * chars.length)])} const output = outputArr.join(''); console.time('interpolation'); const newVariable = `${output}`; console.timeEnd('interpolation'); }();
  • 据我所知,这在 Chrome 中不起作用,因为它实际上并没有释放保留的内存。 imgur.com/a/xAg8ORK
【解决方案3】:

我对字符串、对象、数组等使用Object.assign() 方法:

const newStr = Object.assign("", myStr);
const newObj = Object.assign({}, myObj);
const newArr = Object.assign([], myArr);

请注意,Object.assign 仅复制对象内的键及其属性值(仅一级)。对于嵌套对象的深度克隆,请参考以下示例:

let obj100 = { a:0, b:{ c:0 } };
let obj200 = JSON.parse(JSON.stringify(obj100));
obj100.a = 99; obj100.b.c = 99; // No effect on obj200

【讨论】:

  • 看起来不像预期的结果:i.stack.imgur.com/1hsxF.png
  • 当我执行Object.assign("", "abc"); 时,我得到一个空字符串对象。
  • const newStr = Object.assign("", myStr); console.log(newStr); 这将打印一个数组:[String: ''] {'0': 'H','1': 'e',...}]。不幸的是不适用于字符串复制。
  • 我最喜欢这个解决方案的外观,但不幸的是它在 Chrome 中对我不起作用,我求助于看起来更“黑客”的字符串复制和切片解决方案
【解决方案4】:

你可以使用:

 String.prototype.repeat(1) 

它似乎运作良好。请参阅MDN documentation on repeat

【讨论】:

  • var a = "hi"; var b = a.repeat(1); 为我工作。我尝试更改 ab 保持不变。
  • 最简单的解决方案??
  • 在我的测试中,Chrome 目前实际上并没有在repeat 上进行复制。
  • 据我所知,就像插值一样,这在 Chrome 中不起作用,因为它实际上并没有释放保留的内存。 imgur.com/a/uitL8Dv
【解决方案5】:

推入数组时遇到问题。每个条目都会以相同的字符串结束,因为它引用了一个对象上的值,该值在我通过 .next() 函数迭代结果时发生了变化。这是允许我复制字符串并在我的数组结果中获取唯一值的原因:

while (results.next()) {
  var locationName = String(results.name);
  myArray.push(locationName);
}

【讨论】:

    【解决方案6】:

    在这里看到一些回复很有趣。 如果您不担心旧版浏览器支持 (IE6+),请直接跳到插值方法,因为它非常高效。

    一种最向后兼容(回到 IE6)并且仍然非常高效的按值复制字符串的方法是将其拆分为一个新数组,然后立即以字符串的形式重新加入该新数组:

    let str = 'abc';
    let copiedStr = str.split('').join('');
    console.log('copiedStr', copiedStr);
    

    幕后花絮

    上面所做的是调用 JavaScript 以不使用任何字符作为分隔符来拆分字符串,这会将每个单独的字符拆分为新创建的数组中自己的元素。这意味着,在短时间内,copiedStr 变量看起来像这样:

    ['a', 'b', 'c']
    

    然后,copiedStr 变量立即重新连接,在每个元素之间不使用任何字符作为分隔符,这意味着新创建的数组中的每个元素都被推回一个全新的字符串,有效地复制了字符串。

    执行结束时,copiedStr是自己的变量,输出到控制台:

    abc
    

    性能

    平均而言,这在我的机器上大约需要 0.007 毫秒 - 0.01 毫秒,但您的里程可能会有所不同。在一个 4000 个字符的字符串上进行测试,这种方法复制字符串的时间最长为 0.2 毫秒,平均大约为 0.14 毫秒,因此它仍然具有可靠的性能。

    谁在乎 Legacy 支持?/插值方法

    但是,如果您不担心旧版浏览器支持,那么 Pirijan 的答案之一中提供的 interpolation 方法是一个非常高效且易于复制的字符串:

    let str = 'abc';
    let copiedStr = `${str}`;
    

    在相同的 4,000 个字符长度的字符串上测试 interpolation 的性能,我看到平均为 0.004 毫秒,最大为 0.1 毫秒,最小为惊人的 0.001 毫秒(非常频繁)。

    【讨论】:

    • 在这个问题的标记答案中,有没有理由相信这比 .slice(1) 方法更高效?还是您只是因为喜欢它的语法糖而提倡这种方法?
    • 分割连接方法比 .slice(1) 方法慢了大约 0.05 毫秒。我从来没有说过它比那个方法更有效。我只是给出了另一种方法并对其进行了性能测试。但是插值让他们都击败了,无论如何=]
    【解决方案7】:

    使用 String.slice()

    const str = 'The quick brown fox jumps over the lazy dog.';
    
    // creates a new string without modifying the original string
    const new_str = str.slice();
    
    console.log( new_str );

    【讨论】:

      【解决方案8】:

      我通常使用strCopy = new String (originalStr); 出于某种原因不建议这样做吗?

      【讨论】:

      • 尝试在上面运行typeof。它为您提供了一个 String 类型的实例,而不是提供更多功能的 String 原语。话虽如此,将其作为strCopy = String(originalStr); 之类的函数运行可能会起作用。参考:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
      • 另外,刚刚测试了一下,尝试做strCopy = String(originalStr);,然后修改原来的字符串做strCopy[0] = "X"。两个副本都将被修改。
      【解决方案9】:

      我遇到了这个问题,我就是这样解决的:

      let copy_string = [];
      copy_string.splice(0, 0, str);
      

      我相信这会将 str 深度复制到 copy_string。

      【讨论】:

      • 虽然从技术上讲,str 变量将被推入 copy_string,但 copy_string 是一个数组,因此您必须使用以下内容完成此操作:const copiedVariable = copy_string.join('') 将数组拉到一起回到一个字符串。
      猜你喜欢
      • 1970-01-01
      • 2012-03-14
      • 2013-05-04
      • 2020-05-01
      • 1970-01-01
      • 2012-07-03
      • 2011-10-06
      • 1970-01-01
      • 2012-08-12
      相关资源
      最近更新 更多