Skip to content

Commit d54f531

Browse files
committed
objects-classes, ch3: added a bunch of text about class statics
1 parent bc1ae62 commit d54f531

File tree

1 file changed

+173
-11
lines changed

1 file changed

+173
-11
lines changed

objects-classes/ch3.md

Lines changed: 173 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ class SomethingCool {
9494
greeting() {
9595
console.log("Hello!");
9696
}
97-
9897
appreciate() {
9998
console.log("Thanks!");
10099
}
@@ -179,7 +178,6 @@ class SomethingCool {
179178
constructor() {
180179
console.log("Here's your new instance!");
181180
}
182-
183181
greeting() {
184182
console.log("Hello!");
185183
}
@@ -208,7 +206,6 @@ class SomethingCool {
208206
// add a property to the current instance
209207
this.number = 42;
210208
}
211-
212209
speak() {
213210
// access the property from the current instance
214211
console.log(`My favorite number is ${ this.number }!`);
@@ -238,7 +235,6 @@ class SomethingCool {
238235
constructor() {
239236
// no need for the constructor here
240237
}
241-
242238
speak() {
243239
// access the property from the current instance
244240
console.log(`My favorite number is ${ this.number }!`);
@@ -317,7 +313,6 @@ class SomethingCool {
317313
this.number = 21;
318314
this.getNumber = () => this.number * 2;
319315
}
320-
321316
speak() { /* .. */ }
322317
}
323318

@@ -417,7 +412,6 @@ class SomethingCool extends Something {
417412
greeting() {
418413
return `Here's ${this.what}!`;
419414
}
420-
421415
speak() {
422416
console.log( this.greeting().toUpperCase() );
423417
}
@@ -448,7 +442,6 @@ class SomethingCool extends Something {
448442
greeting() {
449443
return `Wow! ${ super.greeting() }!`;
450444
}
451-
452445
speak() {
453446
console.log( this.greeting().toUpperCase() );
454447
}
@@ -471,7 +464,6 @@ class Something {
471464
constructor(what = "something") {
472465
this.what = what;
473466
}
474-
475467
greeting() {
476468
return `That's ${this.what}!`;
477469
}
@@ -481,7 +473,6 @@ class SomethingCool extends Something {
481473
constructor() {
482474
super("something cooler");
483475
}
484-
485476
speak() {
486477
console.log( this.greeting().toUpperCase() );
487478
}
@@ -520,7 +511,6 @@ class SomethingCool extends Something {
520511
greeting() {
521512
return `Here's ${this.what}!`;
522513
}
523-
524514
speak() {
525515
console.log( this.greeting().toUpperCase() );
526516
}
@@ -553,7 +543,179 @@ As nice as the `class` syntax is, don't forget what's really happening under the
553543

554544
## Static Class Behavior
555545

556-
// TODO
546+
We've so far emphasized two different locations for data or behavior (methods) to reside: on the constructor's prototype, or on the instance. But there's a third option: on the constructor (function object) itself.
547+
548+
In a traditional class-oriented system, methods defined on a class are not concrete things you could ever invoke or interact with. You have to instantiate a class to have a concrete object to invoke those methods with. Prototypal languages like JS blur this line a bit: all class-defined methods are "real" functions residing on the constructor's prototype, and you could therefore invoke them. But as I asserted earlier, you really *should not* do so, as this is not how JS assumes you will write your `class`es, and there are some weird corner-case behaviors you may run into. Best to stay on the narrow path that `class` lays out for you.
549+
550+
Not all behavior that we define and want to associate/organize with a class *needs* to be aware of an instance. Moreover, sometimes a class needs to publicly define data (like constants) that developers using that class need to access, independent of any instance they may or may not have created.
551+
552+
So, how does a class system enable defining such data and behavior that should be available with a class but independent of (unaware of) instantiated objects? **Static properties and functions**.
553+
554+
| NOTE: |
555+
| :--- |
556+
| I'll use "static property" / "static function", rather than "member" / "method", just so it's clearer that there's a distinction between instance-bound members / instance-aware methods, and non-instance properties and instance-unaware functions. |
557+
558+
We use the `static` keyword in our `class` bodies to distinguish these definitions:
559+
560+
```js
561+
class Point2d {
562+
// class statics
563+
static origin = new Point2d(0,0)
564+
static distance(point1,point2) {
565+
return Math.sqrt(
566+
((point2.x - point1.x) ** 2) +
567+
((point2.y - point1.y) ** 2)
568+
);
569+
}
570+
571+
// instance members and methods
572+
x
573+
y
574+
constructor(x,y) {
575+
this.x = x;
576+
this.y = y;
577+
}
578+
toString() {
579+
return `(${this.x},${this.y})`;
580+
}
581+
}
582+
583+
console.log(`Starting point: ${Point2d.origin}`);
584+
// Starting point: (0,0)
585+
586+
var next = new Point2d(3,4);
587+
console.log(`Next point: ${next}`);
588+
// Next point: (3,4)
589+
590+
console.log(`Distance: ${
591+
Point2d.distance( Point2d.origin, next )
592+
}`);
593+
// Distance: 5
594+
```
595+
596+
The `Point2d.origin` is a static property, which just so happens to hold a constructed instance of our class. And `Point2d.distance(..)` is a static function that computes the 2-dimensional cartesian distance between two points.
597+
598+
Of course, we could have put these two somewhere other than as `static`s on the class definition. But since they're directly related to the `Point2d` class, it makes *most sense* to organize them there.
599+
600+
| NOTE: |
601+
| :--- |
602+
| Don't forget that when you use the `class` syntax, the name `Point2d` is actually the name of a constructor function that JS defines. So `Point2d.origin` is just a regular property access on that function object. That's what I meant at the top of this section when I referred to a third location for storing *things* related to classes; in JS, `static`s are stored as properties on the constructor function. Take care not to confuse those with properties stored on the constructor's `prototype` (methods) and properties stored on the instance (members). |
603+
604+
### Static Property Initializations
605+
606+
The value in a static initialization (`static whatever = ..`) can include `this` references, which refers to the class itself (actually, the constructor) rather than to an instance:
607+
608+
```js
609+
class Point2d {
610+
// class statics
611+
static originX = 0
612+
static originY = 0
613+
static origin = new this(this.originX,this.originY)
614+
615+
// ..
616+
}
617+
```
618+
619+
| WARNING: |
620+
| :--- |
621+
| I don't recommend actually doing the `new this(..)` trick I've illustrated here. That's just for illustration purposes. The code would read more cleanly with `new Point2d(this.originX,this.originY)`, so prefer that approach. |
622+
623+
An important detail not to gloss over: unlike public field initializations, which only happen once an instantiation (with `new`) occurs, class static initializations always run *immediately* after the `class` has been defined. Moreover, the order of static initializations matters; you can think of the statements as if they're being evaluated one at a time.
624+
625+
Also like class members, static properties do not have to be initialized (default: `undefined`), but it's much more common to do so. There's not much utility in declaring a static property with no initialized value (`static whatever`); Accessing either `Point2d.whatever` or `Point2d.nonExistent` would both result in `undefined`.
626+
627+
Recently (in ES2022), the `static` keyword was extended so it can now define a block inside the `class` body for more sophisticated initialization of `static`s:
628+
629+
```js
630+
class Point2d {
631+
// class statics
632+
static origin = new Point2d(0,0)
633+
static distance(point1,point2) {
634+
return Math.sqrt(
635+
((point2.x - point1.x) ** 2) +
636+
((point2.y - point1.y) ** 2)
637+
);
638+
}
639+
640+
// static initialization block (as of ES2022)
641+
static {
642+
let outerPoint = new Point2d(6,8);
643+
this.maxDistance = this.distance(
644+
this.origin,
645+
outerPoint
646+
);
647+
}
648+
649+
// ..
650+
}
651+
652+
Point2d.maxDistance; // 10
653+
```
654+
655+
The `let outerPoint = ..` here is not a special `class` feature; it's exactly like a normal `let` declaration in any normal block of scope (see the "Scope & Closures" book of this series). We're merely declaring a localized instance of `Point2d` assigned to `outerPoint`, then using that value to derive the assignment to the `maxDistance` static property.
656+
657+
Static initialization blocks are also useful for things like `try..catch` statements around expression computations.
658+
659+
### Static Inheritance
660+
661+
Class statics are inherited by subclasses (obviously, as statics!), can be overriden, and `super` can be used for base class references (and static function polymorphism), all in much the same way as inheritance works with instance members/methods:
662+
663+
```js
664+
class Point2d {
665+
static origin = /* .. */
666+
static distance(x,y) { /* .. */ }
667+
668+
static {
669+
// ..
670+
this.maxDistance = /* .. */;
671+
}
672+
673+
// ..
674+
}
675+
676+
class Point3d extends Point2d {
677+
// class statics
678+
static origin = new Point3d(
679+
// here, `this.origin` references wouldn't
680+
// work (self-referential), so we use
681+
// `super.origin` references instead
682+
super.origin.x, super.origin.y, 0
683+
)
684+
static distance(point1,point2) {
685+
// here, super.distance(..) is Point2d.distance(..),
686+
// if we needed to invoke it
687+
688+
return Math.sqrt(
689+
((point2.x - point1.x) ** 2) +
690+
((point2.y - point1.y) ** 2) +
691+
((point2.z - point1.z) ** 2)
692+
);
693+
}
694+
695+
// instance members/methods
696+
z
697+
constructor(x,y,z) {
698+
super(x,y); // <-- don't forget this line!
699+
this.z = z;
700+
}
701+
toString() {
702+
return `(${this.x},${this.y},${this.z})`;
703+
}
704+
}
705+
706+
Point2d.maxDistance; // 10
707+
Point3d.maxDistance; // 10
708+
```
709+
710+
As you can see, the static property `maxDistance` we defined on `Point2d` was inherited as a static property on `Point3d`.
711+
712+
| TIP: |
713+
| :--- |
714+
| Remember: any time you define a subclass constructor, you'll need to call `super(..)` in it, usually as the first statement. I find that all too easy to forget. |
715+
716+
Don't skip over the underlying JS behavior here. Just like method inheritance discussed earlier, the static "inheritance" is *not* a copying of these static properties/functions from base class to subclass; it's sharing via the `[[Prototype]]` chain. Specifically, the constructor function `Point3d()` has its `[[Prototype]]` linkage changed by JS (from the default of `Function.prototype`) to `Point2d`, which is what allows `Point3d.maxDistance` to delegate to `Point2d.maxDistance`.
717+
718+
It's also interesting, perhaps only historically now, to note that static inheritance -- which was part of the original ES6 `class` mechanism feature set! -- was one specific feature that went beyond "just syntax sugar". Static inheritance, as we see it illustrated here, was *not* possible to achieve/emulate in JS prior to ES6, in the old prototypal-class style of code. It's a special new behavior introduced only as of ES6.
557719

558720
## Private Class Behavior
559721

0 commit comments

Comments
 (0)