DEV Community

Nicolle Romero
Nicolle Romero

Posted on • Originally published at Medium on

Foxy JavaScript

How to outfox some of the trickiest aspects of the language

Photo by Alex Andrews from Pexels

It’s common knowledge that JavaScript is a highly nuanced, fast and loose programming language that can result in some head shaking results.

Based on my journey into the language so far, this is what I’ve observed are the Top 10 Slyest Quirks of JavaScript.

#1 Implicit Coercion

Notably, this quirk is one of the most infamous parts of the language. To oversimplify, implicit coercion is JavaScript’s ability (or deranged propensity?) to coerce one value type to another type. Some would argue that the value is coerced to the expected type. We get these fun little nuggets as a result of implicit coercion:

1 < 2 < 3; // --> true 

// but...   

3 > 2 > 1; // --> false

9 + '1' // --> '91'

// but...

91 - '1' // --> 90
Enter fullscreen mode Exit fullscreen mode

It gets even odder when working with arrays:

[] == ![]; // --> true

true == []; // --> false
true == ![]; // --> false

false == []; // --> true
false == ![]; // --> true
Enter fullscreen mode Exit fullscreen mode

Head-scratching for sure. There’s actually some interesting logic behind these coercions (based on the operation being performed and the operand types that are involved), but it’s pretty opaque to someone new to the language. If you get some surprising results, be on the lookout for this (and better yet, test any questionable operations being performed in your handy REPL). Also, get in the habit of using the strict equality operator === when comparing values or checking a variable’s type with the typeof keyword.

#2 Semicolons

This is a touchy subject, but for the beginner programmer, it’s more than an annoyance. It’s difficult to know when not using them will cause your code to implode. So, we’ve had the fear of God scared into us, and we just add them religiously. Point taken.

Take a look at this example:

let students = ''

['Sam', 'Ari', 'Sharon', 'Steve'].forEach((name) => {

    if (name !== 'Steve') {
      name += ' & ';
    }

    students += name;

});

console.log(students);

// expected: 'Sam & Ari & Sharon & Steve'
Enter fullscreen mode Exit fullscreen mode

Instead of the expected output of a string with alternating names and ampersands, it throws the error “Cannot read property ‘forEach’ of undefined.” Because a semicolon is missing from the end of the first line, the code is not parsed correctly, and the first line essentially bleeds into the second line:

let students = ''['Sam', ...].forEach(...)
Enter fullscreen mode Exit fullscreen mode

To avoid this issue, you need get in the habit of terminating every statement with a semicolon. Period. (Or, I guess, semicolon?)

#3 Elusive Return Values

Question: What does this function return?”

“Answer: A new array? Itself, just mutated? The length of the array? Undefined?”

Some methods, such as Array.prototype.map() and Array.prototype.filter() return a brand new array of elements with the intended changes implemented in the new array. Great! Just as we expected.

Other methods, such as Array.prototype.sort() and Array.prototype.reverse() mutate and then return the original array. As a n00b, how do you know exactly what a method will return? Well, there’s no easy way to know what a method will return unless you commit it to memory or look it up (or test it in your REPL). Honestly, it has more to do with when the methods were added to the language than the underpinnings of the methods themselves, but a little consistency would be a welcome relief to someone new to the language.

#4 Array.prototype.forEach() and Array.prototype.reduce()

You know that awkward moment when you bring something up at a party that makes everyone in a previously chatty group go quiet? Say something strongly worded about either of these methods at a local JS Meetup, and you’ll know what I mean.

This is an interesting dilemma, and it might be too early for a developer to pick a side if they’re just getting their feet wet. Some argue that forEach is problematic because it primarily relies on side effects to get the job done. Case in point, it’s important to note (and commit to memory) that forEach always returns… undefined (surprise!) and not itself, so you can’t chain it like many other array methods. This may come back to bite you at some point. From a best practices standpoint, other methods that don’t depend on side effects (e.g., changes in state that don’t depend directly on the function inputs), are easier to read and predict because they have clear semantic meaning. In this case, less is more.

let newArr = [];

[1, 2, 3].forEach((element) => {
  if(element > 1) {
    newArr.push(element);
  }
});

console.log(newArr); // [2, 3]
Enter fullscreen mode Exit fullscreen mode

Compare this to the same task using Array.prototype.filter():

let newArr = [1, 2, 3].filter((element) => element > 1);

console.log(newArr); // [2, 3]
Enter fullscreen mode Exit fullscreen mode

Array.prototype.reduce() is an interesting, and powerful method, but some would argue that it makes the code more difficult to parse (for humans). It essentially “reduces” an array down to a single output value. This can make calculating a sum or removing duplicates from an array very streamlined, but it comes at the expense of less readable code. I think it’s valuable to become familiar with the method, but keep the pros and cons in mind.

#5 Spread Operators (AKA “…”)

This is the perfect syntax to use if you want to build anticipation...

All joking aside, the spread operator is useful, but it’s not super clear when it should be used. MDN states:

Spread syntax allows an iterable such as an array expression or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.

The spread operator performs differently in different contexts. Here are just a few situations where you’ll find the spread syntax useful.

  • It effectively “spreads” an array (or more than one array) into separate arguments when used in a function call. For example, when calling Math.max() or Math.min():
let arr = [1, 3, 2, 0];

Math.max(...arr); // 3
Enter fullscreen mode Exit fullscreen mode
  • With more than one array:
let arr1 = [-1, -2, -3, -4];
let arr2 = [10, 3, -8, 1, 9];

Math.max(...arr1, ...arr2) ); // 10
Enter fullscreen mode Exit fullscreen mode
  • Or it can be used to easily copy an array:
let arr = [1, 2, 3];

let newArr = [...arr];

console.log(newArr); // [1, 2, 3]

arr === newArr; // false
Enter fullscreen mode Exit fullscreen mode
  • It can also be used to expeditiously split a string into an array:
let string = "Hello!";

console.log( [...string] ); // ['H', 'e', 'l', 'l', 'o', '!']
Enter fullscreen mode Exit fullscreen mode

It’s important to distinguish the spread operator from a different construct, rest parameters, since they both use the same syntax of "..." .

  • Rest parameters are used in functions to allow them to accept any number of arguments. You can spot rest parameters when they’re used at the end of function parameters when a function is declared.
  • Spread syntax is used when passing in an array (or multiple arrays) during a function call.

#6 Implied Globals

New programmers are continually warned not to “pollute the global scope,” but what exactly does that mean and how does the concept of implied globals play into that? When variables or functions are inadvertently named the same thing in the global scope, we refer to the problem that causes as name collision. This can happen when multiple teams are contributing code to the same project. Projects are prone to name collision if too many variables and functions are added to the global namespace, also referred to “polluting the global scope.”

To make matters worse, you can very easily add variables to the global scope unintentionally due to the occurrence of implied globals. Basically, if you don’t follow best practices and forget to explicitly declare your variable, it is interpreted by the language that you meant to declare it as a global variable:

You type:

sum = 10;
Enter fullscreen mode Exit fullscreen mode

JS interprets that as:

const sum = 0;
Enter fullscreen mode Exit fullscreen mode

Or another example (of chaining variable assignments):

const findSum = (a, b) => {

  let sum = total = a + b; 

};
Enter fullscreen mode Exit fullscreen mode

In this example, sum is scoped as a local variable, whereas total is scoped as a global variable. Oops.

Word to the wise: define each variable explicitly and keep your global scope tidy.

#7 Double NOTs

Despite there being a perfectly acceptable and readable built-in Boolean function (which casts most anything to a boolean), the double not, or !! is clear as mud. It also forces a value to a boolean:

!!0 
!!null
!!undefined

...all return the boolean false. Nifty.
Enter fullscreen mode Exit fullscreen mode

Here’s a joke that helps explain it:

A linguistics professor once said “A double negative forms a positive, while in some languages, a double negative is still a negative. However, there is no language wherein a double positive can form a negative.”

A voice from the back of the room piped up, “Yeah, right.”

In a nutshell, the first ! converts the value into a boolean and it returns the inverse. The second ! will inverse that boolean, and in this convoluted manner, it converts the value to a boolean.

Can’t hardly find no fault in that.

#8 Variable Shadowing

This quirk isn’t unique to JavaScript, but it will get new programmers every time. It’s taught that you should pick the most descriptive variable names as possible, but what if that leads you to use the same variable name to declare a variable in the global scope and again in the local scope? That’s when it gets messy. The locally scoped variable essentially shadows, or overrides the previously scope global variable. Here’s an example:

let num = 3;

function example() {
    let num = 1;
    num += 10;
    return num;

}

console.log(num); // 3
Enter fullscreen mode Exit fullscreen mode

Did you expect to see an output of 11? In variable shadowing, if you declare a variable by the same name in both the local and global scope, the local variable will take precedence when you use it within a function. Shadowing can make reading code confusing and it also makes it impossible to access the global variable from within the function scope. Linters often have a “no-shadow” rule for good reason. Beware of lurking variable shadowing.

#9 Hoisting

For me, this term always invokes the image of a horse in a gurney (Phish fans?). It doesn’t sound elegant, but it’s a powerful part of the JavaScript language: no matter where variables are declared within a particular scope (local or global), all variable declarations are moved to the top of their scope. Like something out of Inception, it means that variable declarations are always processed before any code is executed (only applies to the declaration, not the assignment).

Related to #8 above, this can catch some developers off guard. It can lead to behaviors that you weren’t expecting if you’re unaware of hoisting: rather than being available only after their declaration, variables and functions might be available beforehand in the code, which might have some unanticipated consequences (both positive and negative).

For this reason, it’s advisable to get in the habit of declaring all variables at the top of their scope (for global scope or at the top of the local function scope).

#10 Using typeof on null results in “object”

smh…

Honestly, this is just accepted as a bug in the language, but if you’re curious about the history of it, you can find out more here.


Top comments (0)