|
5 | 5 | | :--- |
|
6 | 6 | | Work in progress |
|
7 | 7 |
|
8 |
| -The class-design pattern generally entails defining a general definition for a *type of thing* (class), including data (members) and behaviors (methods), and then creating one or more concrete *instances* of this class definition as actual objects that can interact and perform tasks. Moreover, class-orientation allows declaring a relationship between two or more classes, through what's called "inheritance", to derive new and augmented "subclasses" that mix-n-match and even re-define behaviors. |
| 8 | +The class-design pattern generally entails defining a definition for a *type of thing* (class), including data (members) and behaviors (methods), and then creating one or more concrete *instances* of this class definition as actual objects that can interact and perform tasks. Moreover, class-orientation allows declaring a relationship between two or more classes, through what's called "inheritance", to derive new and augmented "subclasses" that mix-n-match and even re-define behaviors. |
9 | 9 |
|
10 | 10 | Prior to ES6 (2015), JS developers mimicked aspects of class-oriented (aka "object-oriented") design using plain functions and objects, along with the `[[Prototype]]` mechanism (as explained in the previous chapter) -- so called "prototypal classes".
|
11 | 11 |
|
@@ -288,6 +288,76 @@ var thing = new SomethingCool();
|
288 | 288 | thing.DEFAULT_GREETING; // Hello!
|
289 | 289 | ```
|
290 | 290 |
|
| 291 | +#### Avoid This |
| 292 | + |
| 293 | +One pattern that has emerged and grown quite popular, but which I firmly believe is an anti-pattern for `class`, looks like the following: |
| 294 | + |
| 295 | +```js |
| 296 | +class SomethingCool { |
| 297 | + number = 21 |
| 298 | + getNumber = () => this.number * 2 |
| 299 | + |
| 300 | + speak() { /* .. */ } |
| 301 | +} |
| 302 | + |
| 303 | +var another = new SomethingCool(); |
| 304 | + |
| 305 | +another.getNumber(); // 42 |
| 306 | +``` |
| 307 | + |
| 308 | +See the field holding an `=>` arrow function? I say this is a no-no. But why? Let's unwind what's going on. |
| 309 | + |
| 310 | +First, why do this? Because JS developers seem to be perpetually frustrated by the dynamic `this` binding rules (see a subsequent chapter), so they force a `this` binding via the `=>` arrow function. That way, no matter how `getNumber()` is invoked, it's always `this`-locked to the particular instance. That's an understandable convenience to desire, but... it betrays the very nature of the `this` / `[[Prototype]]` pillar of the language. How? |
| 311 | + |
| 312 | +Let's consider the equivalent code to the previous snippet: |
| 313 | + |
| 314 | +```js |
| 315 | +class SomethingCool { |
| 316 | + constructor() { |
| 317 | + this.number = 21; |
| 318 | + this.getNumber = () => this.number * 2; |
| 319 | + } |
| 320 | + |
| 321 | + speak() { /* .. */ } |
| 322 | +} |
| 323 | + |
| 324 | +var another = new SomethingCool(); |
| 325 | + |
| 326 | +another.getNumber(); // 42 |
| 327 | +``` |
| 328 | + |
| 329 | +Can you spot the problem? Look closely. I'll wait. |
| 330 | + |
| 331 | +... |
| 332 | + |
| 333 | +We've made it clear repeatedly so far that `class` definitions put their methods on the class constructor's `prototype` object, such that there's just one function and it's inherited (shared) by all instances. That's what will happen with `speak()` in the above snippet. |
| 334 | + |
| 335 | +But what about `getNumber()`? That's essentially a class method, but it won't be handled by JS quite the same as `speak()` will. Consider: |
| 336 | + |
| 337 | +```js |
| 338 | +Object.hasOwn(another,"number"); // true |
| 339 | +Object.hasOwn(another,"speak"); // false |
| 340 | +Object.hasOwn(another,"getNumber"); // true -- oops! |
| 341 | +``` |
| 342 | + |
| 343 | +You see? By defining a function value and attaching it as a field/member property, we're losing the shared prototypal method'ness of the function, and it's becoming just like any per-instance property. That means we're creating a new function property **for each instance**, rather than it being created just once on the class constructor's `prototype`. |
| 344 | + |
| 345 | +That's wasteful in performance and memory, even if by a tiny bit. That alone should be enough to avoid it. |
| 346 | + |
| 347 | +But I would argue that way more importantly, what you've done with this pattern is invalidate the very reason why using `class` and `this`-aware methods is even remotely useful/powerful! |
| 348 | + |
| 349 | +If you go to all the trouble to define class methods with `this.` references throughout them, but then you lock/bind all those methods to a specific object instance, you've basically travelled all the way around the world just to go next door. |
| 350 | + |
| 351 | +If all you want is function(s) that are statically fixed to a particular "context", and don't need any dynamicism or sharing, what you want is... **closure**. And you're in luck: I wrote a whole book in this series on how to use closure so functions remember/access their statically defined scope (aka "context"). That's a way more appropriate, and simpler to code, way to get what you're doing. |
| 352 | + |
| 353 | +Don't abuse/misuse `class` and turn it into a over-hyped, glorified collection of closure. |
| 354 | + |
| 355 | +I'm *not* saying: "never use `=>` arrow functions inside classes". |
| 356 | + |
| 357 | +I *am* saying: "never attach an `=>` arrow function as an instance property in place of a dynamic prototypal class method, either out of mindless habit, or laziness in typing fewer characters, or misguided `this`-binding convenience." |
| 358 | + |
| 359 | +In a subsequent chapter, we'll dive deep into how to understand and properly leverage the full power of the dynamic `this` mechanism. |
| 360 | + |
291 | 361 | ## Class Extension
|
292 | 362 |
|
293 | 363 | The way to unlock the power of class inheritance is through the `extends` keyword, which defines a relationship between two classes:
|
|
0 commit comments