Why `str.length` Works on a Primitive: V8's Prototype Wrapper Trick
JavaScript's Objects, the Mechanism of new, and Prototype Wrapper Classes
A raw string is just a simple value — so why can it call .length? When you call a function with new, it generates a new object — what exactly does the V8 engine do? When a property is not found on an instance, why does it look for it on the constructor? To understand these things, this article should help you. Before starting this study, remember that in JavaScript, everything is an object.
I. A Quick Review
1. Primitive Types
String, Number, Boolean, Null, Undefined, Symbol, BigInt, etc., are all primitive types. Their characteristic is that they are stored in the stack memory, they are just simple values, and you cannot add custom properties or methods to them.
2. Reference Complex Types
Object, Array, Function, Date, etc., are all reference complex types. Their characteristic is that they are stored in the heap memory, and you can freely extend properties and attach methods.
So here's the question: Why can a string, which is a primitive type, call methods, such as str.length?
II. Three Ways to Create Objects
1. Object Literal
const obj = { // object literal
name: 'Qiqi'
}
obj.age = 18
delete obj[hello]
obj.name = 'Dige'
console.log(obj);
The common way we define variables directly is by using object literals. The advantage of this method is that it is concise and intuitive. The disadvantage is obvious: when you need to create multiple similar objects in batches, it generates a lot of repetitive code. Let me mention this: some people see const defining the object obj and then writing obj.name = 'Dige', and they think, "Isn't const supposed to define a constant? It can't be changed!" But think again: in V8's call stack, what does a reference type store? Isn't it the address pointing to the heap? Because changing the content inside the object does not change the address, modifying the content is not a problem — unless you assign a new object to obj, like obj = {}, which is not allowed. From this, we can see that comparison of reference types compares addresses.
2. Native Constructor
const obj = new Object() // In V8's eyes, const obj = {} is actually const obj = new Object()
obj.name = 'Dige'
We also call objects created with new instance objects. If there is no prior function declaration in the code, new Object() means that Object() is a built-in constructor in JavaScript. There are also functions like String() and Number(), but only meaningful types in JavaScript have constructors — undefined does not. You should know that in V8's eyes, const obj = {} and const obj = new Object() are no different.
3. new a Custom Constructor
function Car(){
}
const MyCar = new Car()
console.log(MyCar);
Generally, the first letter of a constructor is capitalized to distinguish it from a regular function. From this, we can also see that functions have a dual nature: a normal call just executes the code, but once you add new, it will definitely produce a brand new object. The output of the above code is an empty object {}.
function Car(){
var i = 1;
console.log(i);
name:'lihua';
age:18
}
const MyCar = new Car()
console.log(MyCar);
So what will be the output if we add some content? Actually, the key-value pairs will be placed inside the instance object MyCar, while other code will execute normally and output, but it won't be inside the instance object — however, it does not affect the instance object. The new method is suitable for scenarios where you need to create instances in batches, reusing the constructor code. This is the purpose of constructors.
III. How new Works
At the beginning of the article, a question was raised: how to understand str.length? From the explanation just given, we know that no matter how you define a variable, V8 will first define it as an object.
let str = 'abcd' // string literal In V8's eyes, it's let str = new String('abcd')
In V8's eyes, whether you define a num or a str, it will first create it as an object via new. Let's put ourselves in V8's perspective.
let str = new String('abcd')
This way, str becomes an instance object. But we know that primitive types are primitive types — string is a primitive type. So V8 will eventually convert the string back to a primitive type. So what exactly do new and V8 do?
1. Create an Empty Object Out of Thin Air
function Car(){
// var this = {}
this.name = 'Benz'
this.price = 1000000;
this.color = 'Red';
this.type = 'Benz';
// return this
}
const MyCar = new Car() // instance object
console.log(MyCar);
Using another example: first, we create this constructor Car. When it is called with new, an empty object this is generated inside the function. this can be roughly seen as this new instance object.
2. Execute the Code in the Function
function Car(){
// var this = {}
this.name = 'Benz'
this.price = 1000000;
this.color = 'Red';
this.type = 'Benz';
// return this
}
const MyCar = new Car() // instance object
console.log(MyCar);
If the function has assignment statements like this.name = 'Benz', the key-value pair name: 'Benz' will be stored in the instance object.
3. Open the Prototype Chain
function Car(){
// var this = {}
this.name = 'Benz'
this.price = 1000000;
this.color = 'Red';
this.type = 'Benz';
// return this
// this.__proto__ = Car.prototype;
}
When creating an instance object, inside the function body, this.__proto__ = Car.prototype; is automatically linked. Every function inherently has a property called prototype, which is an object. Every object also has a property called __proto__, which is also an object. That is, the prototype of the instance object equals the prototype of the function. This can be understood as inheritance. Let's look at str again:
let str = 'abc' // equivalent to let str = new String('abc')
str.name = 'Dige'
console.log(str);
Back to str: new first creates an empty object, then the outer code str.name = 'Dige' runs, but it can never break the rule that primitive types cannot add properties or methods. So before outputting str, V8 performs a delete str.name operation. Finally, is the value output by console.log(str); an object? No. Specifically, for objects constructed from primitive types, there is a [[PrimitiveValue]] — the primitive value stores the primitive type's value, abc.
Figure 3.3 - Internal content of an object created by String
From Figure 3.3, we can see that str.__proto__ = String.prototype;, and [[PrimitiveValue]] is the primitive type's value. When we output console.log(str);, we are essentially outputting str.[[PrimitiveValue]]. Now look at the code below:
let str = new String('abc')
str.length = 4
console.log(str.length);
In Figure 3.3, we know that inside this str instance object, there is indeed a length — the property we added. But it will eventually be deleted by V8. So why does str.length still exist? We know that after the instance object is created, str.__proto__ = String.prototype; is set. So when we look for a property in an object and cannot find it, it will definitely look for it on its prototype. That is, if .length is not found inside str, it will look for it inside the String() constructor.
Figure 3.4 - Content of the String constructor
As shown in Figure 3.4, we can see that there is indeed a key-value pair length: 0. Finally, we find str.length. But what if I insist on modifying the value of str.length? If you want to, you would have to:
let str = new String('abc')
delete String.prototype.length
String.prototype.length = 190999;
console.log(str.length);
We delete the length from the created instance object's prototype and then reassign a new value to length. From this, we can see why string.length exists, and why everything is an object.
IV. Conclusion
If there are any errors or if the content is too shallow, please feel free to correct me. Ciallo~ (∠・ω< )⌒★