New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updated and added pages for private class features #5690
Changes from 4 commits
58ba52e
3b80848
8f6fce4
9b6813c
75cba58
ce83125
a93f017
c175416
920b5bd
937f6d8
b697ec8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
--- | ||
title: Working with private class features | ||
slug: Web/JavaScript/Guide/Working_With_Private_Class_Features | ||
tags: | ||
- Document | ||
- Guide | ||
- JavaScript | ||
browser-compat: javascript.classes.private_class_fields | ||
--- | ||
<div>{{jsSidebar("JavaScript Guide")}}</div> | ||
|
||
<p class="summary">It’s common to want to make fields or methods private, but JavaScript has lacked such a feature since its inception. Conventions have arisen — such as prefixing fields and methods that should be treated as private with an underscore, like <code>_hidden</code> — but these are merely conventions. The underscored features are still fully public.</p> | ||
|
||
<p>Private class features deliver truly private fields and methods, with that privacy enforced by the language instead of convention. This confers benefits such as avoiding naming collisions between class features and the rest of the code base, and allowing classes to expose a very small interface to the rest of the code.</p> | ||
|
||
|
||
<h2>Private fields</h2> | ||
|
||
<p>To understand how private fields work, let’s first consider a class that has only public fields, but uses the constructor to encapsulate data—a somewhat common technique, even if it is a bit of a hack. The following class creates a basic count that accepts a starting number, allows that number to be increased or decreased, and can be reset to the original starting value or any other value.</p> | ||
|
||
<pre class="brush: js example-bad"> | ||
class PublicCounter { | ||
constructor(start = 0) { | ||
let _count = start; | ||
let _init = start; | ||
this.increase = (x = 1) => _count += x; | ||
this.decrease = (x = 1) => _count -= x; | ||
this.reset = (x = _init) => _count = x; | ||
this.getCount = () => _count; | ||
} | ||
get current() { | ||
return this.getCount(); | ||
} | ||
} | ||
</pre> | ||
|
||
|
||
|
||
<p>The idea here is that once a new counter of this type is spawned, its starting value and its current value are not available to code outside the counter. The only way to modify the value of <code>_count</code> is through the defined methods, such as <code>increase()</code> and <code>reset()</code>. Similarly, <code>_init</code> can’t be modified, because there are no methods inside the class to do so, and outside code is unable to reach it.</p> | ||
|
||
<p>Here’s the same idea, only this time, we’ll use private fields.</p> | ||
|
||
<pre class="brush: js"> | ||
class PrivateCounter { | ||
#count; | ||
#init; | ||
constructor(start = 0) { | ||
this.#init = start; | ||
this.reset(start); | ||
} | ||
increase(x = 1) { this.#count += x; } | ||
decrease(x = 1) { this.#count -= x; } | ||
reset(x = this.#init) { this.#count = x; } | ||
get current() { | ||
return this.#count; | ||
} | ||
} | ||
|
||
let total = new PrivateCounter(7); | ||
console.log(total.current); // expected output: 7 | ||
total.increase(); // #count now = 8 | ||
total.increase(5); // #count now = 13 | ||
console.log(total.current); // expected output: 13 | ||
total.reset(); // #count now = 7 | ||
</pre> | ||
|
||
<p>The “hash mark” (<code>#</code>) is what marks a field as being private. It also prevents private fields and property names from ever being in conflict: private names <strong>must</strong> start with <code>#</code>, whereas property names can <strong>never</strong> start that way.</p> | ||
|
||
<p>Having declared the private fields, they act as we saw in the public example. The only way to change the <code>#count</code> value is via the publicly available methods like <code>decrease()</code>, and because (in this example) there are no defined ways to alter it, the <code>#init</code> value is immutable. It’s set when a new <code>PrivateCounter</code> is constructed, and can never be changed thereafter.</p> | ||
|
||
<p>It’s also the case that you <strong>cannot</strong> read a private value directly from code outside the class object. Consider:</p> | ||
|
||
<pre class="brush: js"> | ||
let score = new PrivateCounter(); // #count and #init are now both 0 | ||
score.increase(100); | ||
console.log(score.current); | ||
// output: 100 | ||
console.log(score.#count); | ||
// output: | ||
// "Uncaught SyntaxError: Private field '#count' must be declared in an enclosing class" | ||
</pre> | ||
|
||
<p>If you wish to read private data from outside a class, you must first invent a method or other function to return it. We had already done that with the <code>current()</code> getter that returns the current value of <code>#count</code>, but <code>#init</code> is locked away. Unless we add something like a <code>getInit()</code> method to the class, we can’t even see the initial value from outside the class, let alone alter it, and the compiler will throw errors if we try.</p> | ||
|
||
<p>What are the other restrictions around private fields? For one, you can’t refer to a private field you didn’t previously define. You might be used to inventing new fields on the fly in JavaScript, but that just won’t fly with private fields. | ||
|
||
<pre class="brush: js example-bad"> | ||
class BadIdea { | ||
constructor(arg) { | ||
this.#init = arg; // syntax error occurs here | ||
#startState = arg; // syntax error would also occur here | ||
} // because private fields weren’t defined | ||
} // before being referenced | ||
</pre> | ||
|
||
<p>You can’t define the same name twice in a single class, and you can’t delete private fields.</p> | ||
|
||
<pre class="brush: js example-bad"> | ||
class BadIdeas { | ||
#firstName; | ||
#firstName; // syntax error occurs here | ||
#lastName; | ||
constructor() { | ||
delete this.#lastName; // also a syntax error | ||
} | ||
} | ||
</pre> | ||
|
||
<p>And you can have static private fields, for things you want to be both private and set in stone at construction.</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's weird to have this bit about a thing you can do in the middle of a bit about things you can't do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried addressing this without adding subheadings in the latest push; let me know if it feels like it works. If not, I’ll see about using headings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I like this approach :). |
||
|
||
<pre class="brush: js"> | ||
class colorMixer { | ||
static #red = "rgba(1,0,0,1)"; | ||
static #green = "rgba(0,1,0,1)"; | ||
static #blue = "rgba(0,0,1,1)"; | ||
#mixedColor; | ||
constructor() { | ||
… | ||
} | ||
} | ||
</pre> | ||
|
||
<p>There is another limitation: you can’t declare private fields or methods via <a href="/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#using_object_initializers">object literals</a>. You might be used to something like this:</p> | ||
|
||
<pre class="brush: js"> | ||
var planet = { | ||
name: 'Terra', | ||
radiusKm: 6371, | ||
radiusMiles: 3959 | ||
}; | ||
</pre> | ||
|
||
<p>If you try to include a private class feature when doing this, an error will be thrown.</p> | ||
|
||
|
||
<pre class="brush: js example-bad"> | ||
var planet = { | ||
name: 'Terra', | ||
radiusKm: 6371, | ||
radiusMiles: 3959, | ||
#secret: 'central inner core' | ||
}; | ||
// result: | ||
// "Uncaught SyntaxError: Unexpected identifier" | ||
</pre> | ||
|
||
|
||
|
||
<h2>Private methods</h2> | ||
|
||
<p>Just like private fields, private methods are marked with a leading <code>#</code> and cannot be accessed from outside their class. They’re useful when you have something complex that the class needs to do internally, but it’s something that no other part of the code should be allowed to call.</p> | ||
|
||
<p>For example, imagine creating <a href="/en-US/docs/Web/Web_Components/Using_custom_elements">HTML custom elements</a> that should do something somewhat complicated when clicked/tapped/otherwise activated. Furthermore, the somewhat complicated things that happen when the element is clicked should be restricted to this class, because no other part of the JavaScript will (or should) ever access it. Therefore, something like:</p> | ||
|
||
<pre class="brush: js"> | ||
class CustomClick extends HTMLElement { | ||
#handleClicked() { | ||
// do complicated stuff in here | ||
} | ||
constructor() { | ||
super(); | ||
this.#handleClicked(); | ||
} | ||
connectedCallback() { | ||
this.addEventListener('click', this.#handleClicked) | ||
} | ||
} | ||
|
||
customElements.define('chci-interactive', CustomClick); | ||
</pre> | ||
|
||
<p>This can also be done for getters and setters, which is useful in any situation where you want to only get or set things from within the same class. As with fields and methods, prefix the name of the getter/setter with <code>#</code>.</p> | ||
|
||
<pre class="brush: js"> | ||
class Counter extends HTMLElement { | ||
#xValue = 0; | ||
get #x() { return #xValue; } | ||
set #x(value) { | ||
this.#xValue = value; | ||
window.requestAnimationFrame(this.#render.bind(this)); | ||
} | ||
#clicked() { | ||
this.#x++; | ||
} | ||
constructor() { | ||
super(); | ||
this.onclick = this.#clicked.bind(this); | ||
} | ||
connectedCallback() { this.#render(); } | ||
#render() { | ||
this.textContent = this.#x.toString(); | ||
} | ||
} | ||
|
||
customElements.define('num-counter', Counter); | ||
</pre> | ||
|
||
<p>In this case, pretty much every field and method is private to the class. Thus, it presents an interface to the rest of the code that’s essentially just like a built-in HTML element. No other part of the JavaScript has the power to affect any of its internals.</p> | ||
|
||
<h2 id="See_also">See also</h2> | ||
|
||
<ul> | ||
<li><a href="https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md">Private Syntax FAQ</a></li> | ||
<li><a href="https://rfrn.org/~shu/2018/05/02/the-semantics-of-all-js-class-elements.html">The Semantics of All JS Class Elements</a></li> | ||
</ul> | ||
hamishwillee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
<h2 id="Browser_compatibility">Browser compatibility</h2> | ||
|
||
<p>{{Compat}}</p> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm very much on the fence about this, but giving this example "bad-example" styling feels a little harsh. It's not actually broken after all, it's just not as good as the new thing.