Professional Documents
Culture Documents
Welcome back! Oh man am I super excited about this tutorial. In the first episode, we dug deep into JavaScript: its great
features, its weird features, and ultimately, enough good stuff to start getting our code organized.
But, uh, I ignored a gigantic thing in the JavaScript world. I mean, really big, like trying to stay cool when Godzilla is stomping
on cars all around you! JavaScript... has a new version... with a ton of new features and significant changes to the language!
And guess what? It's not even new anymore - it came out in 2015! That's all the more reason that it's time for us to understand
what it brings. Because if you eventually want to use a frontend framework like ReactJS, you need to know this stuff!
Project Setup
Before we dive in and meet Godzilla, you should totally code along with me. To do that, use the download button on this page
to get the course code. When you unzip it, there will be a start/ directory which will have the same code that I have here. Check
out the README.md file for witty banter and setup instructions.
The last step will be to open a terminal, go into the directory and run:
to start the built-in PHP web server. Find your browser and open http://localhost:8000 . You can login with ron_furgandy ,
password pumpup . Welcome back to LiftStuff . Our app for keeping track of everything we lift during the day to stay in top
shape. This is a Symfony application, but that's not really important: almost everything we've been doing lives inside a single
JavaScript file called RepLogApp.js .
JavaScript is no different. Wait, that's not right! JavaScript is totally different. When a new version of JavaScript comes out, well,
what does that even mean? Because there isn't just one JavaScript, there are many: Chrome has a JavaScript engine, Internet
Explorer maintains its own... crappy one, and of course, Node.js is another JavaScript engine. So when the JavaScript
community decides it wants to add a new function... well, it can't! All it can really do is recommend that the new function be
added... and then wait for all the browsers to add it!
Here are some functions and language changes that we think would make JavaScript more hipster. Now, quick,
everyone go and implement these!
And guess what? The language isn't even called JavaScript! It's called ECMAScript. And there is a group of smart people that
work on new versions of ECMAScript. But unlike PHP, that doesn't mean they're writing code: they're simply deciding what
should be included in the next version. Then, it's up to each browser and JavaScript engine to implement that. But as we will
learn later... some smart people in the JS world have found a way around needing to wait for browser support...
ECMAScript 2015?
Back to the story: in 2015 - after over 5 years of work - ECMAScript released a new version. What's it called? Um, yea, it has a
bunch of names: ES6, ECMAScript 6, ECMAScript 2015, Harmony, or, to its closest friends, Larry. Ok, maybe nobody calls it
Larry.
The official name is ECMAScript 2015, though you'll even hear me call it ES6 because it's the sixth version of ECMAScript.
As we'll learn, ES2015 comes with a lot of new functions and tools. But, more importantly, it comes with new language
constructs - new syntaxes that weren't allowed before. In fact, it comes with so many new syntaxes, that if you look at a
JavaScript file that uses everything, you might not even recognize it as JavaScript.
And that's not okay: because you and I, we need to be able to understand and write modern JavaScript.
Tip
There is already another new version of ECMAScript: ECMAScript 2016. But it only contains a few, minor features.
So here's our mission: jump into the important stuff of ES2015 that will let us understand, and write truly, modern JavaScript.
Let's start with my absolute favorite, game-changing feature of ES2015: arrow functions.
Chapter 2: Arrow Functions
You will see the first big feature or ES2015 used everywhere... and at first, it looks weird. Very simply, there is a new, shorter
syntax for creating anonymous functions.
In ES2015, we can remove the word function , and add an "equal arrow" ( => ) after the arguments:
That's it! That will do the exact same thing as before. Well, PhpStorm is really angry about this, but ignore it for a second. Let's
try it! This loadRepLogs() function is called on page-load to populate the table. Refresh!
It works: no errors.
Make PhpStorm Less Angry
But, apparently PhpStorm hates the arrow function! That's because it's setup to only recognize old, ES5 JavaScript.
Go into your settings and search for ES6. Under "Languages & Frameworks", "JavaScript", you can choose what version it
should use. Let's go with "ECMAScript 6". Hit ok... and once it's done indexing... ding! It's happy! And I'm happy too!
If you see a bubble about a "File Watcher to transpile using Babel", ignore that! But, we will talk about that "Babel" thing later,
it's more than just a cool-sounding word. Babel.
And sometimes, it can look a bit different. For example, the parentheses around the arguments? Totally optional! Without them,
everything still works:
I like the parentheses: I feel like it gives my arrow functions a bit more structure. But other code might not have them.
We know that inside of an anonymous function, this always changes to be something different. And that's why we added the
self variable: it allows us to refer to our RepLogApp object from inside the callback.
Ok, find your browser and refresh! Woh, check this out: this appears to be our RepLogApp object! Yea, this and self are the
same thing! What!?
It turns out, a classic anonymous function and the new arrow function do have one difference: when you use an arrow function,
the this variable is preserved. That's awesome news, and it's why I now use the arrow function everywhere in my code.
We can finally remove this silly var = self thing. And instead, below, use this . But because we're inside of another anonymous
function, replace it with the new arrow syntax to get things work:
Here, we're not using this , but I like to stay consistent and use the arrow function everywhere.
Keep going! Inside the next method, remove self , and add our arrow function:
Do the same for fadeOut() . But here, we were using this , which previously pointed to the DOM Element object that was fading
out. We can't use this anymore, but that's fine! Replace it with $row.remove() and then this.updateTotalWeight() :
190 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 26
27 $.extend(window.RepLogApp.prototype, {
... lines 28 - 65
66 _deleteRepLog: function($link) {
... lines 67 - 78
79 }).then(() => {
80 $row.fadeOut('normal', () => {
81 $row.remove();
82 this.updateTotalWeightLifted();
83 });
84 })
85 },
... lines 86 - 170
171 });
... lines 172 - 188
189 })(window, jQuery, Routing, swal);
Double-check that things work. Refresh! Delete one of the items and... perfect!
Since we're going to use arrow functions for all anonymous functions, search for function() . Yep, we're going to replace
everything, except for the methods in our objects. Remove function() , and add the arrow:
Repeat it again, and remove another self variable: just use this :
190 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 26
27 $.extend(window.RepLogApp.prototype, {
... lines 28 - 90
91 handleNewFormSubmit: function(e) {
... lines 92 - 100
101 .then((data) => {
102 this._clearForm();
103 this._addRow(data);
104 }).catch((errorData) => {
105 this._mapErrorsToForm(errorData.errors);
106 });
107 },
... lines 108 - 170
171 });
... lines 172 - 188
189 })(window, jQuery, Routing, swal);
But now that we're using the arrow function, obviously, that won't work. No worries! Just give your arrow function two
arguments: index and element . Use element instead of this :
If we search for .each() , there is one other spot with the same problem. Same solution: add index, element and use element
inside:
In this case, the arrow function is nothing more than a single return statement. In this situation, to be extra fancy, you can
remove the function body and return statement entirely:
188 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 26
27 $.extend(window.RepLogApp.prototype, {
... lines 28 - 47
48 handleRepLogDelete: function (e) {
... lines 49 - 52
53 swal({
... lines 54 - 57
58 preConfirm: () => this._deleteRepLog($link)
... lines 59 - 60
61 });
62 },
... lines 63 - 168
169 });
... lines 170 - 186
187 })(window, jQuery, Routing, swal);
When you don't have the curly braces, it means that this value will be returned. It looks weird at first, but it means the same
thing that we had before. You will see this kind of stuff in code examples.
Phew! After making all these changes, let's refresh and try them. The list loads, we can delete, and the form still validates.
Bananas!
I know this course is aimed at using JavaScript in a browser. Even still... we need to talk about Node.js! If you haven't used it
before, Node.js is basically JavaScript... that you execute on your server! In the same way that we can write a PHP file and run
it... we can do the same thing with Node.js: write a JavaScript file, execute it from the command line, and see the output!
So why are we talking about it? Well, it's going to give us a really easy way to test out some of these new ES2015 features. And
in the next tutorial on Webpack, we'll be working with it even more.
Once you're ready, you should be able to execute node -v from the command line. Ok, let's experiment! At the root of your
project, create a new file called play.js ... because of course, Node.js is JavaScript!
2 lines play.js
1 console.log('OMG! Node is JS on the server!');
$ node play.js
Boom! And now we can start experimenting with new ES2015 features... without needing to constantly refresh the browser.
Let's play a bit more with our arrow functions. Create a new variable called aGreatNumber set to 10:
8 lines play.js
1 var aGreatNumber = 10;
... lines 2 - 8
8 lines play.js
1 var aGreatNumber = 10;
2
3 setTimeout(() => {
4 console.log(aGreatNumber);
5 }, 1000);
... lines 6 - 8
Delay that call for 1 second, and, at the bottom, just log waiting :
8 lines play.js
1 var aGreatNumber = 10;
2
3 setTimeout(() => {
4 console.log(aGreatNumber);
5 }, 1000);
6
7 console.log('waiting...');
Head back to the terminal and run that! It prints, waits and prints! Sweet! Now let's go learn about let and const !
Chapter 4: var Versus let: Scope!
When you look at ES2015 code, one thing tends to jump out immediately: suddenly instead of seeing var everywhere... you
see something called let ! In our JS file, if you scroll down to the bottom, PhpStorm has highlighted my var with a warning:
Now, go refresh. This function is called a bunch of times as the table is loading. And... it looks like everything works fine. It
looks like var and let are equivalent?
In most cases, that's true! let is a new way to initialize a variable that's almost the same as var . 99% of the time, you can use
either one, and it won't make a difference.
12 lines play.js
1 var aGreatNumber = 10;
2
3 if (true) {
4 aGreatNumber = 42;
5 }
... lines 6 - 12
When we run it, it re-assigns the variable to 42. No surprises. But what if we added var aGreatNumber = 42 ?
12 lines play.js
1 var aGreatNumber = 10;
2
3 if (true) {
4 var aGreatNumber = 42;
5 }
... lines 6 - 12
I shouldn't need to say var again: the variable has already been initialized. But, will this give us an error? Or change anything?
Let's find out! No! We still see 42. When we use the second var , it re-declares a new variable called aGreatNumber . But that
doesn't make any real difference: down below, it prints the new variable's value: 42.
But now, wrap this same code in a self-executing function. Use the new arrow syntax to be trendy, then execute it immediately:
14 lines play.js
1 var aGreatNumber = 10;
2
3 if (true) {
4 (() => {
5 var aGreatNumber = 42;
6 })();
7 }
... lines 8 - 14
Remember, the scope of a variable created with var is whatever function it is inside of. Let's follow the code. First, we create
the variable and set it to 10. Then we create a new variable set to 42. But since this is inside of a function, its scope is only this
function. In other words, inside of the self-executing block, aGreatNumber is 42. But outside, the original variable still exists, and
it's still set to 10. Since we're printing it from outside the function, we see 10.
Okay okay, I know, this can be confusing. And most of the time... this subtle scope stuff doesn't make any difference. But, this is
exactly where var and let different.
12 lines play.js
1 let aGreatNumber = 10;
2
3 if (true) {
4 let aGreatNumber = 42;
5 }
... lines 6 - 12
If let and var behaved exactly the same, we would expect this - just like before - to print 42. Try it.
But no! It prints 10! And this is the difference between var and let . With var - just like with any variable in PHP - a variable's
scope is the function it's inside of, plus any embedded functions. But let is different: it's said to be "block-scoped". That means
that anytime you have a new open curly brace ( { ) - like an if statement or for loop - you've entered a new scope for let . In this
case, let is equal to 42, only inside of the if statement. Outside, it's a completely different variable, which is set to 10.
Of course, if we remove the extra let statement and try it, now we get 42:
12 lines play.js
1 let aGreatNumber = 10;
2
3 if (true) {
4 aGreatNumber = 42;
5 }
... lines 6 - 12
This is because without the let , we're no longer creating a new variable: we're simply changing the existing variable to 42.
If this makes your head spin, me too! In practice, there are very few situations where var and let behave different. So, use your
favorite. But there is one other tiny thing that makes me like let , and it deals with variable hoisting.
Chapter 5: var Versus let: Hoisting!
There's one other reason to use let instead of var . To understand it, we need to get really nerdy and talk about something with
a cool name: variable ahoy-sting. I mean, variable hoisting.
13 lines play.js
1 console.log(bar);
2 let aGreatNumber = 10;
... lines 3 - 13
I know! This doesn't even make sense - there is no variable called bar ! When we try it, we get:
13 lines play.js
1 console.log(aGreatNumber);
2 let aGreatNumber = 10;
... lines 3 - 13
13 lines play.js
1 console.log(aGreatNumber);
2 var aGreatNumber = 10;
... lines 3 - 13
And all of a sudden, it does not break. It simply says that that value is undefined .
In PHP, we never need to initialize a variable with a special keyword. We don't say var $aGreatNumber = 10 , we just say
$aGreatNumber = 10 and we're good to go. But in many other languages, including JavaScript, you must initialize a variable first
with a keyword.
When you use var to initialize a variable, when JavaScript executes, it basically finds all of your var variables, goes to the top
of that variable's scope - usually the top of whatever function it's inside of, but in this case, it's the top of the file - and effectively
does this: var aGreatNumber . That initializes the variable, but doesn't set it to any value. This is called variable hoisting: and it's
the reason that we get undefined instead of an error when we try to use a variable that's declared with var ... before it's
declared.
But when we change this to let , we already saw that this does throw a ReferenceError . And that's kinda great! I mean, isn't that
what we would expect to happen when we reference a variable that hasn't been created yet!
So with var , variables are hoisted to the top. But with let , that doesn't happen, and that's kinda cool. Well, technically, let also
does variable hoisting, but thanks to something called the "temporal dead zone" - also an awesome name - let acts normal: as
if its variables were not hoisted.
Since let seems to behave more predictably, let's go into RepLogApp and change all of these "vars" to let . Find all "var space"
and replace with "let space":
And just to make sure that our code doesn't have any edge cases where var and let behave differently, try out the page! Yay!
Everything looks like it's still working great.
The ECMAScript gods didn't stop with let - they added a third way to declare a new variable: var , let and cat . Wait, that's not
right - var , let and const .
Back in our play file, remove the log and initialize the variable with const :
12 lines play.js
1 const aGreatNumber = 10;
2
3 if (true) {
4 aGreatNumber = 42;
5 }
... lines 6 - 12
As you're probably already guessing - and as you can see from PhpStorm being very angry - when you initialize a variable with
const , it can't be reassigned.
But if we comment-out the line where we change the variable, it works just like we expect:
12 lines play.js
1 const aGreatNumber = 10;
2
3 if (true) {
4 //aGreatNumber = 42;
5 }
... lines 6 - 12
As far as scope goes, const and let work the same. So really, const and let are identical... except that you can't modify a
const variable.
16 lines play.js
1 const aGreatNumber = 10;
2 const aGreatObject = { withGreatKeys: true };
3
4 aGreatObject.withGreatKeys = false;
... lines 5 - 16
Try it! It does work! The withGreatKeys property did change! Here's the truth: when you use const , it's not that the value of that
variable can't change. The object can change. Instead, using const means that you cannot reassign the aGreatObject variable
to something else in memory. It must be assigned only once, to this object. But after that, the object is free to change.
In our case, because you know I love to write hipster code, let's change each let to const . Start at the top: const $link makes
sense. We don't need to reassign that. The same is true for deleteUrl , $row , $form , and formData :
Keep going: $form , fieldName , $wrapper , $error ... and eventually we get to the last one: totalWeight . But this variable can't be
set to a const : we set it to 0, but then reassign it in each loop. This is a perfect case for let :
188 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
... lines 4 - 176
177 $.extend(Helper.prototype, {
178 calculateTotalWeight: function() {
179 let totalWeight = 0;
... lines 180 - 184
185 }
186 });
187 })(window, jQuery, Routing, swal);
Let's take our app for a spin! Refresh! And try deleting something. Woohoo! Yep, you can pretty much choose between var , let
and cat , I mean, const .
Chapter 7: Object Literals & Optional Args
When it comes to functions and arrays, ES2015 has a couple of things you are going to love! Well, some of this stuff might look
weird at first... but then you will love them!
Above that line, create a new url variable set to the URL:
Obviously, this will work exactly like before: nothing interesting yet. Well, in ES2015, if your key and your value are the same,
you can just leave off the key:
Yep, this means the same thing as before. So if you suddenly see an associative array or object where one of its keys is
missing... well, it is the key... and the value.
Simple, but too much work, maybe? In ES2015, we can shorten this to loadRepLogs() :
So much cooler! Let's change it everywhere! Search for the word function , because almost everything is about to change:
194 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
... lines 4 - 26
27 $.extend(window.RepLogApp.prototype, {
... lines 28 - 31
32 loadRepLogs() {
... lines 33 - 39
40 },
41
42 updateTotalWeightLifted() {
... lines 43 - 45
46 },
47
48 handleRepLogDelete(e) {
... lines 49 - 61
62 },
63
64 _deleteRepLog($link) {
... lines 65 - 82
83 },
84
85 handleRowClick() {
... line 86
87 },
88
89 handleNewFormSubmit(e) {
... lines 90 - 104
105 },
106
107 _saveRepLog(data) {
... lines 108 - 127
128 },
129
130 _mapErrorsToForm(errorData) {
... lines 131 - 146
147 },
148
149 _removeFormErrors() {
... lines 150 - 152
153 },
154
155 _clearForm() {
... lines 156 - 159
160 },
161
162 _addRow(repLog) {
... lines 163 - 169
170 }
171 });
... lines 172 - 178
179 $.extend(Helper.prototype, {
180 calculateTotalWeight() {
... lines 181 - 186
187 },
... lines 188 - 191
192 });
193 })(window, jQuery, Routing, swal);
Ultimately, the only function keywords that will be left are for the self-executing function - which could be an arrow function - and
the two constructors:
194 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
4 window.RepLogApp = function ($wrapper) {
... lines 5 - 24
25 };
... lines 26 - 175
176 const Helper = function ($wrapper) {
177 this.$wrapper = $wrapper;
178 };
... lines 179 - 192
193 })(window, jQuery, Routing, swal);
Nice!
Optional Args
Ready for one more cool thing? This one is easy. Suppose we have a new method, not calculateTotalWeight() , but
getTotalWeightString() . Use the new shorthand syntax and return this.calculateTotalWeight() and append "pounds" to it:
Perfect! Then above, in updateTotalWeightLifted() , instead of calling calculateTotalWeight() and passing that to .html() , pass
getTotalWeightString() :
Ok, nothing too crazy so far: when we refresh, at the bottom, yep, "pounds".
But now suppose that we want to set a max weight on that. What I mean is, if we are over a certain weight - maybe 500 -
instead of printing the actual total, we want to print "500+"
Start by adding a new argument called maxWeight . Then say let weight = this.calculateTotalWeight() . And if weight > maxWeight ,
add weight = maxWeight + '+' . At the bottom, return weight and "pounds":
200 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
... lines 4 - 178
179 $.extend(Helper.prototype, {
... lines 180 - 188
189 getTotalWeightString(maxWeight) {
190 let weight = this.calculateTotalWeight();
191
192 if (weight > maxWeight) {
193 weight = maxWeight + '+';
194 }
195
196 return weight + ' lbs';
197 }
198 });
199 })(window, jQuery, Routing, swal);
But what if I wanted to make this argument optional with a default value of 500? You could do this before in JavaScript, but it
was ugly. Now, thanks to our new best friend ES2015, we can say maxWeight = 500 - the same way we do in PHP:
So, yay! Finally, JavaScript has optional function arguments! And a second yay, because we are ready to learn perhaps the
biggest change in ES2015: JavaScript classes.
Chapter 8: Legit JavaScript Classes
In the first JavaScript tutorial, we learned about objects. I mean, real objects: the kind you can instantiate by creating a
constructor function, and then adding all the methods via the prototype. Objects look a lot different in PHP than in in JavaScript,
in large part because PHP has classes and JavaScript doesn't. Well... that's a big fat lie! ES2015 introduces classes: true
classes.
That's it! With this syntax, the constructor is called, just, constructor . Move the old constructor function into the class and rename
it: constructor . You can also remove the semicolon after the method, just like in PHP:
Moving everything else into the new class syntax is easy: remove $.extend(helper.prototype) and move all of the methods inside
of the class:
201 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
... lines 4 - 172
173 /**
174 * A "private" object
175 */
176 class Helper {
177 constructor($wrapper) {
178 this.$wrapper = $wrapper;
179 }
180
181 calculateTotalWeight() {
182 let totalWeight = 0;
183 this.$wrapper.find('tbody tr').each((index, element) => {
184 totalWeight += $(element).data('weight');
185 });
186
187 return totalWeight;
188 }
189
190 getTotalWeightString(maxWeight = 500) {
191 let weight = this.calculateTotalWeight();
192
193 if (weight > maxWeight) {
194 weight = maxWeight + '+';
195 }
196
197 return weight + ' lbs';
198 }
199 }
200 })(window, jQuery, Routing, swal);
And congratulations! We just created a new ES2015 class. Wasn't that nice?
To make things sweeter, it all works just like before: nothing is broken. And that's no accident: behind the scenes, JavaScript
still follows the prototypical object oriented model. This new syntax is just a nice wrapper around it. It's great: we don't need to
worry about the prototype , but ultimately, that is set behind the scenes.
Let's make the same change at the top with RepLogApp : class RepLogApp { and then move the old constructor function inside.
But, make sure to spell that correctly! I'll indent everything and add the closing curly brace:
203 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
4 class RepLogApp {
5 constructor($wrapper) {
6 this.$wrapper = $wrapper;
7 this.helper = new Helper(this.$wrapper);
8
9 this.loadRepLogs();
10
11 this.$wrapper.on(
12 'click',
13 '.js-delete-rep-log',
14 this.handleRepLogDelete.bind(this)
15 );
16 this.$wrapper.on(
17 'click',
18 'tbody tr',
19 this.handleRowClick.bind(this)
20 );
21 this.$wrapper.on(
22 'submit',
23 this._selectors.newRepForm,
24 this.handleNewFormSubmit.bind(this)
25 );
26 }
27 }
... lines 28 - 201
202 })(window, jQuery, Routing, swal);
Rude! PhpStorm is trying to tell us that properties are not supported inside classes: only methods are allowed. That may seem
weird - but it'll be more clear why in a minute. For now, change this to be a method: _getSelectors() . Add a return statement, and
everything is happy:
Well, everything except for the couple of places where we reference the _selectors property. Yea, this._selectors , that's not
going to work:
207 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
4 class RepLogApp {
5 constructor($wrapper) {
... lines 6 - 20
21 this.$wrapper.on(
... line 22
23 this._selectors.newRepForm,
... line 24
25 );
26 }
... lines 27 - 135
136 _mapErrorsToForm(errorData) {
... line 137
138 const $form = this.$wrapper.find(this._selectors.newRepForm);
... lines 139 - 152
153 },
154
155 _removeFormErrors() {
156 const $form = this.$wrapper.find(this._selectors.newRepForm);
... lines 157 - 158
159 },
160
161 _clearForm() {
... lines 162 - 163
164 const $form = this.$wrapper.find(this._selectors.newRepForm);
... line 165
166 },
... lines 167 - 176
177 });
... lines 178 - 205
206 })(window, jQuery, Routing, swal);
Right now, move the rest of the methods inside: just delete the } and the prototype line to do it. We can also remove the
comma after each method:
203 lines web/assets/js/RepLogApp.js
1 'use strict';
2
3 (function(window, $, Routing, swal) {
4 class RepLogApp {
5 constructor($wrapper) {
... lines 6 - 25
26 }
27
28 _getSelectors() {
29 return {
30 newRepForm: '.js-new-rep-log-form'
31 }
32 }
33
34 loadRepLogs() {
... lines 35 - 41
42 }
43
44 updateTotalWeightLifted() {
... lines 45 - 47
48 }
49
50 handleRepLogDelete(e) {
... lines 51 - 63
64 }
65
66 _deleteRepLog($link) {
... lines 67 - 84
85 }
86
87 handleRowClick() {
... line 88
89 }
90
91 handleNewFormSubmit(e) {
... lines 92 - 106
107 }
108
109 _saveRepLog(data) {
... lines 110 - 129
130 }
131
132 _mapErrorsToForm(errorData) {
... lines 133 - 148
149 }
150
151 _removeFormErrors() {
... lines 152 - 154
155 }
156
157 _clearForm() {
... lines 158 - 161
162 }
163
164 _addRow(repLog) {
... lines 165 - 171
172 }
173 }
... lines 174 - 201
202 })(window, jQuery, Routing, swal);
Other than that, nothing needs to change.
Rename the method back to _selectors() , and then add a "get space" in front of it:
Woh! Instantly, PhpStorm is happy: this is a valid syntax. And when you search for _selectors , PhpStorm is happy about those
calls too!
This is the new "get" syntax: a special new feature from ES2015 that allows you to define a method that should be called
whenever someone tries to access a property, like _selectors . There's of course also a "set" version of this, which would be
called when someone tries to set the _selectors property.
So even though classes don't technically support properties, you can effectively create properties by using these get and set
methods.
Oh, and btw, just to be clear: even though you can't define a property on a class, you can still set whatever properties you want
on the object, after it's instantiated:
class CookieJar {
constructor(cookies) {
this.cookies = cookies;
}
}
Ok team! Try out our app! Refresh! It works! Wait, no, an error! Blast! It says:
Ah, this code is fine: the problem is that the RepLogApp class only lives within this self executing function:
It's the same problem we had in the first episode with scope.
Solve it in the same way: export the class to the global scope by saying window.RepLogApp = RepLogApp :
Try it now! And life is good! So what else can we do with classes? What about static methods?
Chapter 9: Static Class Methods
Interesting: PhpStorm is highlighting it like something is wrong! If you hover over it, it says:
In the first episode, we talked about how when you add your methods to the prototype , it's like creating non-static methods on
PHP classes:
Greeter.prototype.sayHi = function () {
console.log(this.greeting);
}
In other words, when you create new instances of your object, each method has access to its own instance properties:
I also said that if you decided not to put a method on the prototype , that is legal, but it effectively becomes static:
Greeter.sayHi = function () {
console.log('YO!');
}
Greeter.sayHi(); // YO!
If that didn't make a lot of sense then, it's okay. Because with the new class syntax, it's much easier to think about!
PhpStorm is suggesting that this method could be static for one simple reason: the method doesn't use the this variable. That's
the same as in PHP: if a method doesn't use the this variable, it could be made static if we wanted.
It's probably fine either way, but let's make this static! Add static before get _selectors() :
And as soon as we do that, we can't say this._selectors anymore. Instead, we need to say RepLogApp._selectors :
And that makes sense: in PHP, we do the same thing: we use the class name to reference items statically. Let's change that in
a few other places:
208 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
4 class RepLogApp {
5 constructor($wrapper) {
... lines 6 - 20
21 this.$wrapper.on(
22 'submit',
23 RepLogApp._selectors.newRepForm,
24 this.handleNewFormSubmit.bind(this)
25 );
26 }
... lines 27 - 134
135 _mapErrorsToForm(errorData) {
... line 136
137 const $form = this.$wrapper.find(RepLogApp._selectors.newRepForm);
... lines 138 - 151
152 }
153
154 _removeFormErrors() {
155 const $form = this.$wrapper.find(RepLogApp._selectors.newRepForm);
... lines 156 - 157
158 }
159
160 _clearForm() {
... lines 161 - 162
163 const $form = this.$wrapper.find(RepLogApp._selectors.newRepForm);
... line 164
165 }
... lines 166 - 175
176 }
... lines 177 - 206
207 })(window, jQuery, Routing, swal);
Perfect!
Let's see one more example: scroll all the way down to the Helper class. Create a new method: static _calculateWeight() with an
$elements argument:
This will be a new static utility method whose job is to loop over whatever elements I pass, look for their weight data attribute,
and then return the total weight:
214 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 177
178 /**
179 * A "private" object
180 */
181 class Helper {
... lines 182 - 201
202 static _calculateWeights($elements) {
203 let totalWeight = 0;
204 $elements.each((index, element) => {
205 totalWeight += $(element).data('weight');
206 });
207
208 return totalWeight;
209 }
210 }
... lines 211 - 212
213 })(window, jQuery, Routing, swal);
Now, in calculateTotalWeight() , just say: return Helper - because we need to reference the static method by its class name
Helper._calculateTotalWeight() and pass it the elements: this.$wrapper.find('tbody tr') :
Coolio! Try that out! And we still see the correct total.
And that is 10 times easier to understand as a PHP developer! Sure, JavaScript still has prototypical inheritance behind the
scenes... but most of the time, we won't know or care.
Chapter 10: Class Inheritance and super Calls
In PHP, a class can extend another class. So, can we do that in JavaScript? Totally! And once again, you're going to love it.
To try this out, let's go back to the play.js file. Create a new class: AGreatClass , because, it's going to be a great class. Give it a
constructor with a greatNumber arg and set that on a property:
16 lines play.js
1
2 class AGreatClass {
3 constructor(greatNumber) {
4 this.greatNumber = greatNumber;
5 }
... lines 6 - 9
10 }
... lines 11 - 16
Below, add one new method: returnGreatThings , which it will, because it's going to return our greatNumber !
16 lines play.js
1
2 class AGreatClass {
... lines 3 - 6
7 returnGreatThings() {
8 return this.greatNumber;
9 }
10 }
... lines 11 - 16
Finally, create a new constant, aGreatObject , set to new AGreatClass(42) . Let's console.log(aGreatObject.returnGreatThings()) :
16 lines play.js
1
2 class AGreatClass {
... lines 3 - 9
10 }
11
12 const aGreatObject = new AGreatClass(42);
13 console.log(
14 aGreatObject.returnGreatThings()
15 );
There's no mystery about what this is going to return. Run the file! Yep, 42!
Extending a Class
Now, let's create another class, class AnotherGreatClass , because we're on a roll. But, I want this to be a sub class of
AGreatClass . How? The same way we do it in PHP: extends AGreatClass :
20 lines play.js
1
2 class AGreatClass {
... lines 3 - 9
10 }
11
12 class AnotherGreatClass extends AGreatClass{
13
14 }
... lines 15 - 20
That is it. We're not overriding anything yet, but this should work. Change the variable to be a new AnotherGreatClass , and then
run the file!
20 lines play.js
1
2 class AGreatClass {
... lines 3 - 9
10 }
11
12 class AnotherGreatClass extends AGreatClass{
13
14 }
15
16 const aGreatObject = new AnotherGreatClass(42);
... lines 17 - 20
Success!
22 lines play.js
1
2 class AGreatClass {
... lines 3 - 6
7 returnGreatThings() {
8 return this.greatNumber;
9 }
10 }
11
12 class AnotherGreatClass extends AGreatClass{
13 returnGreatThings() {
14 return 'adventure';
15 }
16 }
17
18 const aGreatObject = new AnotherGreatClass(42);
... lines 19 - 22
Okay, but what if we want to call the parent method. In PHP, we would say parent::returnGreatThings() . Well, in JavaScript, the
magic word is super : let greatNumber = super.returnGreatThings() . Let's return an array of great things, like greatNumber and
adventure :
24 lines play.js
1
2 class AGreatClass {
... lines 3 - 6
7 returnGreatThings() {
8 return this.greatNumber;
9 }
10 }
11
12 class AnotherGreatClass extends AGreatClass{
13 returnGreatThings() {
14 let greatNumber = super.returnGreatThings();
15
16 return [greatNumber, 'adventure'];
17 }
18 }
19
20 const aGreatObject = new AnotherGreatClass(42);
... lines 21 - 24
Override the constructor and give it just one argument: greatWord . Then, set this.greatWord = greatWord :
28 lines play.js
... line 1
2 class AGreatClass {
3 constructor(greatNumber) {
4 this.greatNumber = greatNumber;
5 }
... lines 6 - 9
10 }
11
12 class AnotherGreatClass extends AGreatClass{
13 constructor(greatWord) {
14 this.greatWord = greatWord;
15 }
... lines 16 - 21
22 }
... lines 23 - 28
Before we try to print that below, what do you think will happen when we run this? We're setting the greatWord property... but
we're not calling the parent constructor. Actually, try to run it!
Interesting! Inside construct() , there is no this variable? And PhpStorm is trying to tell us why:
Unlike PHP - where calling the parent constructor is usually a good idea, but ultimately optional - in JavaScript, it's required.
You must call the parent's constructor.
So let's give this two arguments: greatNumber and greatWord :
30 lines play.js
... lines 1 - 11
12 class AnotherGreatClass extends AGreatClass{
13 constructor(greatNumber, greatWord) {
... lines 14 - 15
16 this.greatWord = greatWord;
17 }
... lines 18 - 23
24 }
... lines 25 - 30
To call the parent constructor... it's not what you might think: super.constructor() . You actually treat super like a function:
super(greatNumber) :
30 lines play.js
... lines 1 - 11
12 class AnotherGreatClass extends AGreatClass{
13 constructor(greatNumber, greatWord) {
14 super(greatNumber);
15
16 this.greatWord = greatWord;
17 }
... lines 18 - 23
24 }
... lines 25 - 30
30 lines play.js
... lines 1 - 11
12 class AnotherGreatClass extends AGreatClass{
... lines 13 - 18
19 returnGreatThings() {
20 let greatNumber = super.returnGreatThings();
21
22 return [greatNumber, this.greatWord];
23 }
24 }
25
26 const aGreatObject = new AnotherGreatClass(42, 'adventure');
... lines 27 - 30
Yes! It works! Oh man, are you feeling like a JavaScript class pro or what? Now let's talk about something kinda weird:
destructuring!
Chapter 11: Destructuring
Next, we're going to talk about two kinda weird things. At first, neither or these will seem all that useful. But even if that were
true, you're going to start to see them in use, even in PHP! So we need to understand them!
Destructuring
The first has a cool name: destructuring! In RepLogApp , find _addRow() . To start, just dump the repLog variable:
Now, refresh! This is called a bunch of times, and each repLog has the same keys: id , itemLabel , links , reps and
totalWeightLifted . Destructuring allows us to do this weird thing: let {id, itemLabel, reps} = repLog . Below, log id , itemLabel and
reps :
Yep, this weird line is actually going to create three new variables - id , itemLabel , and reps - set to the values of the id ,
itemLabel and reps keys in repLog .
Let's see it in action: refresh! Got it! This is called destructuring, and you can even do it with arrays, which looks even stranger.
In that case, the variables are assigned by position, instead of by name. Oh, and side-note, PHP7 introduced destructuring - so
this exists in PHP!
It's not an error: it just prints as undefined. So destructuring is friendly: if something goes wrong, it doesn't kill your code: it just
assigns undefined. If you think this might be possible, you can give that variable a default, like whatever :
So, this is destructuring. It may or may not be useful to you, but you will see it! Don't let it surprise you!
Chapter 12: The... Spread Operator
The second thing weird thing we need to talk about is much more important. It's called the spread operator. Open up play.js
and clear everything out. Create a new function called printThreeThings(thing1, thing2, thing3) . Inside, console.log() our three
things:
8 lines play.js
1 let printThreeThings = function(thing1, thing2, thing3) {
2 console.log(thing1, thing2, thing3);
3 };
... lines 4 - 8
Easy enough!
Below that, create a new array called yummyThings , which of course naturally will include pizza, gelato, and sushi:
8 lines play.js
1 let printThreeThings = function(thing1, thing2, thing3) {
2 console.log(thing1, thing2, thing3);
3 };
4
5 let yummyThings = ['pizza', 'gelato', 'sushi'];
... lines 6 - 8
All delicious, but maybe not if you eat them all at the same time.
How can I pass the three yummy things as the first, second, and third argument to printThreeThings() ?
Well, we could say printThreeThings() with yummyThings[0] , yummyThings[1] and yummyThings[2] . But! Not cool enough! In
ES2015, you can use the spread operator: printThreeThings(...yummyThings) :
8 lines play.js
1 let printThreeThings = function(thing1, thing2, thing3) {
2 console.log(thing1, thing2, thing3);
3 };
4
5 let yummyThings = ['pizza', 'gelato', 'sushi'];
6
7 printThreeThings(...yummyThings);
Woh.
If we try that, it prints our three things! That's crazy! The ... is called the "spread operator": it's always ... and then your array.
When you do this, it's almost as if somebody went through and literally just wrote out all of the items in your array manually,
instead of us doing it by hand.
But of course, there are a lot of yummy things in the world, and since I'm from the US, my yummyThings should probably have a
cheeseburger:
8 lines play.js
1 let printThreeThings = function(thing1, thing2, thing3) {
2 console.log(thing1, thing2, thing3);
3 };
4
5 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
6
7 printThreeThings(...yummyThings);
What will happen now? The array has 4 things, but we only have 3 arguments? Let's find out! Run the script!
It's the same result! Thanks to the spread operator, pizza is passed as the first argument, then gelato , sushi and finally,
cheeseburger is passed as the 4th argument. Since the function doesn't have a 4th argument, it's just ignored!
Dang, good question. Actually, there are two really great use-cases. One is in ReactJS. I won't talk about it now... just trust me!
The second deals with merging arrays together.
Suppose we need to create another array called greatThings . And we decide that swimming is super great, and sunsets are the
best. They are the best. We also decide that anything "yummy" must also be great, so I want to add all four of these
yummyThings into the greatThings array. In PHP, we might use array_merge() . In JavaScript, one option is the spread operator.
Add a comma - as if we were going to add another entry - and then say ...yummyThings . We could even keep going and add
something else great, like New Orleans . Because New Orleans is a really great place:
6 lines play.js
1 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
2
3 let greatThings = ['swimming', 'sunsets', ...yummyThings, 'New Orleans'];
... lines 4 - 6
Tip
There are often many ways to do the same thing in JavaScript, especially with arrays and objects. In this case,
greatThings.concat(yummyThings) is also an option.
6 lines play.js
1 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
2
3 let greatThings = ['swimming', 'sunsets', ...yummyThings, 'New Orleans'];
4
5 console.log(greatThings);
It does: swimming , sunset , 4 yummy things, and New Orleans at the bottom.
10 lines play.js
1 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
2
3 let greatThings = ['swimming', 'sunsets', ...yummyThings, 'New Orleans'];
4
5 let copyOfGreatThings = greatThings;
... lines 6 - 10
Now, add something else to this new variable: use copyOfGreatThings.push() to add something that we all know is great:
summer :
10 lines play.js
1 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
2
3 let greatThings = ['swimming', 'sunsets', ...yummyThings, 'New Orleans'];
4
5 let copyOfGreatThings = greatThings;
6 copyOfGreatThings.push('summer');
... lines 7 - 10
10 lines play.js
1 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
2
3 let greatThings = ['swimming', 'sunsets', ...yummyThings, 'New Orleans'];
4
5 let copyOfGreatThings = greatThings;
6 copyOfGreatThings.push('summer');
7
8 console.log(greatThings);
9 console.log(copyOfGreatThings);
Here's the question: we know summer now lives in copyOfGreatThings() . But does it also now live inside of greatThings ? Try it!
It does! Summer lives in both arrays! And this makes sense: arrays are objects in JavaScript, and just like in PHP, objects are
passed by reference. In reality, greatThings and copyOfGreatThings are identical: they both point to the same array in memory.
As we get more advanced, especially in React.js, the idea of not mutating objects will become increasingly important. What I
mean is, there will be times when it wil be really important to copy an object, instead of modifying the original.
So how could we make copyOfGreatThings a true copy? The spread operator has an answer: re-assign copyOfGreatThings to
[...greatThings] :
10 lines play.js
1 let yummyThings = ['pizza', 'gelato', 'sushi', 'cheeseburger'];
2
3 let greatThings = ['swimming', 'sunsets', ...yummyThings, 'New Orleans'];
4
5 let copyOfGreatThings = [...greatThings];
... lines 6 - 10
And that is it! This will create a new array, and then put each item from greatThings into it, one-by-one.
Try it! Yes! We can see summer in the copy, but we did not modify the original array.
You know what's always kind of a bummer... in any language? Creating strings that have variables inside. I know, that doesn't
sound like that big of a deal, but it kind of is... especially in JavaScript!
For example, let's say that our favorite food is, of course, gelato:
5 lines play.js
1 const favoriteFood = 'gelato';
... lines 2 - 5
And below that, we want to create a string that talks about this: The year is, end quote, plus, and then new Date().getFullYear() ,
another plus, open quote, space, my favorite food is, another quote, one more plus, and favoriteFood :
5 lines play.js
1 const favoriteFood = 'gelato';
2 const iLoveFood = 'The year is '+(new Date()).getFullYear()+' and my favorite food is '+favoriteFood;
... lines 3 - 5
5 lines play.js
1 const favoriteFood = 'gelato';
2 const iLoveFood = 'The year is '+(new Date()).getFullYear()+' and my favorite food is '+favoriteFood;
3
4 console.log(iLoveFood);
5 lines play.js
1 const favoriteFood = 'gelato';
2 const iLoveFood = `The year is ${(new Date()).getFullYear()} and my favorite food is ${favoriteFood}`;
3
4 console.log(iLoveFood);
Usually, you'll use this to print a variable, but you can use any expression you want: like we just did with the date.
Then, in _addRow() , we found that script element by its id and fetched the string:
So, why did we put the the template in Twig? Well, it's not a bad option, but mostly, we did it because putting that template into
JavaScript was, basically, impossible.
Why? Try it! Copy the template, find the bottom of RepLogApp.js , create a new const rowTemplate and paste the string inside
single quotes:
234 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 215
216 const rowTemplate = '
217 <tr data-weight="<%= totalWeightLifted %>">
218 <td><%= itemLabel %></td>
219 <td><%= reps %></td>
220 <td><%= totalWeightLifted %></td>
221 <td>
222 <a href="#"
223 class="js-delete-rep-log"
224 data-url="<%= links._self %>"
225 >
226 <span class="fa fa-trash"></span>
227 </a>
228 </td>
229 </tr>
230 ';
... lines 231 - 232
233 })(window, jQuery, Routing, swal);
Oh wow, PHP storm is SO angry with me. Furious! And that's because, unlike PHP, you are not allowed to have line breaks
inside traditional strings. And that makes putting a template inside of JavaScript basically impossible.
Guess what? Template strings fix that! Just change those quotes to ticks, and everything is happy:
Now, we can simplify things! Up in _addRow() , set tplText to simply equal rowTemplate :
First, update the template with ${repLog.totalWeightLifted} . In a moment, instead of making the individual variables available,
we'll just set one variable: the repLog object:
235 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 216
217 const rowTemplate = `
218 <tr data-weight="${repLog.totalWeightLifted}">
... lines 219 - 229
230 </tr>
231 `;
... lines 232 - 233
234 })(window, jQuery, Routing, swal);
Of course, PhpStorm is angry because there is no repLog variable... but we can fix that!
Next, make the same change very carefully to the other variables: replacing the kind of ugly syntax with the template string
syntax. And yea, be more careful than I am: I keep adding extra characters!
At this point, this is a valid template string... with one screaming problem: there is no repLog variable! If we refresh now, we of
course see:
Hey! Here's an idea: let's wrap this in a function! Instead of rowTemplate simply being a string, set it to an arrow function with a
repLog argument:
232 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 213
214 const rowTemplate = (repLog) => `
215 <tr data-weight="${repLog.totalWeightLifted}">
216 <td>${repLog.itemLabel}</td>
217 <td>${repLog.reps}</td>
218 <td>${repLog.totalWeightLifted}</td>
219 <td>
220 <a href="#"
221 class="js-delete-rep-log"
222 data-url="${repLog.links._self}"
223 >
224 <span class="fa fa-trash"></span>
225 </a>
226 </td>
227 </tr>
228 `;
... lines 229 - 230
231 })(window, jQuery, Routing, swal);
And suddenly, the template string will have access to a repLog variable.
Back in _addRow() , remove all this stuff and very simply say html = rowTemplate(repLog) :
And that is it! Try that out! Refresh! The fact that it even loaded proves it works: all of these rows are built from that template.
If you downloaded the start code for this project, you should have a tutorial directory with an upper.js file inside. Copy that
function. And then, at the bottom of RepLogApp.js , paste it before the template:
238 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 213
214 function upper(template, ...expressions) {
215 return template.reduce((accumulator, part, i) => {
216 return accumulator + (expressions[i - 1].toUpperCase ? expressions[i - 1].toUpperCase() : expressions[i - 1]) + part
217 })
218 }
... lines 219 - 236
237 })(window, jQuery, Routing, swal);
Just follow me on this: right before the tick that starts the template string, add the function's name: upper :
So literally upper , then without any spaces, the opening tick. This is called a tagged template. And by doing this, the string and
its embedded expressions will be passed into that function, allowing it to do some transformations. In this case, it'll upper case
all the words.
Other possible uses for tagged templates might be to escape variables. But honestly, the web seems to be filled mostly with
possible use-cases, without too many real-world examples. And also, these functions are really complex to write and even
harder to read!
Oh, btw, the upper function uses the spread operator in a different way: to allow the function to have a variable number of
arguments!
Anyways, let's see if it works! Refresh! Hey, all uppercase! Ok, kinda cool! You may not use tagged template strings, but you
might see them. And now, you'll understand what the heck is going on!
One of the crazy things about JavaScript... is that there's not one good way to loop over a collection! In PHP, we have foreach ,
and it works perfectly. But in JavaScript, you need to create an ugly custom for loop. Well actually, there is a .forEach()
function, but it only works on arrays, not other loopable things, like the Set object we'll talk about later. And, with .forEach() ,
there is no break if you want to exit the loop early.
That's why we've been using jQuery's $.each() . But guess what? ES2015 fixes this, finally. Introducing, the for of loop!
And it's pretty easy to follow: repLog is the new variable inside the loop, and data.items is the thing we want to loop over. We're
no longer passing this an anonymous function, so we can get rid of everything else. That's it. Say hello to your new best friend:
the for of loop.
Let's look for the other $.each() spots and update those too! Instead, say for let fieldData of $form.serializeArray() :
Before, the anonymous function received a key and then the fieldData . But, we didn't actually need the key: the $.each()
function just forced us to add it. Now, things are cleaner!
Tip
Since we are in a for loop now, we need to also update the return statement to be continue .
Make this same change in two more places: for $element of $form.find(':input') . Ah, don't forget your let or var :
Oh, and PhpStorm is warning me because I forgot to remove one of my closing parentheses! And, we don't need that
semicolon! Yay!
So, use the for of loop for everything! Well actually, that's not 100% true. for of is perfect when you want to loop over a
collection of items. But, if you want to loop over an associative array... or object, and you need to know the key for each item,
then you'll use for in .
Tip
Actually, you can use for of with an object, with a clever combination of Object.entries() and array destructuring!
let pets = {
beagle: 'Bark Twain',
poodle: 'Snuffles'
};
BUT, the Object.entries() method is still experimental, and may be included in ES2017.
This is the one limitation of for of : it gives you the value of the item you're looping over, but not its key, or index. In fact, if try to
use for of with an object, you'll get an error.
Chapter 15: Map and WeakMap
So far, all the new ES2015 stuff has been new language constructs: new syntaxes and keywords, like let , const and classes!
And that was no accident: these are the most important things to understand.
But ES2015 comes packed with other new features, like new functions and new objects. And mostly, those are easy enough to
understand: when you see an object or function you don't recognize, look it up, see how it works... and keep going!
Right now, when you need an associative array, you just create an object: foods = {} and start adding delicious things to it:
foods.italian = 'gelato' , foods.mexican = 'torta' and foods.canadian = 'poutine' . Poutine is super delicious:
7 lines play.js
1 let foods = {};
2 foods.italian = 'gelato';
3 foods.mexican = 'tortas';
4 foods.canadian = 'poutine';
... lines 5 - 7
7 lines play.js
1 let foods = {};
2 foods.italian = 'gelato';
3 foods.mexican = 'tortas';
4 foods.canadian = 'poutine';
5
6 console.log(foods.italian);
And no surprise, our console tells us we should eat gelato. Good idea!
In ES2015, we now have a new tool: instead of creating a simple object, we can create a new Map object. The syntax is
slightly different: instead of foods.italian = 'gelato' , use foods.set('italian', 'gelato') :
7 lines play.js
1 let foods = new Map();
2 foods.set('italian', 'gelato');
... lines 3 - 7
Repeat this for the other two keys. And at the bottom, fetch the value with foods.get('italian') :
7 lines play.js
1 let foods = new Map();
2 foods.set('italian', 'gelato');
3 foods.set('mexican', 'tortas');
4 foods.set('canadian', 'poutine');
5
6 console.log(foods.get('italian'));
Great! So... we have a new Map object... and it's a different way to create an associative array. But why would we use it?
Because it comes with some nice helper methods! For example, we can say foods.has('french') :
10 lines play.js
1 let foods = new Map();
2 foods.set('italian', 'gelato');
3 foods.set('mexican', 'tortas');
4 foods.set('canadian', 'poutine');
5
6 console.log(
7 foods.get('italian'),
8 foods.has('french')
9 );
It wasn't too difficult to check if a key existed before, but this feels clean.
Try this: create a new variable: let southernUSStates set to an array of Tennessee , Kentucky , and Texas :
13 lines play.js
... lines 1 - 5
6 let southernUsStates = ['Tennessee', 'Kentucky', 'Texas'];
... lines 7 - 13
13 lines play.js
... lines 1 - 5
6 let southernUsStates = ['Tennessee', 'Kentucky', 'Texas'];
7 foods.set(southernUsStates, 'hot chicken');
... lines 8 - 13
Important side note: hot chicken is really only something you should eat in Tennessee, but for this example, I needed to include
a few other states. In Texas, you should eat Brisket.
13 lines play.js
... lines 1 - 5
6 let southernUsStates = ['Tennessee', 'Kentucky', 'Texas'];
7 foods.set(southernUsStates, 'hot chicken');
8
9 console.log(
10 foods.get('italian'),
11 foods.get(southernUsStates)
12 );
If you're wondering when this would be useful... stay tuned. Oh, and there's one other property you should definitely know
about: foods.size :
14 lines play.js
... lines 1 - 5
6 let southernUsStates = ['Tennessee', 'Kentucky', 'Texas'];
7 foods.set(southernUsStates, 'hot chicken');
8
9 console.log(
10 foods.get('italian'),
11 foods.get(southernUsStates),
12 foods.size
13 );
Tip
You can also loop over a Map using our new friend - the for of loop. You can loop over the values or the keys!
Behind the scenes, the last example uses destructuring to assign each returned by entries() to the countryKey and food
variables. It's all coming together!
14 lines play.js
1 let foods = new WeakMap();
... lines 2 - 14
And this is where things get a little nuts. Why do we have a Map and a WeakMap ?
Let's find out! First try to run our code with WeakMap .
Woh, it explodes!
Map and WeakMap are basically the same... except WeakMap has an extra requirement: its keys must be objects. So yes, for
now, it seems like WeakMap is just a worse version of Map .
Turn each key into an array, which is an object. At the bottom, use foods.get() and pass it the italian array:
14 lines play.js
1 let foods = new WeakMap();
2 foods.set(['italian'], 'gelato');
3 foods.set(['mexican'], 'tortas');
4 foods.set(['canadian'], 'poutine');
... lines 5 - 8
9 console.log(
10 foods.get(['italian']),
... lines 11 - 12
13 );
Now when I run it, it works fine. Wait, or, does it?
Two interesting things: this prints undefined , hot chicken , undefined . First, even though the ['italian'] array in get() is equal to
the ['italian'] array used in set, they are not the same object in memory. These are two distinct objects, so it looks like a different
key to WeakMap . That's why it prints undefined .
Second, with WeakMap , you can't call foods.size . That's just not something that works with WeakMap .
15 lines play.js
1 let foods = new WeakMap();
2 foods.set(['italian'], 'gelato');
3 foods.set(['mexican'], 'tortas');
4 foods.set(['canadian'], 'poutine');
5
6 let southernUsStates = ['Tennessee', 'Kentucky', 'Texas'];
7 foods.set(southernUsStates, 'hot chicken');
8 southernUsStates = null;
... lines 9 - 15
When you try it now, this of course prints "undefined". That makes sense: we're now passing null to the get() function.
But what you can't see is that the southernUSStates object no longer exists... anywhere in memory!
Why? In JavaScript, if you have a variable that isn't referenced by anything else anymore, like southernUSStates , it's eligible to
be removed by JavaScript's garbage collection. The same thing happens in PHP.
But normally, because we set southernUSStates as a key on WeakMap , this reference to southernUSStates would prevent that
garbage collection. That's true with Map , but not WeakMap : it does not prevent garbage collection. In other words, even though
southernUSStates is still on our WeakMap , since it's not being referenced anywhere else, it gets removed from memory thanks
to garbage collection.
But, really, how often do you need to worry about garbage collection when building a web app? Probably not very often. So, at
this point, you should just use Map everywhere: it's easier and has more features.
And that's true! Except for one special, fascinating, nerdy WeakMap use-case. Let's learn about it!
Chapter 16: Private Variables & WeakMap
To see a real-world WeakMap use-case, go back into RepLogApp and scroll to the top. Remember, this file holds two classes:
RepLogApp and, at the bottom, Helper :
The purpose of Helper is to be a private object that we can only reference from inside of this self-executing function.
We set Helper onto a helper property. We do this so that we can use it later, inside of updateTotalWeightLifted() . Here's the
problem: the helper property is not private. I mean, inside of our template, if we wanted, we could say:
repLogApp.helper.calculateTotalWeight() .
Dang! We went to all of that trouble to create a private Helper object... and it's not actually private! Lame!
How can we fix this? Here's an idea: above the class, create a new HelperInstance variable set to null :
235 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
4
5 let HelperInstance = null;
6
7 class RepLogApp {
... lines 8 - 180
181 }
... lines 182 - 233
234 })(window, jQuery, Routing, swal);
Then, instead of setting the new Helper onto a property - which is accessible from outside, say: HelperInstance = new Helper() :
And that's it! The HelperInstance variable is not available outside our self-executing function. And of course down below, in
updateTotalWeightLifted() , the code will now read: HelperInstance.getTotalWeightString() :
Ok, so why not use our cool new Map object to store a collection of Helper objects? let HelperInstances = new Map() :
235 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
4
5 let HelperInstances = new Map();
... lines 6 - 233
234 })(window, jQuery, Routing, swal);
In the constructor() , set the new object into that map: HelperInstances.set() ... and for the key - this may look a little weird - use
this :
In other words, we key this HelperInstance to ourselves, our instance. That means that later, to use it, say
HelperInstances.get(this).getTotalWeightString() :
This is awesome! Helper is still private, but now each RepLogApp instance will have its own instance of Helper in the Map .
Notice that these are not being used: I'm not setting them to a variable. In other words, they are created, and then they're gone:
no longer referenced by anything. Below that - and this won't make sense yet, call setTimeout() , pass it an arrow function, and
inside, console.log(HelperInstances) . Set that to run five seconds after we load the page:
Mysterious!?
Ok, refresh! And then wait a few seconds... we should see the Map printed with five Helper objects inside. Yep, we do! One
Helper for each RepLogApp we created.
But now, back in RepLogApp , after we set the HelperInstance , simply return:
This is a temporary hack to show off garbage collection. Now that we're returning immediately, when we create a new
RepLogApp object, it's not attaching any listeners or adding itself as a reference to anything in the code. In other words, this
object is not attached or referenced anywhere in memory. Because of that, RepLogApp objects - and their Helper objects -
should be eligible for garbage collection.
Now, garbage collection isn't an instant process - it takes places at intervals, and it's up to your JavaScript engine to worry
about that. But if you're using Chrome, you can force garbage collection! On the timeline tab, you should see a little garbage
icon. Try this: refresh! Quickly click the "collect garbage" button, and then see what prints in the console.
Ok, so HelperInstances still has 5 objects inside. In other words, the Helper objects were not garbage collected. Why? Because
they are still being referenced in the code... by the Map itself!
Go back and repeat the dance: refresh, hit the garbage icon, and then go to the console. Woh! Check this out! The WeakMap is
empty. Remember, this is its superpower! Since none of the RepLogApp objects are being referenced in memory anymore, both
those and their Helper instances are eligible for garbage collection. When you use Map , it prevents this: simply being inside of
the Map counts as a reference. With WeakMap that doesn't happen.
Ok, I know, this was still pretty darn advanced. So you may or may not have this use case. But this is when you will see
WeakMap used instead of Map . For us it means we should use Map in normal situations... and WeakMap only if we find
ourselves with this problem.
The Map object is perfect for maps, or associative arrays as we call them in the PHP biz. But what about true, indexed arrays?
Well actually, JavaScript has always had a great way to handle these - it's not new! It's the Array object.
Well, the Array object isn't new, but it does have a new trick. Let's check out an example: when the page loads, we call
loadRepLogs() :
This fetches an array of repLog data via AJAX and then calls _addRow() on each to add the <tr> elements to the table.
But once we add the table rows... we don't actually store those repLog objects anywhere. Yep, we use them to build the page...
then say: Adios!
Now, I do want to start storing this data on my object, and you'll see why in a minute. Up in the constructor , create a repLogs
property set to new Array() :
239 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 6
7 class RepLogApp {
8 constructor($wrapper) {
... line 9
10 this.repLogs = new Array();
... lines 11 - 30
31 }
... lines 32 - 184
185 }
... lines 186 - 237
238 })(window, jQuery, Routing, swal);
If you've never seen that Array object before... there's a reason - stay tuned! Then, down in _addRow() , say this.repLogs() -
which is the Array object - this.repLogs.push(repLog) :
Back up in loadRepLogs() , after the for loop, let's see how this looks: console.log(this.repLogs) . Oh, and let's also use one of its
helper methods: this.repLogs.includes(data.items[0]) :
Refresh! Yea! We see the fancy Array and the word true . Awesome!
But hold on! The Array object may not be new, but the includes() function is new. In fact, it's really new - it wasn't added in
ES2015, it was added in ES2016! ES2015 came with a ton of new features. And now, new ECMAScript releases happen
yearly, but with many fewer new things. The Array ' includes() function is one of those few things in ES2016. Cool!
Oh, and by the way, you don't typically say new Array() ... and PHPStorm is yelling at us! In the wild, you just use [] :
That's right, when you create an array in JavaScript, it's actually this Array object.
At the bottom of this file, change the constructor() for Helper to have a repLogs argument. Set that on a repLogs property:
Below in calculateTotalWeight() , instead of using the $wrapper to find all the tr elements, just pass this.repLogs to the static
function. Inside of that, update the argument to repLogs :
Previously, _calculateWeights() would loop over the $elements and read the data-weight attribute on each. Now, loop over
repLog of repLogs . Inside, set totalWeight += repLog.totalWeightLifted :
It's nice to calculate the total weight from our source data, rather than reading it from somewhere on the DOM.
Okay! Try that out! The table still loads... and the total still prints!
Tip
Actually, we made a mistake! When you delete a rep log, the total weight will no longer update! That's because we now
need to remove the deleted repLog from the this.repLogs array.
No problem! The fix is kinda cool: it involves adding a reference to the $row element: the index on the this.repLogs array
that the row corresponds to. This follows a pattern that's somewhat similar to what you'll see in ReactJS.
249 lines web/assets/js/RepLogApp.js
... lines 1 - 2
3 (function(window, $, Routing, swal) {
... lines 4 - 6
7 class RepLogApp {
... lines 8 - 73
74 _deleteRepLog($link) {
... lines 75 - 83
84 return $.ajax({
... lines 85 - 86
87 }).then(() => {
88 $row.fadeOut('normal', () => {
89 // we need to remove the repLog from this.repLogs
90 // the "key" is the index to this repLog on this.repLogs
91 this.repLogs.splice(
92 $row.data('key'),
93 1
94 );
95
96 $row.remove();
97
98 this.updateTotalWeightLifted();
99 });
100 })
101 }
... lines 102 - 180
181 _addRow(repLog) {
182 this.repLogs.push(repLog);
... lines 183 - 186
187 const html = rowTemplate(repLog);
188 const $row = $($.parseHTML(html));
189 // store the repLogs index
190 $row.data('key', this.repLogs.length - 1);
191 this.$wrapper.find('tbody').append($row);
192
193 this.updateTotalWeightLifted();
194 }
195 }
... lines 196 - 247
248 })(window, jQuery, Routing, swal);
Introducing Set
But, ES2015 added one more new object that's related to all of this: Set . It's a lot like Array : it holds items... but with one
important difference.
7 lines play.js
1 let foods = [];
... lines 2 - 7
Let's add gelato to the array and tortas . Clear everything else out:
7 lines play.js
1 let foods = [];
2 foods.push('gelato');
3 foods.push('tortas');
... lines 4 - 7
And ya know what? Gelato is so good, we should add it again. At the bottom, log foods :
7 lines play.js
1 let foods = [];
2 foods.push('gelato');
3 foods.push('tortas');
4 foods.push('gelato');
5
6 console.log(foods);
When you run the script, there are no surprises: gelato , tortas , gelato .
But now, change the array to be a new Set() . To add items to a Set , you'll use add() instead of push() - but it's the same idea:
7 lines play.js
1 let foods = new Set();
2 foods.add('gelato');
3 foods.add('tortas');
4 foods.add('gelato');
5
6 console.log(foods);
Woh! Just two items! That's the key difference between Array and Set : Set should be used when you need a unique
collection of items. It automatically makes sure that duplicates aren't added.
Oh, and there is also a WeakSet , which has the same super powers of WeakMap - all that garbage collection stuff. But, I
haven't seen any decent use-case for it. Just use Set ... or Array if values don't need to be unique.
Chapter 18: yarn & npm: Installing Babel
Ok, so do you love ES2015 yet? Man, I sure do. So... let's go use it immediately on our site! Oh wait.... didn't I mention that its
features aren't supported by all browsers? Whoops. Yea, the latest version of Chrome supports everything... but if some of our
users have older browsers, does this mean we can't use ES2015?
But wait! There's a disturbance in the Node.js package manager force. Yes, there is another... package manager... called Yarn!
It's sort of a competitor to npm, but they work almost identically. For example, in PHP, we have a composer.json file. In Node,
we will have a package.json ... and you can use either npm or yarn to read it.
In other words,... you can use npm or yarn : they basically do the same thing. You could even have some people on your team
using npm , and others using yarn .
We're going to use Yarn... because it's a bit more sophisticated. But, that means you need to install it! I'm on a Mac, so I already
installed it via Brew. Check Yarn's Docs for your install details.
Creating a package.json
To use Yarn, we need a package.json file... which we don't have yet! No worries, to create one, run:
$ yarn init
It'll ask a bunch of questions - none are too important - and you can always update your new package.json file by hand later:
9 lines package.json
1 {
2 "name": "javascript",
3 "version": "1.0.0",
4 "main": "index.js",
5 "repository": "git@github.com:knpuniversity/javascript.git",
6 "author": "Ryan Weaver <ryan@thatsquality.com>",
7 "license": "MIT"
8 }
Awesome!
Installing Babel
Ok, the wonderful tool that will fix all of our browser compatibility problems with ES2015 is called Babel. Google for it to find
babeljs.io .
In a nut shell, Babel reads new JavaScript code, i.e. ES2015 code, and recompiles it to old JavaScript so that all browsers can
understand it. Yea, it literally reads source code and converts it to different source code. It's wild!
Go to Setup. In our case, to see how it works, we're going to use the CLI, which means we will run Babel from the command
line. To install Babel CLI, it wants us to run npm install --save-dev babel-cli .
That does the same thing, but with more exciting output!
This made a few changes to our project. Most importantly, it added this devDependencies section to package.json with babel-cli
inside:
12 lines package.json
1 {
... lines 2 - 7
8 "devDependencies": {
9 "babel-cli": "^6.22.2"
10 }
11 }
It also created a yarn.lock file: which works like composer.lock . And finally, the command added a new node_modules/
directory, where it downloaded babel-cli and all of its friends, um, dependencies. That is the vendor/ directory for Node.
18 lines .gitignore
... lines 1 - 16
17 /node_modules
We don't need to commit that directory because - thanks to the package.json and yarn.lock files - anyone else can run
yarn install to download everything they need.
$ ./node_modules/.bin/babel
Tip
$ node ./node_modules/.bin/babel
That is the path to the executable for Babel. Next, point to our source file: web/assets/js/RepLogApp.js and then pass -o and the
path to where the final, compiled, output file should live: web/assets/dist/RepLogApp.js .
Before you run that, go into web/assets , and create that new dist/ directory. Now, hold your breath and... run that command!
Before we look at it, go into index.html.twig and update the script tag to point to the new dist version of RepLogApp.js that Babel
just created:
67 lines app/Resources/views/lift/index.html.twig
... lines 1 - 53
54 {% block javascripts %}
... lines 55 - 57
58 <script src="{{ asset('assets/dist/RepLogApp.js') }}"></script>
... lines 59 - 65
66 {% endblock %}
So what did Babel do? What are the differences between those two files? Let's find out! Open the new file:
218 lines web/assets/dist/RepLogApp.js
1 'use strict';
2
3 (function (window, $, Routing, swal) {
4
5 let HelperInstances = new WeakMap();
6
7 class RepLogApp {
8 constructor($wrapper) {
9 this.$wrapper = $wrapper;
10 this.repLogs = new Set();
11
12 HelperInstances.set(this, new Helper(this.repLogs));
13
14 this.loadRepLogs();
15
16 this.$wrapper.on('click', '.js-delete-rep-log', this.handleRepLogDelete.bind(this));
17 this.$wrapper.on('click', 'tbody tr', this.handleRowClick.bind(this));
18 this.$wrapper.on('submit', RepLogApp._selectors.newRepForm, this.handleNewFormSubmit.bind(this));
19 }
... lines 20 - 165
166 }
167
168 /**
169 * A "private" object
170 */
171 class Helper {
172 constructor(repLogSet) {
173 this.repLogSet = repLogSet;
174 }
... lines 175 - 197
198 }
199
200 const rowTemplate = repLog => `
201 <tr data-weight="${repLog.totalWeightLifted}">
202 <td>${repLog.itemLabel}</td>
203 <td>${repLog.reps}</td>
204 <td>${repLog.totalWeightLifted}</td>
205 <td>
206 <a href="#"
207 class="js-delete-rep-log"
208 data-url="${repLog.links._self}"
209 >
210 <span class="fa fa-trash"></span>
211 </a>
212 </td>
213 </tr>
214 `;
215
216 window.RepLogApp = RepLogApp;
217 })(window, jQuery, Routing, swal);
Hmm, it actually doesn't look any different. And, that's right! To prove it, use the diff utility to compare the files:
Wait, so there are some differences... but they're superficial: just a few space differences here and there. Babel did not actually
convert the code to the old JavaScript format! We can still see the arrow functions!
Here's the reason. As crazy as it sounds, by default, Babel does... nothing! Babel is called a transpiler, which other than being a
cool word, means that it reads source code and converts it to other source code. In this case, it parses JavaScript, makes some
changes to it, and outputs JavaScript. Except that... out-of-the-box, Babel doesn't actually make any changes!
Adding babel-preset-env
We need a little bit of configuration to tell Babel to do the ES2015 to ES5 transformation. In other words, to turn our new
JavaScript into old JavaScript.
And they mention it right on the installation page! At the bottom, they tell you that you probably need something called
babel-preset-env . In Babel language, a preset is a transformation. If we want Babel to make the ES2015 transformation, we
need to install a preset that does that. The env preset is one that does that. And there are other presets, like CoffeeScript,
ActionScript and one for ReactJS that we'll cover in the future!
Perfect! To tell Babel to use that preset, at the root of the project, create a .babelrc file. Babel will automatically read this
configuration file, as long as we execute Babel from this directory. Inside, add "presets": ["env"] :
4 lines .babelrc
1 {
2 "presets": ["env"]
3 }
Woh! Now there are big differences! In fact, it looks like almost every line changed. Let's go look at the new RepLogApp.js file in
dist/ - it's really interesting.
Below, instead of using the new class syntax, it calls one of those functions - _createClass() - which helps to mimic that new
functionality:
Our arrow functions are also gone, replaced with classic anonymous functions.
There's a lot of cool, but complex stuff happening here. And fortunately, we don't need to worry about any of this! It just works!
Now, even an older browser can enjoy our awesome, new code.
Tip
The purpose of the babel-preset-env is for you to configure exactly what versions of what browsers you need to support. It
then takes care of converting everything necessary for those browsers.
But... isn't that only available in ES2015? Yep! Babel's job is to convert all the new language constructs and syntaxes to the old
version. But if there are new objects or functions, it leaves those. Instead, you should use something called a polyfill.
Specifically, babel-polyfill . This is another JavaScript library that adds missing functionality, like WeakMap , if it doesn't exist in
whatever browser is running our code.
We actually did something just like this in the first episode. Remember when we were playing with the Promise object? Guess
what? That object is only available in ES2015. To prevent browser issues, we used a polyfill.
To use this Polyfill correctly, we need to go a little bit further and learn about Webpack. That's the topic of our next tutorial...
where we're going to take a huge step forward with how we write JavaScript. With webpack, we'll be able to do cool stuff like
importing JavaScript files from inside of each other:
myHelperFunctions.now();
Heck, you can even import CSS from inside of JavaScript. It's bananas.
Ok guys! I hope you learned tons about ES2015/ES6/Harmony/Larry! You can already start using it by using Babel. Or, if your
users all have brand-new browsers, then lucky you!