Home > OS >  Using underscores in classes in Javascript
Using underscores in classes in Javascript

Time:08-28

I've read that the underscore in js is an acceptable naming character with no additional functionality, used to signify something is private. My question is how does this work then?

class User {
    constructor(name) {
        this.name = name
    }

    get name() {
        return this._name
    }

    set name(value) {
        if (value.length < 4) {
            alert("Name is too short")
            return
        }
        this._name = value
    }
}

let user = new User("Kiki")
alert(user.name)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

The variable is not defined the first time with the underscore, how does JavaScript know what I'm referring to?

Also if I remove the underscores I get an error that

InternalError: too much recursion

I don't understand why? If the underscore has no functionality, shouldn't it work the same without the underscores?

CodePudding user response:

The underscore is just a commonly-used way of remembering that the property is private. You can use any variable name you want!

class User {
    constructor(name) {
        this.privateName = name
    }

    get name() {
        return this.privateName
    }

    set name(value) {
        if (value.length < 4) {
            alert("Name is too short")
            return
        }
        this.privateName = value
    }
}

let user = new User("Kiki")
console.log(user.name)

That being said, in the latest javascript you can use the # to make private properties that are actually private (not reachable from outside the class)

class User {

  #name

  constructor(name) {
    this.#name = name
  }

  get name() {
    return this.#name
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short")
      return
    }
    this.#name = value
  }
}

let user = new User("Henk")
console.log(user.name)


   

CodePudding user response:

Your working version has two properties:

  • _name, a data property you create in the constructor
  • name, an accessor property you create on the class prototype via get name() { /*...*/ } (and set name(value) { /*...*/ })

In this code:

let user = new User("Kiki")
alert(user.name)

...when you use user.name, you're using the accessor property's getter function (get name), which returns the value of this._name.

If you remove the _, you have only one property, which is the accessor property, and assigning to this.name in the constructor calls the accessor's setter function, which (in that version) would do this.name = ____, which would call the setter function, which would do this.name = ____, which would call the setter function...

If you're going to have that public name property that does validation on the value you assign to it, you need a place to keep the value the accessor accesses. That can be another public property with a different name (_name), which is fine though easily bypassed; or in modern code you might use a private instance field instead:

class User {
    #name; // Private instance field declaration

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

    get name() {
        return this.#name;
    }

    set name(value) {
        if (value.length < 4) {
            console.log("Name is too short");
            return;
        }
        this.#name = value;
    }
}

let user = new User("Kiki");
console.log(user.name);

Side note: I'd make the error condition throw an Error, not just output a message, not least so that new User("x") doesn't leave the name with the default value undefined:

set name(value) {
    if (value.length < 4) {
        throw new Error(`Name is too short, expected at least 4 chars, got ${value.length}`);
    }
    this.#name = value;
}

In a comment you've asked:

...if I put this._name=name in the constructor it works as well. Would that be better practice?

That would mean that you wouldn't check the length of the name passed to the constructor. But your instinct is good! In class-based OOP circles, best practice is usually not to call setters (or other public methods) from constructors, because the setter/method may be overridden in a derived class, and the derived class may expect that the setter is working with a fully-initialized instance. But when you all a setter from the constructor, the instance may not be fully initialized yet.

In your specific case it doesn't really matter, there's only one property (conceptually), but the usual way to avoid the issue with overridden setters/methods is to use a private function that does the work of the setter, and use that function both in the setter and the constructor:

class User {
    #name;

    constructor(name) {
        this.#setName(name);
    }

    #setName(value) { // <== Private instance method
        if (value.length < 4) {
            throw new Error(`Name is too short, expected at least 4 chars, got ${value.length}`);
        }
        this.#name = value;
    }

    get name() {
        return this.#name;
    }

    set name(value) {
        this.#setName(value);
    }
}

try {
    let user = new User("Kiki");
    console.log(user.name);
} catch (error) {
    console.error("ERROR, the first call should work");
}

try {
    let user = new User("X");
    console.log(user.name);
    console.error("ERROR, the second call should fail");
} catch (error) {
    console.log(`Second call failed as expected: ${error.message}`);
}


try {
    let user = new User("Kiki");
    console.log(user.name);
    user.name = "x";
    console.error("ERROR, the third call should fail");
} catch (error) {
    console.log(`Third call failed as expected: ${error.message}`);
}

Since a private method can't be overridden in a derived class, it's okay to call it from the constructor.

  • Related