You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
console.log(`My favorite number is ${this.number}!`);
@@ -238,7 +235,6 @@ class SomethingCool {
238
235
constructor() {
239
236
// no need for the constructor here
240
237
}
241
-
242
238
speak() {
243
239
// access the property from the current instance
244
240
console.log(`My favorite number is ${this.number}!`);
@@ -317,7 +313,6 @@ class SomethingCool {
317
313
this.number=21;
318
314
this.getNumber= () =>this.number*2;
319
315
}
320
-
321
316
speak() { /* .. */ }
322
317
}
323
318
@@ -417,7 +412,6 @@ class SomethingCool extends Something {
417
412
greeting() {
418
413
return`Here's ${this.what}!`;
419
414
}
420
-
421
415
speak() {
422
416
console.log( this.greeting().toUpperCase() );
423
417
}
@@ -448,7 +442,6 @@ class SomethingCool extends Something {
448
442
greeting() {
449
443
return`Wow! ${super.greeting() }!`;
450
444
}
451
-
452
445
speak() {
453
446
console.log( this.greeting().toUpperCase() );
454
447
}
@@ -471,7 +464,6 @@ class Something {
471
464
constructor(what="something") {
472
465
this.what= what;
473
466
}
474
-
475
467
greeting() {
476
468
return`That's ${this.what}!`;
477
469
}
@@ -481,7 +473,6 @@ class SomethingCool extends Something {
481
473
constructor() {
482
474
super("something cooler");
483
475
}
484
-
485
476
speak() {
486
477
console.log( this.greeting().toUpperCase() );
487
478
}
@@ -520,7 +511,6 @@ class SomethingCool extends Something {
520
511
greeting() {
521
512
return`Here's ${this.what}!`;
522
513
}
523
-
524
514
speak() {
525
515
console.log( this.greeting().toUpperCase() );
526
516
}
@@ -553,7 +543,179 @@ As nice as the `class` syntax is, don't forget what's really happening under the
553
543
554
544
## Static Class Behavior
555
545
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
+
classPoint2d {
562
+
// class statics
563
+
static origin =newPoint2d(0,0)
564
+
staticdistance(point1,point2) {
565
+
returnMath.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 =newPoint2d(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
+
classPoint2d {
610
+
// class statics
611
+
static originX =0
612
+
static originY =0
613
+
static origin =newthis(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
+
classPoint2d {
631
+
// class statics
632
+
static origin =newPoint2d(0,0)
633
+
staticdistance(point1,point2) {
634
+
returnMath.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 =newPoint2d(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
+
classPoint2d {
665
+
static origin =/* .. */
666
+
staticdistance(x,y) { /* .. */ }
667
+
668
+
static {
669
+
// ..
670
+
this.maxDistance=/* .. */;
671
+
}
672
+
673
+
// ..
674
+
}
675
+
676
+
classPoint3dextendsPoint2d {
677
+
// class statics
678
+
static origin =newPoint3d(
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
+
staticdistance(point1,point2) {
685
+
// here, super.distance(..) is Point2d.distance(..),
686
+
// if we needed to invoke it
687
+
688
+
returnMath.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.
0 commit comments