What part of ECMAScript spec mandates that String type is immutable?

179 views Asked by At

I know that String primitive type in Javascript is immutable, via practice and word of mouth. But what combination of rules in the ECMA-262 makes it so? More specifically, why the second line in the following example silently does nothing?

const str = 'abc';
str[1] = '8';
console.log(str); // prints unmodified "abc", not "a8c".

Section 6.1.4 describes the internal composition of String datum. It does not contain anything about modifying the data, or at least I couldn't find anything after reading it thrice.

Section 13.15.2 describes the semantics of assignment. It does not contain any exceptions for any specific data type.

Section 13.3.2.1 describes the semantics of the property accessor operator. It does not contain any exceptions for any specific data type.

So, how exactly the string data type is defined to be immutable in Javascript?

3

There are 3 answers

0
Bergi On BEST ANSWER

The EMCAScript specification is silent about this, just as it is silent on object identity (i.e. object mutability). So yes, it's more about "via practice and word of mouth", but I guess a conforming implementation could change the memory representation of a string in the background (or even provide an API to do so as an extension).

However, you can infer the immutability of strings from the phrasing of §6.1.4 "The String Type" that you linked:

The String type is the set of all ordered sequences of zero or more 16-bit unsigned integer values (“elements”) up to a maximum length of 253 - 1 elements.

This is a very mathematical definition, and in mathematics, values are always immutable. In addition, we can observe that there simply is no operation within ECMAScript that would mutate such a value.

The definition applies only to primitive values though, not to String objects which wrap such a value and can have additional (mutable) properties. Again, there just is no operation that would change the [[StringData]] internal slot of a String instance. This is even explicitly described in the section on String Exotic Objects:

A String object is an exotic object that encapsulates a String value and exposes virtual integer-indexed data properties corresponding to the individual code unit elements of the String value. String exotic objects always have a data property named "length" whose value is the number of code unit elements in the encapsulated String value. Both the code unit data properties and the "length" property are non-writable and non-configurable.

This is also what you observe in your example code. The assignment str[1] = … goes to String's [[DefineOwnProperty]] internal method, which finds that the 1 property is non-writable, rejecting a change. The PutValue abstract operation will then throw an exception in strict mode (or ignore the failure in sloppy mode).

4
Appurist - Paul W On

Edit: I think my original answer was so wrong that it may not be redeemable at all, but since there's a great reply, I don't want to delete it outright. I will attempt to fix it.

I was trying to say that on the left side of an assignment there are only two choices for [ ], object access by key, and destructuring. Strings are objects without keys for each index in the text value. A destructuring expression doesn't fit either, and even then it would be a new expression (value) of undefined, and thus not modify the original string. This may be why there is no error but it appears to do nothing. It modifies a (temporary) value of an expression that isn't stored anywhere.

Section 10.4.3.5 of the standard might be the closest thing to identifying the rule, saying the result is [[Writable]]: false, but since it is still an expression, it represents a value that can still be assigned to something, even though that value will be discarded if not.

Interestingly, let B = 'abc'[1] = 'B' would assign 'B' to B as expected, but not modify the string, regardless of whether it was a string constant or variable. And so let A='abc'; let B = A[1] = 'B' has the same effect. So again, because it is a basic type without keys for [1] in this case, the new expression is more of a new value than targeting the original memory where the string is stored.

Even more interesting: let B = undefined = 'B' does successfully assign 'B' to B. I'm not quite sure how, but I think it's the same case as {}[1] even though undefined is an actual value that could have been assigned to B. I think this means there's an exception for an intermediate value in a sequence of assignments if it evaluates to undefined, and does not pass on the undefined to the next expression on the left.

I think it's the same case as undefined = 'B' not doing anything, but that value on the left-hand side is still 'B', if assigned somewhere. I can predict that calling func(undefined = 3) will in fact pass a 3 to the function.


Original answer: It was just so dead wrong that I've removed it now. Thanks to @hijarian for waking me up with his comment about the [ ] operator.

0
Garrett On

10.4.3 String Exotic Objects says: The integer-indexed data properties that correspond 1:1 to each character of the string (the string's "code unit elements") have [[Writable]]:false.

In non-strict mode, assignment to data properties with attribute { [[Writable]]: false } fails silently (See "Ex 1: Non-strict").

However, in strict mode, simple assignment where the LeftHandSideExpression references a data property with the attribute value { [[Writable]]: false } throws a TypeError. (See Appendix C "The Strict Mode of ECMAScript"). (See "Ex 2: Strict");

"Ex 1: Non-strict:"

(function(){
  'f'[0] = 1;
})();

"Ex 2: Strict:"

(function(){ 
  'use strict';
  'f'[0] = 1; // TypeError.
})();

From discussion: https://www.linkedin.com/feed/update/urn:li:groupPost:7039829-7011351896053456897?commentUrn=urn%3Ali%3Acomment%3A%28groupPost%3A7039829-7011351896053456897%2C7011363968413831171%29&replyUrn=urn%3Ali%3Acomment%3A%28groupPost%3A7039829-7011351896053456897%2C7011742156822380544%29

YouTube: https://www.youtube.com/watch?v=4lR7BLWcqa8