5 interesting and not-necessarily-useful Javascript tricks
Arek Nawo
Posted on November 19, 2019
This post was taken from my blog, so be sure to check it out for more up-to-date content.
A while ago, I've created a post titled "7 adorable web development tricks". There, I described some interesting tricks that you could pull off using one of 3 major web technologies - HTML, CSS, and JavaScript. And, I must admit - you guys seemed to like it! And so, here comes the sequel!
This time, to be a bit more consistent, I decided to focus solely on JavaScript. It's probably the most interesting and versatile out of the 3, so there's a lot to talk about. We'll go over 5 JavaScript tricks, that weren't mentioned in the previous article. I hope you'll find them interesting!
A quick note before we get into the list. I saw some replies to the previous post and would like to clear something up. I know that not all entries on this or the previous list might be truly useful or a recommended practice, but that's not my goal. By the word "trick" I mean just that - a "trick" that's interesting or worth knowing just for the sake of it. Usefulness is just a bonus. If it was meant to be 100% useful, then I'd call it a "tip". I hope you understand. Now, let's go to the list!
5. Switch with ranges
Starting with the less "extreme" trick, we've got the switch
statement. Most of its use-cases come down to string or numeric value matching. But, did you know that you can use it with some more complex boolean values too? Take a look.
const useSwitch = value => {
let message = "";
switch (true) {
case value < 0:
message = "lesser than 0";
break;
case value === 0:
message = "0";
break;
case value % 1 !== 0:
message = "a float";
break;
case value > 0 && value <= 9:
message = "higher than 0 and is 1 digit long";
break;
case value >= 10 && value <= 99:
message = "2 digits long";
break;
case value >= 100:
message = "big";
break;
}
console.log(`The value is ${message}`);
};
useSwitch(24); // The value is 2 digits long.
Instead of providing the switch
statement with an actual variable, we're simply passing true
. This way, we essentially make it one big if-else
alternative. If you should use it depends solely on your personal preference or the code guidelines you're following. If you find it more readable than an if-else
chain, go for it. The performance of both solutions is about the same.
4. Stringify a function
Next up we have something that’s not really a trick by itself. Stringifying a function is a feature that you've most likely known about for a long time now. Instead, I wanted to let you know of some interesting use-cases for this kind of functionality.
const func = () => {
console.log("This is a function");
}
const stringifiedFunc = `${func}`; /* `() => {
console.log("This is a function");
}` */
Starting with a quick look at the syntax side. You should know that when you convert a value of any JS type to a string, a special .toString()
method is invoked. You can use this fact to implement your own version of this method and handle converting your JS value to a string differently. This can be considered a trick on its own. ;) Anyway, the point I wanted to make is that you can convert your values (including functions) to a string with ES6 template literals (like in the example), by concatenating them with an empty string literal (""
), or just by calling the .toString()
method directly.
Now, let's get back to functions. I wanted to note that you cannot depend on the result string to contain all the code of your function as it was written. For example, it's only from ES2019 (currently the latest revision of ECMAScript standard), that .toString()
is meant to include all the comments and whitespaces inside the function's body in the resulting string. You can read more about ES2019 features in one of my previous articles. Anyway, with all this in mind, how stringifying a function can be even useful?
Not to search too far, I'd like to reference a neat trick that I've used in one of my recent side-projects. Imagine, that there is a kind of nodes that can be created by calling a function. This function takes another function as a parameter, which is then run to configure the new node. The resulting nodes are the same for functions that consist of the same statements.
Sadly, creating new nodes is a slow process (especially when considering large quantities of them), and you'd like to at least minimize the number of nodes being created. To do this, you can e.g. create a "cache" object, where you'd store all the already created nodes by their stringified config function, to prevent any repetitive calls - interesting, huh?
Of course, the stringified function-based IDs would be considered different even with a tiny whitespace or a comment. You could fix it with some additional string processing, but that would neglect all the performance improvements that we're trying to achieve.
However, you shouldn't tolerate object keys being as long as the config functions are. You can easily solve this issue by simply hashing the stringified function - it shouldn't cost you a lot performance-wise.
// ...
const hash = value => {
let hashed = 0;
for (let i = 0; i < value.length; i += 1) {
hashed = (hashed << 5) - hashed + value.charCodeAt(i);
hashed |= 0;
}
return `${hashed}`;
};
const hashedFunc = hash(stringifiedFunc); // "-1627423388"
I know that what I've just described might seem a bit too specific to be applied to more general use-cases. Surely that's somewhat true, but I just wanted to give you a real-world example of possibilities that tricks like this one give you.
3. Callable objects
A callable object, a function with properties, or whatever you want to call it is a fairly simple idea that demonstrates the versatility of JavaScript pretty well.
const func = () => {
// ...
};
func.prop = "value";
console.log(func.prop); // "value"
The snippet above shouldn't seem any special to you. You can save own properties on pretty much any JS objects, unless it's indicated otherwise with the use of .freeze()
, .seal()
, or the .preventExtensions()
method. The function above can now be used both as a usual function, but also as an object containing some sort of data.
The code snippet above doesn't look very polished though. Assigning properties to the given function can start to feel repetitive and messy with time. Let's try to change that!
const func = Object.assign(() => {
// ...
}, {
prop: "value"
});
console.log(func.prop); // "value"
Now we're using the Object.assign()
method to make our code look better. Of course, this method is available only in ES6-compatible environments (or with a transpiler), but, as we're also utilizing arrow functions here, I just take it for granted.
2. Lexically-bound class methods
Let's say that we've got a class with a lot of fields and methods. You can imagine yourself in such a situation, don't you? What if, at the given moment, you only need a small subset of all the class properties and methods? Maybe you could use the ES6 destructuring assignment to make your code look better? Sadly, it's not that easy - take a look.
class Example {
method() {
console.log(this);
}
}
const instance = new Example();
const { method } = instance;
method(); // undefined
As you can see, after we extracted our method, the value of this
changed to undefined
. That's expected behavior - the value of this
is runtime-bound and determined by the way and place that your function was called in. I discussed this in my previous post.
There's a way around, however - .bind()
.
class Example {
constructor() {
this.method = this.method.bind(this);
}
method() {
console.log(this);
}
}
const instance = new Example();
const { method } = instance;
method(); // Example {}
Now our code works as intended, though it required the addition of the class constructor, and thus a few more lines of code. Can we make it shorter?
class Example {
method = () => {
console.log(this);
}
}
// ...
It seems like we've done it! A short and easy way to have lexically-bound methods inside your classes. The syntax above works in the latest ever-green browsers and can be transpiled if necessary, so enjoy!
1. Return from constructor
The last entry on this list is also connected with classes. You might have heard of the possibility of returning custom values from the constructor. It's not a very popular or recommended practice, but it allows you to achieve some interesting results. Remember the previous example of cached nodes that I brought up before? Let's build on that!
// ...
const cache = {};
class Node {
constructor(config) {
const id = hash(`${config}`);
if (cache[id]) {
return cache[id];
} else {
cache[id] = this;
config();
}
}
}
const node = new Node(() => {});
const nodeReference = new Node(() => {});
const secondNode = new Node(() => {
console.log("Hello");
});
console.log(node === nodeReference, node === secondNode); // true, false
Our node now has a form of a class, and like before, it can be cached with the use of stringified & hashed config function. How nice to see all the pieces coming together!
Something new?
So, that's it for this list. I know that it's not the longest one that you've seen, but hey, at least I managed to get you interested, right? Anyway, let me know in the comments section about which of the above tricks you didn't know? Also down there you can share your opinions on such a type of article and if you'd like to see more of them. ;)
So, if you like this post, consider sharing this post and following me on Twitter, Facebook or Reddit to stay up-to-date with the latest content. As always, thank you for reading this, and I wish you a happy day!
Posted on November 19, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024