JavaScript Tips of the Day

:information_source: Update (May 23): We’ve reached 100 tips! While there are still some more tips to come, they will not be released daily. Also, the format will change slightly where each tip will be its own topic rather than a comment in this topic.

:information_source: Update (Apr. 14): The title of the thread will now reflect the latest tip.
:information_source: Update (Apr. 7): List now organized by category with the latest 5 tips being listed at the top.

Happy Valentines Day! To celebrate, I’m reviving one of the most popular kirupa forum posts of all time, ActionScript 3 Tip of the Day (archive), with a new focus on JavaScript!

Here, you’ll find tips posted daily relating to JavaScript: its features, how its used, and anything else related to the language and developing with it.

Tips

Latest

  1. Using Generators to Animate
  2. Labels
  3. The Underscore Convention
  4. Canvas for Bitmaps
  5. Console $ Utilities

Language Features

Web API

Classes/Constructors

Generators and Iterators

How Tos

Miscellaneous

Tooling

Reference

7 Likes

Optional Chaining
Version: ES2020
Level: Intermediate

To start off the tip of the day list, we’ll start with a brand new feature of JavaScript that only just recently got approved for the JavaScript (ECMAScript) standard: optional chaining!

Optional chaining is a brand new JavaScript feature introduced in ES2020 that lets you safely dig down into an object reference without having to worry about whether or not any objects within the path are not available or undefined.

Normally, if you attempt to access a property of an object that doesn’t exist, you’ll get an error.

let empty = {};
let x = empty.something.x;
// TypeError: Cannot read property 'x' of undefined

Here, because something is undefined on empty, a type error occurs when accessing x. The optional chaining operator (?.) allows you to attempt the same object reference but will return undefined rather than throwing an error.

let empty = {};
let x = empty?.something?.x;
console.log(x); // undefined

This makes it easier to look for a deeply nested value without having to check each object reference along the way or wrap your code in a try...catch block. But be careful, this approach doesn’t differentiate between there not being a variable (or object along the way) and the variable existing but having a value of undefined.

Optional chaining works in other situations as well, including, but not limited to, function calls.

More info:

2 Likes

Nullish Coalescing Operator
Version: ES2020
Level: Intermediate

The nullish coalescing operator (??) is also new to ES2020. It works much like logical OR (||) except that it will short circuit (or return) for non-null, non-undefined falsy values such as false, 0, and empty strings whereas OR would not. This is useful for setting defaults that may include these kinds of values.

let initialValue = 0;
let x = initialValue || 1;
let y = initialValue ?? 1;
console.log(x); // 1
console.log(y); // 0

More info:

1 Like

The Official JavaScript Reference: MDN

Ever need to look up a JavaScript function or know what properties a built-in object in JavaScript has? If so, your first stop should be the JavaScript Reference on the Mozilla Developer Network, or MDN. It is the official JavaScript reference for the JavaScript language and APIs on the web.

As tips are posted here, you’ll notice that many will include a link to their respective documentation on MDN. Those links will provide much more information about a topic than the tip will, so it’s recommended to use those links to gain more information.

1 Like

Detecting Dark Mode
Level: Intermediate

Modern OSes now allow users to specify a dark mode for their system’s UI. This can be detected using a media query in CSS allowing site authors to also present their site in a dark mode.

@media (prefers-color-scheme: dark) {
  /* add styles for dark mode users */
}

Using matchMedia, dark mode can also be detected in JavaScript using the same query used in the CSS.

let darkModeQuery = matchMedia('(prefers-color-scheme: dark)');
if (darkModeQuery.matches) {
    // user is using dark mode
}

More info:

Default Function Parameters
Version: ES2015
Level: Beginner

Functions in JavaScript can be called with any number of arguments, whether or not they expect them. If a function parameter does not have a respective argument, it will appear in that function as undefined.

function logValues (a, b) {
    console.log(a);
    console.log(b);
}

logValues(1); // 1, undefined

If you want a parameter to have a specific value if the function is called without that respective argument, you can specify a default parameter value. To define a default parameter add an assignment expression in the parameter list of the function for that parameter. If the parameter were to have an undefined value, it would instead be assigned to the default parameter value.

function logValues (a, b = 2) { // b's default parameter value is 2
    console.log(a);
    console.log(b);
}

logValues(1); // 1, 2

More info:

1 Like

Thenable Objects
Version: ES2015
Level: Advanced

A thenable is any object that has a function property named then. The following is an example of a thenable object:

let myThenable = { then () {} };

Thenable objects in JavaScript are objects that are given special treatment when resolved by promises, effectively being seen as promise objects themselves. Rather than a thenable object becoming the resolved value, the then() method of the object is called and passed resolve and reject functions, much like you would specify in a then called from a promise. The thenable can then provide the promise chain a resolved or rejected value using these functions.

let normalObject = { normal: true };
let thenableObject = {
    then (resolve, reject) {
        resolve(1);
    }
};

Promise.resolve(normalObject)
    .then(value => console.log(value)); // { normal: true }

Promise.resolve(thenableObject)
    .then(value => console.log(value)); // 1

Thenables make it easy for built-in promises to work with alternative promise implementations as long as they use thenable objects.

More info:

Reduce Code with Destructuring
Version: ES2015
Level: Intermediate

Destructuring is a process that allows you to copy properties from an object into other individual variables. Destructuring comes in two forms: array and object.

Array destructuring works on any iterable and uses ordering to copy values from the iterable into variables within the current scope.

let myArray = [1, 2, 3];
let [one, two, three] = myArray;
console.log(one, two, three) // 1, 2, 3

The destructuring expression here would be the same as saying:

let one = myArray[0];
let two = myArray[1];
let three = myArray[2];

Object destructuring, on the other hand, works with any object, but instead of using order, it uses the property names in the object to create new variables.

let myObject = { a: 1, b: 2, c: 3 };
let { a, c } = myObject;
console.log(a, c); // 1, 3

The destructuring expression here would be the same as saying:

let a = myObject.a;
let c = myObject.c;

Using destructuring this way can reduce the amount of code you write, not just by reducing your object references into single variables, but also by allowing you to do so in a single line of code.

What was covered here so far only scratches the surface of destructuring. There’s a lot more to learn about this feature which will be touched upon later on.

More info:

1 Like

JavaScript (ECMAScript) Versioning

The JavaScript language standard is defined by the ECMAScript specification. This is a standard maintained by the standards body Ecma International, with a name that is fittingly suggestive of that very fact. But this wasn’t always the case.

There was a time in history where there was no official standard and JavaScript was as Netscape decided it would be. In those days you saw JavaScript versions along the lines of 1.1 and 1.8 etc. You may even see old HTML with script tags that say something like <script language="JavaScript1.8" ...>, but that was never officially supported, and thankfully, not something you have to worry about today.

Once standardized through Emca, JavaScript’s versioning was instead defined by the release of the ECMAScript standard. As of this writing, there are officially 9 major released editions of ECMAScript, with the current being ECMAScript edition 10 (edition 4 was never released). At first, the versioning of these releases was represented by “ES” followed by the edition number. The first versions of ECMAScript were:

  • ES1
  • ES2
  • ES3
  • (ES4 never released)
  • ES5 (with an ES5.1)
  • ES6

ES6 was a major release with a lot of new additions to the language - the largest release there ever was, and probably the largest there ever will be. It was also with this release that additions to the language were becoming more common, following a yearly release cycle. The naming for these releases then changed to a format using “ES” followed by the year related to the release. Releases since have included:

  • ES2015 (ES6)
  • ES2016
  • ES2017
  • ES2018
  • ES2019
  • ES2020 (coming soon)

Tips in this list for ECMAScript-defined features will follow the date format seen above in identifying which version of the specification they’re from. This will serve as an indication to how new the feature is and how likely you’ll be able to use it in your browser.

Before Ecma, JavaScript releases were usually tied to browser releases, but that is no longer the case. In fact, as modern browsers release updates, they may choose to support only a portion of the features in the newest specification. Sometimes you may not exactly know if a feature is supported in a browser until you try. And its far more difficult to know what other users’ browsers support. By using features defined in older versions of the specification, the more likely other users will be able to use that feature in their browser. This is something you’ll need to keep in mind if you’re developing content for people other than yourself. A website for helping with this is caniuse.

Note that not everything you see and use in JavaScript is defined in the ECMAScript standard. This standard only defines the core language components and syntax. Other APIs, particularly those you see in the browser, are extensions to the language and specific to the browser environement. These include things like document.querySelector() and setTimeout(). When tips here cover these APIs, an ECMAScript version will not be included as it would not apply. Those are instead defined by the WHATWG “living standard” which has no real versioning (and as a result, tips covering those APIs will be without any mention of an associated version).

More info:

1 Like

Variables in Strings with Template Literals
Version: ES2015
Level: Beginner

It’s not uncommon that you’d need to add variable values into your strings. For example if you’re telling the user the results of a test, you might need to use something like:

let grade = 'A+';
let message = 'Your grade is ' + grade + '!';

Historically, this is how you would add values to a string, using the addition or string concatenation operator (+). But with the newer template literal syntax for strings, you no longer have to terminate the string, add what you want, then start up a new string up again, bridging them all together with + operators. Instead you can place a variable (or any expression, really) directly within the string using the interpolation syntax ${}.

let grade = 'A+';
let message = `Your grade is ${grade}!`;

Anything inside ${} in a template literal string (strings defined with `) will be evaluated and inserted into the resulting string at that location. This makes it much easier to insert values into your strings in a way that can also be easier to read.

Keep in mind, however, that this syntax does not work with strings defined with single or double quotes. It only works with template literal strings defined with backticks.

let grade = 'A+';
let message = `Your grade is ${grade}!`;
console.log(message); // Your grade is A+!

let quotedMessage = 'Your grade is ${grade}!';
console.log(quotedMessage); // Your grade is ${grade}!

More info:

1 Like

Event Attributes also Define Callback Functions
Level: Beginner

When defining event handlers for HTML elements, you have a number of options, including adding event handlers in the HTML through attributes, assigning a callback function to an element reference, or using addEventListener.

If you use an HTML attribute, the text of that attribute becomes the code executed when that event fires. In the following example, clicking the button will log a message.

<!-- html -->
<button id="btn" onclick="console.log('You clicked me!')">
  Click me
</button>
<!-- click:
You clicked me!
-->

The happens because onclick attribute code becomes the body of a function that gets automatically created for the element and assigned to its onclick property. This is done much in the same way that you would define an onclick callback in JavaScript yourself. You can see this function by looking at the onclick property for an HTML element that has been defined with an onclick attribute.

// JavaScript
let btn = document.getElementById('btn');
console.log(btn.onclick.toString());
/*
function onclick(event) {
console.log('You clicked me!')
}
*/

If you set your own callback function to the onclick property, this will overwrite the function defined by the onclick attribute. In doing this, however, the attribute value will not update. This means what the attribute shows may not be representative of the actual behavior of the event.

btn.onclick = function () { // overwrite attribute-set callback
    console.log(`onclick="${btn.getAttribute('onclick')}"`);
}
/* click:
onclick="console.log('You clicked me!')"
*/

Setting the onclick attribute dynamically will again re-set the onclick property with a new function containing code from that attribute value.

btn.setAttribute('onclick', "console.log('Stop touching me!')"));
/* click:
Stop touching me!
*/

Using both event attributes and callback properties will not allow you to have more than one event handler for the same event in any given HTML element since they both use the same callback. If you need multiple event handlers in this manner, you can use addEventListener.

More info:

Symbols for Safe Property Names
Version: ES2015
Level: Intermediate

Symbols are a special kind of primitive value that represent a unique value. When you create a new symbol value, it will not equal anything else, even another symbol that was created the exact same way.

let original = Symbol('copy me');
let maybeACopy = Symbol('copy me');
console.log(original === maybeACopy); // false

Symbols, like strings, can be used as property keys in objects. Their uniqueness means that they are guaranteed not to conflict with any other property used in a given object.

let namedObject = { name: 'Bob' };
let name = Symbol('name');
namedObject[name] = 'Bobbert'; // <- uses symbol as key
console.log(namedObject.name); // Bob
console.log(namedObject[name]); // Bobbert

Using symbols like this can provide 2 kinds of safety:

  1. Helps prevent other users from overwriting your object properties
  2. Allows you to add properties to other objects without fear of overwriting it’s properties

For the first point, if you use a symbol property for an object that might be used by other users, then those users would not be able to overwrite that property accidentally with their own properties.

// your code
function getUserDefinedParamsObject () {
    let desc = Symbol('framework object description');
    return {
        [desc]: 'a user defined parameters object'
    };
}

// user code
let params = getUserDefinedParamsObject();
params.x = 3;
params.foo = 'bar';
params.desc = 'custom user description';
// ...

Here, an object is created for users to add their own properties. No matter what properties they choose, they will not overwrite the property defined by the symbol inside the function. While it’s not impossible for the user to get access to the symbol in this case (there are ways to get symbols out of objects that have them), this would require the user to do extra work specifically with the intention to cause problems and be a general nuisance.

Similarly, with respect to point 2, you as a user can also help prevent conflicts by using symbols for properties of objects that you didn’t create. HTML elements, for example, already have a large number of pre-existing property values, and different HTML elements can have different sets of properties. Additionally, with custom HTML elements, you have no idea what new or custom property keys may exist on any arbitrary element you pull from the DOM. If you want to add your own property to such an element, you might want to be sure you’re not accidentally overwriting something that’s already there. Symbols can allow you to easily do this.

let elem = document.getElementById('elem');
let meta = Symbol('metadata');
elem[meta] = 'If you can read this, you are a hacker, like me';

No matter what properties exist on the elem element, the meta symbol property is guaranteed not to collide with any of them allowing for a means to more safely add custom data to that element.

If you’re thinking you could just instead use a normal, yet very unique property name for cases like this, you’d be correct; that’s certainly an option.

let object = {};
object.__youll_never_catch_the_likes_of_me__ = 'so lonely';

But symbols also have the characteristic of being non-enumerable for normal object enumeration, keeping them out of loops like for...in. This can help hide them from other operations that interact with the object using these loops.

let object = {};
object.__youll_never_catch_the_likes_of_me__ = 'so lonely';
let hidden = Symbol('hidden');
object[hidden] = 'most lonely';

for (let key in object) {
    console.log('Found key: ' + key);
}
// Found key: __youll_never_catch_the_likes_of_me__

More info:

Internal Slots
Level: Advanced

JavaScript maintains hidden properties within your objects known as internal slots. These are used internally by the runtime for things like managing how the object behaves, or what kind of data it it might store internally. Internal slots are usually represented by a property name surrounded by two square brackets ([[<name>]]) but they are not directly accessible as properties to JavaScript code. For those that can be accessed from JavaScript, you’d need to go through an alternative API.

An object’s prototype is an example of something that is stored in an internal slot. When an object needs to see what it inherits, it looks to its internal [[Prototype]] which is the internal slot that stores its prototype object. While user code can’t access [[Prototype]] directly, there are APIs like Object.getPrototypeOf() and the __proto__ getter which will provide its value to JavaScript.

let map = new Map(); // map.[[Prototype]] = Map.prototype
let proto = Object.getPrototypeOf(map); // returns map.[[Prototype]]
console.log(proto === Map.prototype); // true

Internal slots usually hide in the background allowing you to ignore their existence as they secretly work to ensure your objects behave as they should. But sometimes the nature of internal slots work can cause unexpected problems.

Unlike normal object properties, internal slots are not inherited. If you create an object that inherits from another object with a certain internal slot, that slot will not be accessible from the new object. Map instances, like map in the example above, use an internal slot called [[MapData]] (which you may see represented as [[Entries]] in developer tooling) to store their key-value pairs. If you created an object that inherited from map, Map methods of that object would fail because of the lack of [[MapData]] in that new object.

let map = new Map([['Home', '37.7N-122.5W']]);
console.log(map.get('Home')); // 37.7N-122.5W

let fakeMap = Object.create(map); // new fakeMap object inherits from map
console.log(fakeMap.get('Home'));
// TypeError: incompatible receiver

Maps aren’t the only type that suffer from this problem. There are a number of other objects that would behave similarly, including, but not limited to:

Object.create(new Set()).size; // TypeError
Object.create(new Date()).getTime(); // TypeError
Object.create(new Number(1)).valueOf(); // TypeError
Object.create(Promise.resolve(1)).then(() => {}); // TypeError

While you may not be able to create working objects that inherit from instances of these types, using class syntax, you are able to subclass them.

class FakeMap extends Map {
    constructor (entries) {
        super(entries); // initializes this instance with Map internal slots
    }
}

let fakeMap = new FakeMap([['Home', '37.7N-122.5W']]);
console.log(fakeMap.get('Home')); // 37.7N-122.5W (not so fake anymore)

This version of fakeMap doesn’t fail when calling the Map get method because it was initialized with the proper internal slot expected by that method (and others like it) having gone through the FakeMap, and therefore Map, constructor.

More info:

Reduce Code with Object Shorthand Syntax
Version: ES2015
Level: Beginner

Object literals ({}) offer a couple of shortcuts that allow you to simplify and reduce redundancy in your code. The first we’ll look at is a shorthand for assigning properties in a new object to variables of the same name in the available scope. Not using this shorthand syntax, you’d have something like:

let wantsToBeAProperty = true;
let holderOfProperties = {
    wantsToBeAProperty: wantsToBeAProperty
};

This creates a wantsToBeAProperty property on the holderOfProperties object with a value equal to the value in the variable wantsToBeAProperty (true). The problem is, this code is redundant, causing you to write out wantsToBeAProperty twice for the property, once for the key and once for the value. With the property shorthand syntax, you’ll only need to refer to it once.

let wantsToBeAProperty = true;
let holderOfProperties = {
    wantsToBeAProperty
};

Simply including the property name without a colon (:) and a respective value, JavaScript will automatically give it the value of the variable of the same name.

Functions, or object methods, also have a shorthand syntax. Normal method properties include the property name followed by a function expression as a value.

let doerOfThings = {
    doAThing: function () {
        // does a thing...
    }
};

Using the method shorthand syntax, we can drop the colon (:) and the function keyword allowing us to more simply write:

let doerOfThings = {
    doAThing () {
        // does a thing...
    }
};

This creates a function much like before, but without requiring as much code. In fact, if you’re familiar with class syntax, you may recognize this is the same syntax used for class methods there.

More info:

Declarations let, const and class Can’t be Redeclared
Version: ES2015
Level: Beginner

When declaring variables with let, const, and class, you’ll need to be sure not to duplicate declarations of the same name in the same scope. Doing so will create an error.

let num = 1;
let num = 2;
// SyntaxError: Identifier 'num' has already been declared

This is not the case with var and function declarations. They can be repeated without producing an error.

var num = 1;
var num = 2; // Ok, num = 2

If you mix these declarations, for example using both let and var with the same identifier name, the let behavior will be used; that is, the redeclaration will not be allowed.

var num = 1;
let num = 2;
// SyntaxError: Identifier 'num' has already been declared

You can redeclare variables of the same name in different scopes, even if just an arbitrary block scope (when using let, const, and class which are block scoped).

let num = 1;
{
    let num = 2; // Ok, no conflict
}

Note: As of Chrome version 80, you can also redeclare let and class in multiple entries within the JavaScript console without there being an error. Attempting to redeclare a const, however, will still produce a SyntaxError.

More info:

Missing Semicolon ASI Gotchas
Level: Intermediate

For the most part, semicolons used for marking the end of a line of code are optional in JavaScript. If not added by you explicitly, they’ll be added for you automatically. This process is known as Automatic Semicolon Insertion, or ASI for short. If you wish to rely on ASI for your code rather than adding semicolons yourself, you should be aware of the following gotchas where ambiguity can cause ASI to behave unexpectedly, specifically where it is not inserting a semicolon when you might expect it to.

When a line starts with [ bracket access or array literal?

Consider the following:

let x = 1
[1,2,3].indexOf(x)
// TypeError: Cannot read property 'indexOf' of undefined

You might expect [1,2,3] to be an array literal, starting a new statement below the declaration of the x variable above it. However, in this case ASI does not add a semicolon after the first statement, so when executed, this code is seen as:

let x = 1[1,2,3].indexOf(x)

This then causes the square brackets to be seen as bracket access notation for the 1 which, in turn, treats the values inside as a comma-separated list resolving the last value in that list or 3. What you get is 1[3].indexOf(x) or, since Number values do not have a 3 property, undefined.indexOf(x) which causes the error.

This is a case where, to prevent this from happening, you would need to explicitly provide a semicolon after the first statement because ASI was not providing one for you.

let x = 1;
[1,2,3].indexOf(x) // 0

Now, the declaration of x is correctly terminated at the 1 and [1,2,3] is seen as an array literal. This means indexOf() will correctly be called from an array value rather than undefined.

ASI here, and in additional examples outlined below, can’t tell from the line following the declaration of x whether or not it is supposed to be used with the previous line or not since it would be valid syntax (despite possibly throwing a runtime error) in either case. What it ends up deciding to do is not including an automatic semicolon - something which may actually be right in some cases. It just happens not to be right in this case.

Starting a line with an array literal is one of the more likely circumstances you might run into this, but there are a few others as well…

When a line starts with ( function call or grouping?

let x = 1
(() => x)()
// TypeError: 1 is not a function

Here, the first set of parenthesis containing the function are treating 1 like a function, calling it as 1(() => x). With an explicit semicolon, the parens are a group wrapping the function rather than being seen as a function call:

let x = 1;
(() => x)() // 1

When a line starts with / division or RegExp literal?

let x = 1
/abc/i
// ReferenceError: abc is not defined

This is being seen as the division of the values 1 / abc / i, but since abc (or whatever other character string you might have in a regular expression) is not a defined variable, we get an error. With a semicolon, the forward slashes are seen as the start of a regular expression literal:

let x = 1;
/abc/i // /abc/i

When a line starts with ` tagged template literal or template literal?

let x = 1
`text`
// TypeError: 1 is not a function

Here, 1 is being seen as a template tag (which should be functions) for the template literal below it. With a semicolon, its an untagged template string:

let x = 1;
`text` // "text"

When a line starts with + unary + or addition?

let x = 1
+'1'
// x = "11"

This is one statement assigning x to 1 + '1'. With a semicolon, there’s two statements, the second being a unary + which converts the string “1” to the number 1:

let x = 1;
+'1' // 1
// x = 1

When a line starts with - unary - or subtraction?

let x = 1
-'1'
// x = 0

This assigns x to 1 - '1'. The semicolon again allows separation, with the second statement being a unary - which converts the string “1” to the number -1:

let x = 1;
-'1' // -1
// x = 1

More info:

typeof null === "object"
Level: Beginner

The typeof operator is used to identify the basic type of a value in JavaScript. It can individually identify primitive types as well as recognize any other object type as being an object (with a special case for recognizing function objects as functions).

console.log(typeof 1); // number
console.log(typeof "text"); // string
console.log(typeof undefined); // undefined
console.log(typeof {}); // object
console.log(typeof function () {}); // function
// ...

One particular quirk with typeof is that it will report the type of the value null as “object”.

console.log(typeof null); // object

Because of this, if you’re using typeof to check for objects in your code, you may also want to include a check to make sure it is not also null. Otherwise, you might be accidentally using a null value as an object in a way that could cause errors.

if (typeof someValue === "object" && someValue !== null) {
    // I don't object to the use of this object
}

More info:

Overriding Constructor Returns
Level: Advanced

Constructors are used to make new object instances. When you see the new keyword followed by an object type (represented by a constructor) you’ll know that an instance of that type is being created.

let noodleInstance = new Noodle();

But in actuality, this may not necessarily be the case. Objects returned from constructors called with new may return something else entirely. While the default, implicit behavior of constructors is to return the object instantiated by that constructor function, it is possible for constructors to override that return with a different object value by specifying an explicit return.

class Noodle {
    constructor () {
        return new Rice();
    }
}

let riceInstance = new Noodle(); // (could it be a rice noodle?)

You should do this sparingly, however, as it violates the expectations set forth by the constructor. That is, if you see new something, you should expect to get an instance of that something in return. Nevertheless, there are cases where it can become useful. One example is with object pooling, the process by which instances that are no longer needed are kept in memory and reused for future new instances rather than having to recreate those new instances again from scratch.

class Noodle {
    constructor () {
        if (Noodle.pool.length) {
            return Noodle.pool.pop(); // use pooled instance
        }
        // otherwise implicitly returns new instance
    }

    destroy () {
        Noodle.pool.push(this); // in pool for future new instances
    }
}
Noodle.pool = []; // pool is static property on Noodle class

let newNoodle = new Noodle(); // new instance
newNoodle.destroy(); // done using newNoodle, adds to pool

let wetNoodle = new Noodle(); // gets instance from pool
console.log(wetNoodle === newNoodle); // true

Here, pooling is used to override the return value of the constructor with values from the pool if available. And in doing so, it’s continuing to return Noodle instances which is what someone calling new would expect to get from the constructor.

Note that in returning an explicit object from a constructor like this, you’re not stopping the original object from being created. Even when a pooled instance of Noodle is returned, a new Noodle is still getting instantiated and assigned to this as part of that constructor getting called (though we’ll look at an exception to this later on). If you wish this not to happen, you may want to consider using a factory function instead.

class Noodle {
    static create () {
        if (Noodle.pool.length) {
            return Noodle.pool.pop(); // use pooled instance
        }
        return new Noodle(); // use new instance
    }
    // ...
}
// ...
let newNoodle = Noodle.create();
// ...

More info: