JavaScript字符串是不可变的吗?在JavaScript中是否需要“字符串构建器”?

300

Javascript使用不可变字符串还是可变字符串?我是否需要一个“字符串构建器”?


5
是的,它们是不可变的,您需要一种“字符串构建器”。阅读此http://blog.codeeffects.com/Article/String-Builder-In-Java-Script或此http://www.codeproject.com/KB/scripting/stringbuilder.aspx。 - Kizz
4
有趣,这些例子与我在答案中得出的结论相矛盾。 - Ruan Mendes
10个回答

354
它们是不可变的。你不能像这样改变字符串中的一个字符:var myString = "abbdef"; myString[2] = 'c'。字符串操作方法,如trimslice,会返回新的字符串。
同样地,如果你有两个对同一个字符串的引用,修改其中一个不会影响另一个。
let a = b = "hello";
a = a + " world";
// b is not affected

神话揭秘 - 字符串连接并不

我一直听说过Ash在他的回答中提到的(使用Array.join进行连接更快),所以我想测试一下不同的字符串连接方法,并将最快的方法抽象成一个StringBuilder。我编写了一些测试来验证这是否正确(事实并非如此!)。

这是我认为最快的方法,避免使用push,并使用数组来存储字符串,然后在最后将它们连接起来。

class StringBuilderArrayIndex {
  array = [];
  index = 0;
  append(str) {
    this.array[this.index++] = str 
  }
  toString() {
    return this.array.join('')
  }
}

一些基准测试

  • 在下面的代码片段中阅读测试用例
  • 运行代码片段
  • 点击基准测试按钮运行测试并查看结果

我创建了两种类型的测试

  • 使用数组索引来避免使用Array.push,然后使用Array.join
  • 直接字符串拼接

对于这些测试中的每一个,我循环追加一个常量值和一个随机字符串;

<script benchmark>

  // Number of times to loop through, appending random chars
  const APPEND_COUNT = 1000;
  const STR = 'Hot diggity dizzle';
  
  function generateRandomString() {
    const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    const length = Math.floor(Math.random() * 10) + 1; // Random length between 1 and 10
    let result = '';

    for (let i = 0; i < length; i++) {
      const randomIndex = Math.floor(Math.random() * characters.length);
      result += characters.charAt(randomIndex);
    }
    return result;
  }

  const randomStrings = Array.from({length: APPEND_COUNT}, generateRandomString);
  
  class StringBuilderStringAppend {
    str = '';

    append(str) {
      this.str += str;
    }

    toString() {
      return this.str;
    }
  }
  
  class StringBuilderArrayIndex {
    array = [];
    index = 0;

    append(str) {
      this.array[this.index] = str;
      this.index++;
    }

    toString() {
      return this.array.join('');
    }
  }

  // @group Same string 'Hot diggity dizzle'

  // @benchmark array push & join 
  {
    const sb = new StringBuilderArrayIndex();

    for (let i = 0; i < APPEND_COUNT; i++) {
      sb.append(STR)
    }
    sb.toString();
  }

  // @benchmark string concatenation
  {
    const sb = new StringBuilderStringAppend();

    for (let i = 0; i < APPEND_COUNT; i++) {
      sb.append(STR)
    }
    sb.toString();
  }

  // @group Random strings

  // @benchmark array push & join
  {
    const sb = new StringBuilderArrayIndex();

    for (let i = 0; i < APPEND_COUNT; i++) {
      sb.append(randomStrings[i])
    }
    sb.toString();
  }

  // @benchmark string concatenation
  {
    const sb = new StringBuilderStringAppend();

    for (let i = 0; i < APPEND_COUNT; i++) {
      sb.append(randomStrings[i])
    }
    sb.toString();
  }
  
  
</script>
<script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>

研究结果

如今,所有常用的浏览器在字符串拼接方面的处理能力都有所提升,至少快了两倍。

i-12600k(由Alexander Nenashev添加)

Chrome/117

--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x100000  224  232  254  266  275
    array push & join      3.2x  |  x100000  722  753  757  762  763
Random strings
    string concatenation   1.0x  |  x100000  261  268  270  273  279
    array push & join      5.4x  |   x10000  142  147  148  155  166
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark

Firefox/118

--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x100000  304  335  353  358  370
    array push & join      9.5x  |   x10000  289  300  301  306  309
Random strings
    string concatenation   1.0x  |  x100000  334  337  345  349  377
    array push & join      5.1x  |   x10000  169  176  176  176  180
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark

以下是2023年10月在一台2.4 GHz 8核i9 Mac上的结果。
Chrome
--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x100000  574  592  594  607  613
    array push & join      2.7x  |   x10000  156  157  159  164  165
Random strings
    string concatenation   1.0x  |  x100000  657  663  669  675  680
    array push & join      4.3x  |   x10000  283  285  295  298  311
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark

火狐浏览器

--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x100000  546  648  659  663  677
    array push & join      5.8x  |   x10000  314  320  326  331  335
Random strings
    string concatenation   1.0x  |  x100000  647  739  764  765  804
    array push & join      2.9x  |   x10000  187  188  199  219  231
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark

勇敢

--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x100000  566  571  572  579  600
    array push & join      2.5x  |   x10000  144  145  159  162  166
Random strings
    string concatenation   1.0x  |  x100000  649  658  659  663  669
    array push & join      4.4x  |   x10000  285  285  290  292  300
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark `

Safari
--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x10000   76   77   77   79   82
    array push & join      2.2x  |  x10000  168  168  174  178  186
Random strings
    string concatenation   1.0x  |  x100000  878  884  889  892  903
    array push & join      2.3x  |   x10000  199  200  202  202  204
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark `

歌剧

--------------------------------------------------------------------
Same string 'Hot diggity dizzle'
    string concatenation   1.0x  |  x100000  577  579  581  584  608
    array push & join      2.7x  |   x10000  157  162  165  166  171
Random strings
    string concatenation   1.0x  |  x100000  688  694  740  750  781
    array push & join      4.2x  |   x10000  291  315  316  317  379
--------------------------------------------------------------------
https://github.com/silentmantra/benchmark

1
@RoyTinker Roy,哦Roy,你的测试作弊了,因为你在测试的设置中创建了数组。这是真正的测试,使用不同的字符 http://jsperf.com/string-concat-without-sringbuilder/7 随意创建新的测试用例,但创建数组是测试本身的一部分。 - Ruan Mendes
2
@RoyTinker 是的,任何字符串生成器都需要构建数组。问题是是否需要一个字符串生成器。如果您已经有了字符串数组,则它不是我们正在讨论的有效测试案例。 - Ruan Mendes
1
@JuanMendes - 好的,收到。我的测试假设数组已经存在,但在评估字符串生成器时不能假设这一点。 - Roy Tinker
@JuanMendes 您不是说过“问题在于是否需要使用字符串生成器”的同一人吗?如果您的运行时在幕后执行重新分配,则不需要使用字符串生成器。 - Gabe
1
Exploder 用 D。我看到你的点子了。 :D - Calmarius
显示剩余8条评论

44

来自Rhino Book

在JavaScript中,字符串是不可变的对象,这意味着它们内部的字符不能被更改,并且对字符串进行的任何操作都会实际创建新的字符串。字符串是通过引用而非值分配的。通常情况下,当通过引用分配对象时,通过一个引用对对象所做的更改将通过所有对该对象的其他引用可见。然而,由于字符串无法更改,因此您可以对字符串对象拥有多个引用,而不必担心字符串值会在未知情况下更改。


8
以下是需要翻译的内容:Link to an appropriate section of the rhino book: http://books.google.com/books?id=VOS6IlCsuU4C&pg=PA47&vq=strings+immutable&dq=javascript&client=firefox-a&source=gbs_search_s&cad=0 - baudtack
142
犀牛书的引用(因此也包括这个答案)在这里是错误的。在JavaScript中,字符串是原始值类型而不是对象(规范)。实际上,从ES5开始,它们是仅有的5种值类型之一,与nullundefinednumberboolean并列。字符串是通过_值_分配而不是通过引用传递的。因此,字符串不仅是不可变的,而且是一个_值_。将字符串“hello”更改为“world”,就像决定从现在开始数字3是数字4一样……这毫无意义。 - Benjamin Gruenbaum
12
就像我的评论所说,字符串是不可变的,但它们既不是引用类型也不是对象 - 它们是原始值类型。一个简单的方法来看出它们都不是,就是尝试给一个字符串添加属性然后读取它:var a = "hello"; var b=a; a.x=5; console.log(a.x,b.x); - Benjamin Gruenbaum
12
不,使用字符串构造函数创建的“String”对象是JavaScript字符串值的包装器。您可以使用.valueOf()函数访问包装类型的字符串值 - 对于Number对象和数字值也是如此。需要注意的是,使用new String创建的“String”对象不是实际的字符串,而是字符串的包装器或盒子。请参见http://es5.github.io/#x15.5.2.1。有关内容如何转换为对象,请参见http://es5.github.io/#x9.9。 - Benjamin Gruenbaum
8
为什么有些人会说字符串是对象?可能是因为他们来自使用术语“对象”表示任何类型数据(包括整数)的编程语言,如Python或Lisp。他们只需要阅读ECMA规范如何定义“对象”的内容:“属于Object类型的成员”。此外,即使是“值”一词在不同语言的规范中也可能有不同的含义。 - Jisang Yoo
显示剩余6条评论

26

为了对像我这样的简单脑袋进行澄清(来自MDN):

不可变对象是指对象在创建后其状态不能更改。

字符串和数字是不可变的。

不可变意味着:

您可以将变量名称指向新值,但以前的值仍然保留在内存中。因此需要垃圾回收。

var immutableString = "Hello";

// 在上面的代码中,创建了一个具有字符串值的新对象。

immutableString = immutableString + "World";

// 我们现在正在将“World”附加到现有值上。

这似乎是我们正在改变字符串“immutableString”的内容,但实际上并非如此。相反:

附加字符串值“immutableString”会触发以下事件:

  1. 检索“immutableString”的现有值
  2. 将“World”附加到“immutableString”的现有值上
  3. 将结果值分配给新的内存块
  4. “immutableString”对象现在指向新创建的内存空间
  5. 以前创建的内存空间现在可以进行垃圾回收。

如果你这样做 var immutableString = "Hello"; immutableString="world",那么结果会是一样的吗?我的意思是给变量分配一个全新的值。 - user6791921
当然,你可以这样做。 - Katinka Hesselink
谢谢Katinka,但我的意思是,如果您分配一个全新的值,您是否会“改变字符串”?还是适用于您在这里解释得很好的相同内容?如果看起来您正在突变它,但实际上并没有... - user6791921
3
你正在用一个新的记忆字段替换它。旧的字符串被丢弃。 - Katinka Hesselink

24

性能提示:

如果你需要连接大型字符串,请将字符串部分放入数组中,并使用 Array.Join() 方法获取整个字符串。对于连接大量字符串,这可以比直接连接快很多。

在 JavaScript 中没有StringBuilder


我知道.NET框架中没有stringBuilder,但msAjax有一个,我在思考它是否有用。 - DevelopingChris
7
这与字符串是不可变的有什么关系? - baudtack
4
由于字符串是不可变的,所以字符串拼接需要创建比使用Array.join方法更多的对象。 - Ruan Mendes
9
根据Juan的上述测试结果,在IE和Chrome中,字符串拼接实际上更快,而在Firefox中则更慢。 - Bill Yang
10
请考虑更新您的答案,它可能在很久以前是正确的,但现在已经不再正确了。请参阅http://jsperf.com/string-concat-without-sringbuilder/5。 - Ruan Mendes

5

字符串类型的值是不可变的,但是使用String()构造函数创建的String对象是可变的,因为它是一个对象,你可以向其中添加新属性。

> var str = new String("test")
undefined
> str
[String: 'test']
> str.newProp = "some value"
'some value'
> str
{ [String: 'test'] newProp: 'some value' }

同时,虽然你可以添加新属性,但不能更改已存在的属性。
Chrome控制台测试截图如下: 总之,1. 所有字符串类型值(原始类型)都是不可变的。2. 字符串对象是可变的,但它包含的字符串类型值(原始类型)是不可变的。

Javascript的字符串对象是不可变的。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Data_structures - prasun
1
@prasun 但是在那个页面上它说:“除对象以外的所有类型都定义了不可变值(即不能改变的值)。”字符串对象是对象。如果您可以在其上添加新属性,那么它如何是不可变的? - zhanziyang
请阅读“字符串类型”部分。JavaScript的String链接指向原始类型和对象,后面又说“JavaScript字符串是不可变的”。看起来这个文档在这个主题上并不清楚,因为它在两个不同的注释中存在冲突。 - prasun
7
new String 会在不可变字符串周围生成一个可变的包装器。 - tomdemuyt
2
通过运行@zhanziyang上面的代码非常容易进行测试。你完全可以向String对象(包装器)添加新属性,这意味着它_不是_不可变的(默认情况下;像任何其他对象一样,您可以调用Object.freeze使其不可变)。但是,包含在String对象包装器中或不包含在其中的原始字符串值类型始终是不可变的。 - Mark Reed
尽管这是真的,但它与问题并不相关。当然,有一个可变的字符串包装器用于不可变字符串,您可以修改它,但一旦它再次取消包装,任何修改都会消失。最糟糕的是这是误导性的,最好的情况下是无用的信息。问题是我们是否修改实际字符串,而不是向包装对象添加无用属性。 - Ruan Mendes

3

字符串是不可变的 - 它们不能改变,我们只能创建新字符串。

例如:

var str= "Immutable value"; // it is immutable

var other= statement.slice(2, 10); // new string

1
这是一篇晚发的帖子,但我没有在答案中找到好的书摘引用。
以下是一本可靠书籍的明确摘录: “在ECMAScript中,字符串是不可变的,这意味着一旦它们被创建,它们的值就不能改变。要更改变量保存的字符串,必须销毁原始字符串,并用包含新值的另一个字符串填充变量...——《专业JavaScript for Web Developers, 3rd Ed.,p.43》”
现在,引用Rhino书摘的答案关于字符串不可变性是正确的,但说“字符串是按引用而非按值分配的”是错误的。(可能他们最初想把这些词放反了)
在“Professional JavaScript”一章中名为“基本类型和引用类型”的文章中澄清了“引用/值”的误解: “五种基本类型...[是]:未定义、Null、布尔、数字和字符串。这些变量被称为按值访问,因为您正在操作存储在变量中的实际值。——《专业JavaScript for Web Developers, 3rd Ed.,p.85》”
这与对象相反:
当您操作一个对象时,实际上是在处理对该对象的引用,而不是对象本身。因此,这样的值被称为通过引用访问。——《JavaScript高级程序设计》第三版,第85页

顺便说一句:犀牛书可能意味着在字符串赋值时内部/实现存储/复制的是指针(而不是复制字符串的内容)。根据之后的文本,这似乎不是他们的失误。但我同意:他们滥用了“按引用传递”的术语。仅仅因为实现使用指针(为了提高性能),并不意味着它是“按引用传递”的。维基百科-求值策略是关于这个主题的有趣阅读材料。 - ToolmakerSteve

1
关于您在Ash的回复中提到的ASP.NET Ajax中的StringBuilder的问题,专家们似乎对此持不同意见。
Christian Wenz在他的书《Programming ASP.NET AJAX》(O'Reilly)中说:“这种方法对内存没有任何可测量的影响(实际上,实现似乎比标准方法慢一点)。”
另一方面,Gallo等人在他们的书《ASP.NET AJAX in Action》(Manning)中说:“当要连接的字符串数量较大时,字符串构建器成为避免性能下降的必要对象。”
我想您需要进行自己的基准测试,而且结果在不同的浏览器之间可能会有所不同。然而,即使它不能改善性能,对于那些习惯于在像C#或Java这样的语言中使用StringBuilders编码的程序员来说,它仍然可能被认为是“有用”的。

0

JavaScript字符串确实是不可变的。


文档链接在哪里?我找不到具体内容。 - DevelopingChris
1
尝试设置单个字符是行不通的。 - Ken

0
JavaScript 中的字符串是不可变的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接