javascriptstack-overflowes6-classaccessorclass-fields

I expect my User Class to have an internal recursion error using accessors but it didn't. Why?


Before we begin, I'm running on a webpack-dev-server, and I'm building a to do list app. My goal is to improve my understanding of how accessors behave so that I can standardizing my writing style with classes.

Now the snippet below is how I set up my class User:

export class User {
    name; //public field

    constructor(name, id = crypto.randomUUID(), xp = 0) {
        this.id = id;
        this.name = name;
        Object.assign(this, createExpHolder(xp), createProjectHolder(), createTaskHolder());
    }

    get name() {
        return this.name;
    }

    set name(name) {
        this.name = name;
    }

}
const user = new User("StackOverflow");
console.log(user.name); //prints StackOverflow and no errors

At first glance, you would have thought that this code will return an error due to stack overflow. And I'm surprised it didn't. Whenever I mutate the instance's name it doesn't trigger an error, and it works fine! Is it because of the dev server? Does webpack handle it? Is this a quirk of the JavaScript engine?

I've done my research for finding explanation in MDN, and bunch of AIs to explain it for me but the solution/response they gave was to use private field or the underscore naming convention.

This is a problem for me because I want to standardize my writing style with classes. And if I've grown accustomed to this setup, I might have a technical debt in the future.


Solution

  • You're not running into a StackOverflow error, because you declare the public field name, which is created on the instance before the constructor body runs. So when you do this.name = name in the constructor, you're just assigning to that own data property. Because the instance already has a data property named name, it masks the get name() / set name() accessors that live on the prototype. If you comment out / remove the declaration, you will indeed run into an error:

    export class User {
        constructor(name, id = crypto.randomUUID(), xp = 0) {
            this.id = id;
            this.name = name;
            Object.assign(this, 0, 0, 0);
        }
    
        get name() {
            return this.name;
        }
    
        set name(name) {
            this.name = name;
        }
    
    }
    const user = new User("StackOverflow");
    console.log(user.name);
    
    // Line 15: Uncaught RangeError: Maximum call stack size exceeded
    //    at set name (<anonymous>:15:19)
    // [...]
    

    I suggest declaring a private name field using #name

    class User {
        #name; //private field
    
        constructor(name, id = crypto.randomUUID(), xp = 0) {
            this.id = id;
            this.#name = name;
        }
    
        get name() {
            return this.#name;
        }
        
        /*set name(name) {
           this.#name = name;
        }*/
    
    }
    const user = new User("StackOverflow");
    console.log(user.name);
    user.name = "Brentspine" // Only works when setter is declared, read below
    console.log(user.name);

    In strict mode, trying to use user.name = "..." without a defined setter, it throws a TypeError; in non-strict mode it’s silently ignored.

    When trying to access the name using the # like in the following snippet, you would get a SyntaxError

    console.log(user.#name); 
    user.#name = "..."
    

    While trying to to access the field user["#name"] would return undefined, as it would treat #name as its own property name different to name

    console.log(user["#name"]); // Would log undefined
    user["#name"] = "Brentspine"
    console.log(user["#name"]); // Would log "Brentspine" 
    console.log(user.name);     // Would log initial value
    

    Btw: Private fields will not appear in Object.keys, for…in or JSON.stringify

    Fiddle