Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris
TLDR; this is not a critique against JavaScript, it's just acknowledging the fact that it differs a bit from OO languages and you can either curse JS Or you can use the patterns that are made possible through that, to your advantage.
I love the language but it works differently than other languages I'm used to.
Regardless if you are a beginner to JavaScript or beginner to programming there are things in JS that might surprise you. Just because it surprises you doesn't mean it's wrong, it's just different, quirky, or perfectly sane, depending on what your previous experience is. Each one of the upcoming topics deserves their own article or even book, almost, but here goes:
-1- Really really equals
If you learned to code in some other language maybe Java, you've learned that one =
means an assignment and ==
compares. In JavaScript, you have both ===
and ==
for comparing equality. Which one to use? What's the difference? ==
compare values only. Consider this example:
if('2' == 2) {} // true
It returns true
when it's the same value but the type differs.
Look at this example now:
if('2' === 2) {} // false
if(2 === 2) {} // true
Above the ===
detects that '2'
and 2
have different types and therefore evaluates to false
. It's generally recommended to be using this way of doing comparison.
-2- There are many ways to create an object
In Java or C# you have a class. From that class, you can instantiate an object. It makes sense. JavaScript gives you more options. There you can create an object in the following way:
-
Using a class, There is the keyword
class
that you use to define fields, methods, getters/setters all within the context of a class. Here's an example:
class Person {
constructor(n) {
this.name = n;
}
getName() { return this.name; }
}
-
Object literal, You can define an object without defining a class. All you need is
{}
. It can look like so:
const person = {
name: 'chris',
city: 'location',
getAll() {
return `${this.name} ${this.city}`;
}
}
-
Object create, you can use the method
Object.create()
to create an object. It takes a prototype object to base it off of. Here's an example:
const address = {
city: '',
country: ''
}
const adr = Object.create(address);
adr.city = 'London';
adr.country = 'UK'
console.log(adr.city); // London
console.log(adr.country); // UK
Block statements, look ma no scope
Block statements, if
, for
, while
etc, don't create a local scope. That means whatever you create in there is accessible outside of the statement, like so:
for (var i =0; i< 10; i++) {
console.log(i);
}
console.log(i);
The last console.log()
will print 10
. This might surprise you.
Yes, why is JS designed so that is
i
still alive?
Ask Brendan Eich, it's a feature :)
To make JS behave like other languages you might know, you need to use a let
or a const
, like so:
for (let i = 0; i< 10; i++) {
console.log(i);
}
console.log(i);
Running this code now states i is not defined
. Why did this work? Well, let
allows you to declare variables that are limited to the scope of a block statement. So it's the usage of the keyword let
over var
that does this rather than the block statement being given a scope. (Thanks to Will for this comment)
Phew
-3- Context, what's the value of this
You might have heard the jokes that no one knows what this
is. Starting out with an empty file this
is the global context. Consider the following code:
global.name = "cross";
function someFunction() {
console.log(this.name);
}
someFunction();
Above we are assigning name
to the variable global
(that's what we call it in Node.js, on the frontend it would be window
). The value of this
comes from the global context.
Let's look at a different example below:
var object = {
name: 'chris',
getName() {
console.log(`${this.name}`);
}
}
object.getName();
Here, the value of this
is the object itself, it knows what name
is, i.e the value chris
.
Changing context
We can change what this
is. There are some helper methods in JavaScript that allows us to do that bind()
, call()
and apply()
. Consider this example again but with object
added:
global.name = "cross";
var object = {
name: 'chris',
getName() {
console.log(`${this.name}`);
}
}
function someFunction() {
console.log(this.name);
}
someFunction();
We can alter this
from the global context to that of object
. Below we showcase how anyone of the mentioned methods can use this principle:
someFunction.bind(object)();
someFunction.call(object)
someFunction.apply(object)
It will now print chris
, instead of cross
.
These three methods are used in a little bit different ways normally but for this example, they are pretty equivalent.
The this
confusion
Ok, so when are we actually confused what the value of this
is? It happens in more than one place, but one common place is when we try to use a constructor function to create an object like so:
function Person(n) {
this.name = n || 'chris';
function getName() {
return this.name;
}
return {
getName
};
}
const person = new Person();
console.log(person.getName()) // undefined
This is because the this
changes for inner functions once you use new
on it. There are different solutions to fixing this:
Solution 1 - this = that
A way to approach this is to make it remember the value of the outer this
. Rewrite the above example to look like this:
function Person(n) {
this.name = n || 'chris';
var that = this;
function getName() {
return that.name;
}
return {
getName
};
}
const person = new Person();
console.log(person.getName()) // 'chris'
It fixes the issue by introducing the that
variable that remembers the value of this
. But there are other solutions.
Solution 2 - Arrow function
function Person() {
this.name = 'chris';
const getName = () => {
return this.name;
}
return {
getName
}
}
const person = new Person();
console.log(person.getName()) // 'chris'
The above replaces the function
keyword for an arrow function =>
.
Solution 3 - Use a closure
The third solution is to use a so-called closure
. This involves not using the new keyword but relies on the fact that JavaScript barely needs to use this
. Consider the below code:
function Person() {
var name = 'chris';
const getName = () => {
return name;
}
return {
getName
}
}
const person = Person();
console.log(person.getName()) // 'chris'
Above this
has been completely removed. We are also NOT using new
. IMO this is the most JavaScript-like pattern to use.
Solution 4 - put method on the prototype
In this approach, we use a class:
function Person() {
this.name = 'chris';
}
Person.prototype.getName = function() {
return this.name;
}
const person = new Person();
console.log(person.getName()) // 'chris'
This is a good solution for more than one reason. It solves the this
problem but it also makes sure that the method is only created once, instead of once per instance.
Solution 5 - use a class
This is quite close to the fourth solution:
class Person {
constructor() {
this.name = 'chris'
}
getName() {
return this.name;
}
}
const person = new Person();
console.log(person.getName()) // 'chris'
For this article to be crazy long I can't name all the possible cases where this
isn't what you think it is. Hopefully, these solutions offer you an insight into when it goes wrong and approaches to fix it.
-4- const
works, but not the way you might think
There's the const
keyword, we've seen how it creates a local scope. But wait, there's more :) The word const
makes you think it will always have this value, it's a constant, unchanging etc. Weeell.. Looking at the following example:
const PI = 3.14 // exactly :)
PI = 3;
The above gives me the error Assignment to a constant variable
.
So I can't change it, goood
Let's look at another example:
const person = {
name: 'chris'
}
person.name = 'cross';
This works without a problem :)
Wait whaaat. You said the value couldn't change?
Did I say that? I didn't say that. I said the word const
sounds like it. What const
means is that there is a read-only reference, i.e the reference can't be reassigned. I never said it can't be changed. Look at this for clarification:
const person = {
name: "chris",
};
person = {
name: 'chris'
}
The above gives an error. Cannot assign to a constant variable
.
Hmm ok, can I make it immutable?
Well you can use Object.freeze()
like so:
Object.freeze(person)
person.name = "cross";
console.log(person.name) // 'chris'
Great so there is a way to make everything immutable
Weeell.
Well what?
It only freezes on the first level. Consider this code:
const person = {
name: "chris",
address: {
town: 'London'
}
};
Object.freeze(person)
person.name = "cross";
person.address.town = 'Stockholm';
console.log(person.address.town) // Stockholm
But but.. is there no way to freeze it?
You would need a deep freeze algorithm for that. Ask yourself this though, do you need that? I mean in most cases your constants are usually primitives.
...
To be fair this is a little how const
works in other languages as well. I mean in C# it's static readonly
if you want something immutable and locked reference, in Java you need final
.
Yea yea whatever
-5- There's life after function invocation
Let's look at the following piece of code:
function aFunction() {
let name = 'chris';
console.log(name) // prints chris
}
console.log(name)
Nothing special with it, it doesn't know what name
is in the last console.log()
cause it's outside the function. Let's modify it slightly:
function aFunction() {
let name = "chris";
return {
getName() {
return name;
},
setName(value) {
name = value;
}
}
}
const anObject = aFunction();
console.log(anObject.getName());
anObject.setName("cross");
console.log(anObject.getName());
At this point it prints chris
calling getName()
, ok you might think it was bound to a value. Then you call setName()
and lastly you call getName()
again and this time it prints cross
. So why is this surprising? Well think about how a function normally works, you call it and the variables in it seize to exist. Now look at the above code again and notice that the name
variable seems to still exist long after the function has stopped executing. This is not really surprising if you compare it to a language like Objective-c for example. You are then used to referencing counting, if some part of the code is no longer referencing something it is garbage collected. You are clearly still referencing it via the anObject
variable.
But still, if you come from an OO background you might be used to objects holding a state and that the state lives on the object itself. In this case, name
lives in the lexical environment outside of the object, that's trippy right? ;)
The easiest way to think about this one is object creation with private variables. It's also how I create objects more and more these days.. Nothing wrong with classes though, whatever floats your boat :)
Summary
I'd love your comments on other things that may surprise you/trip you up or makes your life better. Cause that is true for me about a lot of things JavaScript - I type a lot less.
Top comments (20)
Well-written and well-explained. I haven't written JS in production in over two years, but JS is still a foot-gun when not doing TS. My favorite foot-gun when doing ES5 that is devoid of a set or map in its standard library is having to employ objects as dictionary-like data types:
The foot-gun here is that
0
is falsy and we won't get0
printed. And objects have been used to maintain locations in arrays which are zero-indexed, so when going to check for membership about whether some character exists at index0
results in, well, an unexpected result...In the section about block scope, I found it interesting that you explained it like this:
Is there a reason that you decided to explain it that way? Compared to explaining that
var
doesn't adhere to local scope andlet
does? I just thought it might give more clarity that it's the variables rather than the actual blocks that are concerned with scope.I really enjoyed how you explained the
this
issue that people run into when learning JS. It is extremely common and the solutions you gave are spot on! I remember banging my head against the wall trying to understand it!hey Will.. I've seen many articles referring it as
if
,for
,while
, doesn't create a new scope but function does, here for example scotch.io/tutorials/understanding-.... I mean I totally see your way as well, especially with MDN saying this aboutlet
,let allows you to declare variables that are limited to the scope of a block statement
. Yea I'll see if I can amend this in a good way, appreciate the comment :)You should probably also mention that
gets you
1 2 3
, whereaswill output
4 4 4
. IMO this is one of the major reason why scope of loop variable matters.The You Don't Know JS series of books (soon with a second edition called You Don't Know JS Yet) by Kyle Simpson should be required reading for any JS dev, for these and many more reasons.
Also (don't @ me, TypeScript devs), treat JS like a statically-typed, class-based language at your own peril. Do enforce (and document) types as needed. Do not waste too much time on static type checking that's just going to disappear in the transpiler. Do learn prototypal inheritance, and definitely do not try to force or hack JS into class inheritance.
I agree on Kyles books, they are great. I think it's important to be inclusive and let people use what they want.. but I also agree it's important to learn the foundations of the language. As for TypeScript itself: There are a lot of TypeScript devs that don't use classes. TypeScript is also well supported for React, Vue and Angular. I find personally it pays off in very large codebases... Sounds like you had some bad experiences using TypeScript.
I appreciate some of the things TS has pushed forward in ES, but classes are IMO a concession to devs who couldn't handle not having them, and never should have made it into the spec (sugar or otherwise).
My biggest beefs with TS are:
And possibly worse than the false sense of security:
Basically add in every point (Node.js and Deno creator) Ryan Dahl made recently in explaining why Deno moved away from TS internally.
I've been warning about all of these points regarding TS for years, even while using it and appreciating some of its features when doing so.
Deno moved away from TS... But moving to Rust. An even more strict statically typed language.
The difference is that Rust is strictly typed by default, not a layer on top trying to hack the underlying language into behaving like a statically typed one.
Hard to admit? No, I'm not that type. If my example is wrong, I'd amend it. My example is a valid foot-gun. But I'm sure my example flew over your head as your
console.log
uses back-ticks and interpolation which isn't a feature in a JS version that you don't seem to be familiar with. Your suggestion would be a terrible idea. I even gave you credit for suggesting it, because although it works, the intent is unclear. But if you want to go with personal attacks, so be it. You're on your own. I'm sure you're quite the team member. "We can lead a horse to water, but we can't make it drink". Cheers, matey!You might want to replace C/C++ with C#. Neither C nor C++ have native reference counting or garbage collection.
fair.. I must have been thinking about Objective-c..
Whoops — misread. I don’t see any reason anyone would write code like this. Moreover, your particular example doesn’t even quite express to the next programmer reading it why this is even needed. The exception being that you are debugging and wondering about what values are falsy. For the scenario that I illustrated, it’s better to write an if-expression checking the object key’s value for zero leaving absolutely no ambiguity
What sort of twisted world do you live in, Petar? You seem to want to construct a world in which you can make wrongful assumptions, craft lies, so that you can play hero. I've learned nothing but how much of an asshole you seem to be. I'm glad I don't have to maintain any code that you write. Perhaps only the most purist of JavaScripters that are all-knowing like Petar can team with him.
The tool is worth blaming - show me another language where you have to do the "hack" you've done. And you can even only do your hack when you're aware of it in the first place. So my bad if you're a "JavaScript Ninja". I'd rest my sword first. I try not to pick fights I cannot win. Ever read the book, "JavaScript - The Good Parts"? Wonder why it is even titled that way? Probably not. The tool is always right in your world? Humble yourself, Petar. Don't bark at every dog that passes by alright? Could you cite where I blamed that the spec was wrong? Yeah, I didn't think so. You read specs? Nah -- didn't think so either. You write libraries and contribute to open-source? Nah, didn't think so either. You know why I say all this? There are people that write good code behind closed doors, but I highly doubt that you're one of them sharing such an absurd example that does more harm than good. Moreover, because of your lack of community contributions, I am pretty certain that you rarely take others' accounts into consideration. You write code as you please that only fits in your warped understanding of the world that no one else can maintain. That's fine -- people do that. But don't come at me spouting your bullshit and making shit up just so you can start a fight. Not cool
Wow! Hell of a stretch, dude! You've obviously missed the entire premise -- that there is a pitfall here. You decided to butt in here with a "smartass" remark about how someone can print falsy values which is completely divorced from what I'm pointing out -- that most people will end up shipping code in an older version of JavaScript (ES5 which I clearly specified) and not realize that there's a bug or don't even know where to look if you're "dabbling" in JavaScript. Most JavaScripters know that 0 is falsy, but forget about it in certain contexts, so you're just being a complete smart-ass and an asshole for driving the conversation away from that. The only way that they can use your example is 1. that they are aware of this scenario or 2. they found the bug and employ this as the fix. No other language that I work with does this and that's a lot of them.
Again, I rest my case -- you must have many friends in your confrontational warped view of the world. Don't forget to pray at the ECMAScripten altar before you sleep tonight. Light some incense while you're at it... I heard you fart. Happy to entertain. I do it pro-bono. I get kicks out of it, too -- it's probably the most social interaction you've gotten in the past few months.
And lastly, I really don't mind if I'm wrong -- I encourage people to call me out when I'm wrong. In fact, I've made many many mistakes in my code before -- wrong assertions, wrong abstractions, each with their own learning lessons, but this was purely entertainment. I swallow my pride from time to time, but I try not to disseminate poor and uninformed advice and certainly don't insult people unless I'm insulted first such is the case here.
Object.freeze(person) will alone not freeze the person object(even top level properties). It returns a new object whose top level properties are frozen.
Please verify before posting.
I don't agree.. Try this code
You will see it outputs
1
. I.e I'm unable to change the code... so I'm not sure what you are talking about to be honest? See MDN even developer.mozilla.org/en-US/docs/W...Oh yeah , My bad you're right
Acc to docs it says
and not what I claimed it to be
Sorry for wasting your time, I was just drained for the day I guess.
appreciate you trying to help to correct the article.. and taking the time reading it.
Nice -- but this is not an expression available in ES5
Some comments have been hidden by the post's author - find out more