Skip to content

Commit 2328050

Browse files
committed
objects-classes, ch3: fixing inconsistency/confusion with privates/statics text, including changing code examples
1 parent 8b2db22 commit 2328050

File tree

1 file changed

+215
-66
lines changed

1 file changed

+215
-66
lines changed

objects-classes/ch3.md

Lines changed: 215 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -768,32 +768,33 @@ If I'm being honest: maybe you shouldn't. Or maybe you should. That's up to you.
768768
You're excited to finally see the syntax for magical *private* visibility, right? Please don't shoot the messenger if you feel angered or sad at what you're about to see.
769769

770770
```js
771-
class SomethingStrange {
772-
#mySecretNumber = 42
771+
class Point2d {
772+
// statics
773+
static samePoint(point1,point2) {
774+
return point1.#ID === point2.#ID;
775+
}
773776

774-
#changeMySecret() {
775-
// ooo, tricky tricky!
776-
this.#mySecretNumber *= Math.random();
777+
// privates
778+
#ID = null
779+
#assignID() {
780+
this.#ID = Math.round(Math.random() * 1e9);
777781
}
778782

779-
guessMySecret(v) {
780-
if (v === this.#mySecretNumber) {
781-
console.log("You win!");
782-
this.#changeMySecret();
783-
}
784-
else {
785-
console.log("Nope, try again.");
786-
}
783+
// publics
784+
x
785+
y
786+
constructor(x,y) {
787+
this.#assignID();
788+
this.x = x;
789+
this.y = y;
787790
}
788791
}
789792

790-
var thing = new SomethingStrange();
791-
792-
this.guessMySecret(42);
793-
// You win!!
793+
var one = new Point2d(3,4);
794+
var two = new Point2d(3,4);
794795

795-
this.guessMySecret(42);
796-
// Nope, try again.
796+
Point2d.samePoint(one,two); // false
797+
Point2d.samePoint(one,one); // true
797798
```
798799

799800
No, JS didn't do the sensible thing and introduce a `private` keyword like they did with `static`. Instead, they introduced the `#`. (insert lame joke about social-media millienials loving hashtags, or something)
@@ -806,99 +807,247 @@ The `#whatever` syntax (including `this.#whatever` form) is only valid inside `c
806807

807808
Unlike public fields/instance members, private fields/instance members *must* be declared in the `class` body. You cannot add a private member to a class declaration dynamically while in the constructor method; `this.#whatever = ..` type assignments only work if the `#whatever` private field is declared in the class body. Moreover, though private fields can be re-assigned, they cannot be `delete`d from an instance, the way a public field/class member can.
808809

809-
#### Exfiltration
810+
#### Subclassing + Privates
810811

811-
Even though a method or member may be declared with *private* visibility, they can still be exfiltrated (extracted) from a class instance:
812+
I warned earlier that subclassing with classes that have private members/methods can be a limiting trap. But that doesn't mean they cannot be used together.
812813

813-
```js
814-
var number, func;
814+
Because "inheritance" in JS is sharing (through the `[[Prototype]]` chain), if you invoke an inherited method in a subclass, and that inherited method in turn accesses/invokes privates in its host (base) class, this works fine:
815815

816-
class SomethingStrange {
817-
#myPrivateNumber = 42
818-
#mySecretFunc() {
819-
return this.#myPrivateNumber;
820-
}
816+
```js
817+
class Point2d { /* .. */ }
821818

822-
constructor() {
823-
number = this.#myPrivateNumber;
824-
func = this.#mySecretFunc;
819+
class Point3d extends Point2d {
820+
z
821+
constructor(x,y,z) {
822+
super(x,y);
823+
this.z = z;
825824
}
826825
}
827826

828-
var thing = new SomethingStrange();
827+
var one = new Point3d(3,4,5);
828+
```
829+
830+
The `super(x,y)` call in this constructor invokes the inherited base class constructor (`Point2d(..)`), which itself accesses `Point2d`'s private method `#assignID()` (see the earlier snippet). No exception is thrown, even though `Point3d` cannot directly see or access the `#ID` / `#assignID()` privates that are indeed stored on the instance (named `one` here).
829831

830-
number; // 42
831-
func; // function #mySecreFunc() { .. }
832-
func.call(thing); // 42
832+
In fact, even the inherited `static samePoint(..)` function will work from either `Point3d` or `Point2d`:
833833

834-
func.call({});
835-
// TypeError: Cannot read private member #myPrivateNumber
836-
// from an object whose class did not declare it
834+
```js
835+
Point2d.samePoint(one,one); // true
836+
Point3d.samePoint(one,one); // true
837+
```
838+
839+
Actually, that shouldn't be that suprising, since:
840+
841+
```js
842+
Point2d.samePoint === Point3d.samePoint;
843+
```
844+
845+
The inherited function reference is *the exact same function* as the base function reference; it's not some copy of the function. Because the function in question has no `this` reference in it, no matter from where it's invoked, it should produce the same outcome.
846+
847+
It's still a shame though that `Point3d` has no way to access/influence, or indeed even knowledge of, the `#ID` / `#assignID()` privates from `Point2d`:
848+
849+
```js
850+
class Point2d { /* .. */ }
851+
852+
class Point3d extends Point2d {
853+
z
854+
constructor(x,y,z) {
855+
super(x,y);
856+
this.z = z;
857+
858+
console.log(this.#ID); // will throw!
859+
}
860+
}
837861
```
838862

839-
The main reason for me pointing this out is to be careful when using private methods as callbacks (or in any way passing them to other parts of the program). There's nothing stopping you from doing so, which can create a bit of an unintended privacy disclosure.
863+
| WARNING: |
864+
| :--- |
865+
| Notice that this snippet throws an early static syntax error at the time of defining the `Point3d` class, before ever even getting a chance to create an instance of the class. The same exception would be thrown if the reference was `super.#ID` instead of `this.#ID`. |
840866

841867
#### Existence Check
842868

843-
I think this use-case is somewhat contrived/unusual, but... you may want to check to see if a private field/method exists on an object (including the current `this` instance). Keep in mind that only the `class` itself knows about, and can therefore check for, any such a private field/method; such checks are almost always going to be in a static function.
869+
Keep in mind that only the `class` itself knows about, and can therefore check for, such a private field/method.
870+
871+
You may want to check to see if a private field/method exists on an object instance. For example (as shown below), you may have a static function or method in a class, which receives an external object reference passed in. To check to see if the passed-in object reference is of this same class (and therefore has the same private members/methods in it), you basically need to do a "brand check" against the object.
844872

845-
Doing could be rather convoluted, because if you access a private field that didn't already exist, you get a JS exception thrown, which would lead to ugly `try..catch` logic. But there's a cleaner approach:
873+
Such a check could be rather convoluted, because if you access a private field that doesn't already exist on the object, you get a JS exception thrown, requiring ugly `try..catch` logic.
874+
875+
But there's a cleaner approach, so called an "ergonomic brand check", using the `in` keyword:
846876

847877
```js
848-
class SomethingStrange {
849-
static checkGuess(thing,v) {
850-
// "ergonomic brand check"
851-
if (#myPrivateNumber in thing) {
852-
return thing.guessMySecret(v);
878+
class Point2d {
879+
// statics
880+
static samePoint(point1,point2) {
881+
// "ergonomic brand checks"
882+
if (#ID in point1 && #ID in point2) {
883+
return point1.#ID === point2.#ID;
853884
}
885+
return false;
854886
}
855887

856-
#myPrivateNumber = 42;
888+
// privates
889+
#ID = null
890+
#assignID() {
891+
this.#ID = Math.round(Math.random() * 1e9);
892+
}
857893

858-
// ..
894+
// publics
895+
x
896+
y
897+
constructor(x,y) {
898+
this.#assignID();
899+
this.x = x;
900+
this.y = y;
901+
}
902+
}
903+
904+
var one = new Point2d(3,4);
905+
var two = new Point2d(3,4);
906+
907+
Point2d.samePoint(one,two); // false
908+
Point2d.samePoint(one,one); // true
909+
```
910+
911+
The `#privateField in someObject` check will not throw an exception if the field isn't found, so it's safe to use without `try..catch` and use its simple boolean result.
912+
913+
#### Exfiltration
914+
915+
Even though a member/method may be declared with *private* visibility, it can still be exfiltrated (extracted) from a class instance:
916+
917+
```js
918+
var id, func;
919+
920+
class Point2d {
921+
// privates
922+
#ID = null
923+
#assignID() {
924+
this.#ID = Math.round(Math.random() * 1e9);
925+
}
926+
927+
// publics
928+
x
929+
y
930+
constructor(x,y) {
931+
this.#assignID();
932+
this.x = x;
933+
this.y = y;
934+
935+
// exfiltration
936+
id = this.#ID;
937+
func = this.#assignID;
938+
}
859939
}
860940

861-
var thing = new SomethingStrange();
941+
var point = new Point2d(3,4);
862942

863-
SomethingStrage.checkGuess(thing,42);
864-
// You win!!
943+
id; // 7392851012 (...for example)
944+
945+
func; // function #assignID() { .. }
946+
func.call(point,42);
947+
948+
func.call({},100);
949+
// TypeError: Cannot write private member #ID to an
950+
// object whose class did not declare it
865951
```
866952

953+
The main concern here is to be careful when passing private methods as callbacks (or in any way exposing privates to other parts of the program). There's nothing stopping you from doing so, which can create a bit of an unintended privacy disclosure.
954+
867955
### Private Statics
868956

869957
Static properties and functions can also use `#` to be marked as private:
870958

871959
```js
872-
class SomethingStrange {
873-
static #errorMsg = "Not available."
960+
class Point2d {
961+
static #errorMsg = "Out of bounds."
874962
static #printError() {
875-
console.log(this.#errorMsg);
963+
console.log(`Error: ${this.#errorMsg}`);
876964
}
877965

878-
static checkGuess(thing,v) {
879-
// "ergonomic brand check"
880-
if (#myPrivateNumber in thing) {
881-
return thing.guessMySecret(v);
882-
}
883-
else {
884-
this.#printError();
966+
// publics
967+
x
968+
y
969+
constructor(x,y) {
970+
if (x > 100 || y > 100) {
971+
Point2d.#printError();
885972
}
973+
this.x = x;
974+
this.y = y;
975+
}
976+
}
977+
978+
var one = new Point2d(30,400);
979+
// Error: Out of bounds.
980+
```
981+
982+
The `#printError()` static private function here has a `this`, but that's referencing the `Point2d` class, not an instance. As such, the `#errorMsg` and `#printError()` are independent of instances and thus are best as statics. Moreover, there's no reason for them to be accessible outside the class, so they're marked private.
983+
984+
Remember: private statics are similarly not-inherited by subclasses just as private members/methods are not.
985+
986+
#### Subclassing Gotcha
987+
988+
Recall that inherited methods, invoked from a subclass, have no trouble accessing (via `this.#whatever` style references) any privates from their own base class:
989+
990+
```js
991+
class Point2d {
992+
// ..
993+
994+
getID() {
995+
return this.#ID;
886996
}
887997

888-
#myPrivateNumber = 42;
998+
// ..
999+
}
8891000

1001+
class Point3d extends Point2d {
8901002
// ..
1003+
1004+
printID() {
1005+
console.log(`ID: ${this.getID()}`);
1006+
}
8911007
}
1008+
1009+
var point = new Point3d(3,4,5);
1010+
point.printID();
1011+
// ID: ..
8921012
```
8931013

894-
Private statics are similarly not-inherited just as private members/methods are not.
1014+
That works just fine.
8951015

896-
| WARNING: |
897-
| :--- |
898-
| Be careful with `this` references inside public static functions that make reference to private static functions. While the public static functions will be inherited by derived subclasses, the private static functions are not, which will cause such `this.#..` references to fail. |
1016+
Unfortunately, and (to me) a little unexpectedly/inconsistently, the same is not true of private statics accessed from inherited public static functions:
1017+
1018+
```js
1019+
class Point2d {
1020+
static #errorMsg = "Out of bounds."
1021+
static printError() {
1022+
console.log(`Error: ${this.#errorMsg}`);
1023+
}
1024+
1025+
// ..
1026+
}
1027+
1028+
class Point3d extends Point2d {
1029+
// ..
1030+
}
1031+
1032+
Point2d.printError();
1033+
// Error: Out of bounds.
1034+
1035+
Point3d.printError === Point2d.printError;
1036+
// true
1037+
1038+
Point3d.printError();
1039+
// TypeError: Cannot read private member #errorMsg
1040+
// from an object whose class did not declare it
1041+
```
1042+
1043+
The `printError()` static is inherited (shared via `[[Prototype]]`) from `Point2d` to `Point3d` just fine, which is why the function references are identical. Like the non-static snippet just above, you might have expected the `Point3d.printError()` static invocation to resolve via the `[[Prototype]]` chain to its original base class (`Point2d`) location, thereby letting it access the base class's `#errorMsg` static private.
1044+
1045+
But it fails, as shown by the last statement in that snippet. Beware that gotcha!
8991046

9001047
## Class Example
9011048

1049+
OK, we've laid out a bunch of disparate class features. I want to wrap up this chapter by trying to illustrate a sampling of these capabilities in a single example that's a little less basic/contrived.
1050+
9021051
// TODO
9031052

9041053
[^POLP]: *Principle of Least Privilege*, https://en.wikipedia.org/wiki/Principle_of_least_privilege, 15 July 2022.

0 commit comments

Comments
 (0)