Chapter 14 ES6+ Syntax
As discussed in Chapter 10, the ECMAScript specification for the JavaScript language has gone through several different versions, each of which added new syntax and features to try and make the language more powerful, expressive, or easier to work with.
After the JavaScript language was initially written in 1995 and then iterated on through the late 90s, it mostly stayed consistent (with some small changes) for about a decade—until 2009 with the release of JavaScript v5. The next major version wasn’t released until 2015—although officially called “ECMAScript 2015”, most developers refer to it by the working name “ES6” (e.g., version 6 of the language). ES6 introduced a host of notable and useful syntactic features—including let
and const
! Since then the language has been updated annually, with each version named after its release year (ES 2016 was released in 2016, ES 2017 released in 2017, etc).
This chapter introduces some of the most notable and useful features introduced in ES6 and later versions of JavaScript—particularly those that will be needed when using the React framework (discussed in the following chapters).
At this point ES6 is mature enough that it is almost entirely supported by modern browsers, with the notable exception of Internet Explorer. However, the JavaScript interpreter in older browsers (and IE) won’t be able to recognize the new syntax introduced in this version. Instead, you would need to covert that code into equivalent ES5 (or earlier) code, which can be understood. The easiest way to do this is with the Babel compiler, which will “transpile” JavaScript code from one version to another. The next chapter discusses how to perform this transpiling with React (spoiler: it is automatically performed by provided build tools), but it is also possible to install and use the Babel compiler yourself.
14.1 ES6+ Syntax Features
Syntactic Sugar causes cancer of the semicolon - Alan Perlis
Many of the ES6 features are syntactic shortcuts—they provide syntax for writing functions and operations in a more concise way. These features aren’t necessary for writing JavaScript, but they can make your code easier to write and to read (once you know the syntax), and are considered the “normal” way of writing modern JavaScript. This section discusses a few of the most common syntactic features.
Arrow Functions
As described in Chapter 11, JavaScript lets you define functions as anonymous values:
const sayHello = function(name) {
return 'Hello '+name;
}
As you have seen, the use of anonymous functions is incredibly common in JavaScript, particularly when used as anonymous callbacks. Because this is so common, ES6 introduced a simpler, more concise shortcut syntax for quickly specifying anonymous functions. Though officially called lambda functions, they are more commonly known as arrow functions, because of how they utilize an “arrow” symbol =>
:
const sayHello = (name) => {
return 'Hello '+name;
}
To turn an anonymous function into an arrow function, you just remove the function
keyword from the definition, and place an arrow =>
between the parameter list and the block (indicating that the parameter list “goes to” the following block). This saves you a couple of characters when typing!
It’s also possible to omit the ()
around the argument list in an arrow function:
//may omit the parentheses around the argument -- but don't do this!
const sayHello = name => {
return 'Hello '+name;
}
//requried to include the parentheses if there are no arguments
const printHello = () => {
console.log('Hello world');
}
However, I find this makes the syntax harder to read (when reading the the text, it looks like a variable declaration until you get to the =>
). So you should always include the parentheses ()
on the parameter list as it helps with readability, as well as making it easier to adjust the parameters later—thus meeting the requirements of good code style.
Arrow functions can be particularly nice when doing anonyous callback functions (literals):
const array = [1,2,3]; //an array to work with
//function declaration syntax
array.map(function(num) {
return num * 2; //multiply each item by 2
});
//arrow function syntax
array.map((num) => {
return num * 2; //multiply each item by 2
});
Using an arrow function makes it slightly more obvious that the function is taking an input and then producing (=>
) an output—letting you focus on the method of interest (such as .map()
) rather than the callback. However, it does make it less obvious that the argument to .map()
is a function until you are used to identifying the =>
arrow.
Arrow functions are lexically scoped, meaning that the value of the this
keyword is retained even inside of the function. This can be important in some contexts (such as classes), and is one of the reasons to prefer arrow functions when defining callback functions).
For very simple functions, arrow functions can be written to be even more compact: if the function body has only a single statement, you can leave the {}
off the block, as well omit the return
. This is called using a concise body format. The function will return the result of the of the single statement, which will either be an expression or undefined
. For example:
//function declaration syntax
function makeFullName(first, last) {
return first + " " + last;
}
//block body arrow function syntax
const makeFullName = (first, last) => {
return first + " " + last;
}
//concise body arrow function syntax
const makeFullName = (first, last) => first + " " + last;
Note that if the expression of a concise-body arrow function returns undefined
(such as from a console.log
) statement, then the arrow function will also return undefined
.
//concise body arrow function syntax
const sayHello = (name) => console.log("Hello" + name);
//that is identical to use block body syntax. Yes, returning the result of
//console.log() is odd
const sayHello = (name) => {
return console.log("Hello" + name);
}
Using a concise body synatx could save you a few more characters (and even line breaks!). However, it can also make the function much more difficult to read—particularly when it has more than 1 argument—and harder to identify as a function because the block isn’t obvious. Concise body functions are also harder to modify—they can only have a single statement, so if you want to have the function to more than 1 thing—including using a loop or if
statment—you would need to convert it into a block body anyway. (Trying to make that one expression really complex is also poor style). And because you can’t add additional lines, it makes it harder to debug since you can’t add console.log
statements or otherwise take debugging steps. For this reason, you should almost never use concise-body arrow function syntax except on the very simplest of callbacks—it is always better to use block body syntax.
Overall, arrow functions (specifically block body arrow functions) are clean syntactic shortcut that are the “normal” way of writing anonyous callback functions—you will see them all over examples and professionally written code. “Top-level” functions should be written using the regular function declaration syntax, but callback functions should always be written using block-body arrow functions.
Destructuring
ES6 also introduced destructing assignments, which allow you to assign each element of an array (or each property of an object) into separate variables all in a single operation. This is called “destructuring” because you are “unpacking” the values of an array into a bunch of different variables.
To destructure an array in JavaScript, you write the variables you wish to assign values to inside of square brackets []
on the left-hand side of the assignment—almost like you are assigning to an array!
const myArray = [1, 2, 3];
const [x, y, z] = myArray; //assign myArray elements to `x`, `y`, `z` respectively
console.log(x); //=> 1;
console.log(y); //=> 2;
console.log(z); //=> 3;
When destructuring an array, values are assigned in order—the value at index 0 goes to the first variable, the value at index 1 goes to the second variable, etc.
It’s possible for the “list of variables” and the size of the destructured array to not match. If there are more values in the array than there are variables, then the extra values will not be assigned to variables (they will be ignored). there are more variables than values in the array, then the excess variables will be undefined
(because the value from that position in the array is undefined
).
const bigArray = [1, 2, 3, 4, 5];
const [a, b] = bigArray; //only the first 2 elements will be assigned
console.log(a); //=> 1;
console.log(b); //=> 2;
//other elements are not assigned
const smallArray = [1, 2]
const [x, y, z] = smallArray;
console.log(x); //=> 1;
console.log(y); //=> 2;
console.log(z); //=> undefined; (because smallArray[2] is undefined!)
It is also possible to destructure objects using a similar syntax, except that you put the variables you wish to assign to inside of {}
on the left-hand side of the assignment—as if you were assigning to an object!
const myObject = {a: 1, b: 2, c: 3};
const {a, b, c} = myObject;
console.log(a); //=> 1; myObject.a
console.log(b); //=> 2; myObject.b
console.log(c); //=> 3; myObject.c
Note that when destructuring an object, the variables specified on the left side of the assignment are assigned the value from the matching property in the object. Thus in const {a, b, c} = myObject
, the a
is assigned myObject.a
, the b
is assigned myObject.b
, etc. The order of the elements on either side of the equation doesn’t matter, because object properties are unordered!
const myObject = {a: 1, b: 2, c: 3};
const {c, b, a} = myObject; //"order" of variables doesn't matter
console.log(a); //=> 1
console.log(b); //=> 2;
console.log(c); //=> 3;
There is also syntax to assign object properties to different variable names as well, (see the destructuring documentation for details).
As with array destructuring, the list of variables and the number of object properties do not need to match. Any unmatched object properties will not be assign to variables, and any variables without matching object properties will not be assigned a value (will remain undefined
).
Destructuring is a useful syntactic shortcut for doing multiple assignments. In React it is often used to create local variables for easier coding than having to constantly write object.property
:
//a function that expects an object as an argument
function getBMI(personObject) {
const {height, weight} = personObject; //destructure to local variables
//this avoids needing to refer to `personObject.height`
return 703*weight/(height*height); //calculate and return value
}
const person = {name: 'Ada', height: 64, weight: 135} //an example person
const adaBMI = getBMI(person); //pass in the object
In fact, this behavior is so common that it also possible to destructure function parameters in the function declaration itself! To do this, you replace the object name in the parameter list with the set of destructured variables (what would go on the left-hand side of the destructuring assignment):
//a function with the object argument destructured
//notice the `{} in the argument list`
function getBMI( {height, weight} ) { //implicitly calls `{height, weight} = personObject`
return 703*weight/(height*height); //calculate and return value
}
const person = {name: 'Ada', height: 64, weight: 135} //an example person
const adaBMI = getBMI(person); //pass in the object
To be clear: in the second example, the getBMI()
function still expects only 1 argument—that single argument is just destructured into two variables when the function is called.
This syntax can save a line of code, but it also makes it easier to misread the function and think it takes more arguments than it does (if you miss the {}
in the argument list). On the other hand, this syntax does make it clear exactly what properties the expected object will need to have—this function clearly defines its API that the “person object” will need to have a height
and a weight
(and not just e.g., a name
or an age
). As with any alternate syntax, there are trade-offs in terms of code style and readability. Nevertheless, this last syntax form is often used in React (where there are many functions that expect a single object, but you need to access their individual properties).
Spreading
Introduced in ES6 but expanded in ES 2018, the spread operator (an ellipsis ...
) lets you reference “the elements of” an existing array or object when declaring a new value. It “expands” the contents of the array or object into the new value. You write it as a unary operator, putting the three dots in front of the value you wish to expand:
const originalArray = ['a', 'b', 'c', 'd'];
//new array contains "the elements of" `originalArray`
const newArray = [...originalArray, 'e', 'f'];
console.log(newArray) //['a', 'b', 'c', 'd', 'e', 'f']
In this example the newArray
contains all the elements of the originalArray
, as well as additional elements. Indeed it is particularly useful when you want to make a “copy” of an arrary or object, with or without additional values.
//assigning an array to a different variable doesn't create a new array!
const myArray = ['a', 'b', 'c', 'd'];
const secondArray = myArray; //just a new label for the same value
myArray[0] = 'Z'; //changing the original variable changes second variable as well
console.log(secondArray[0]) //'Z'
//use spread to create an actual copy that is a different array
const clonedArray = [...myArray];
myArray[1] = 'Q'; //modify the original variable
console.log(clonedArray[1]); //'b'; the clonedArray is different
The spread operator can also be used in the creation of new objects; spreading the elements of an object spreads both the property keys and the values:
const person = {name: 'Ada', height: 64, weight: 135}
const copyOfPerson = {...person}; //clone the object
console.log(copyOfPerson); // {name: 'Ada', height: 64, weight: 135}
//all off the properties are "spread" into the new object
const personWithHat = {hat: 'bowler', ...person}
console.log(person); //has properties 'name', 'height', 'weight'
console.log(personWithHat); //has properties 'hat', 'name', 'height', 'weight'
When combined with destructuring, the spread operator can also refer to the “rest of” the elements in an array or objec that have not been assigned to specific variables. In this situation it is often referred to as a rest operator.
const dimensions = [10, 20, 30, 40];
//extra values are "spread" into destructuring slots
const [width, height, ...others] = dimensions
console.log(width); //=> 10
console.log(height); //=> 20
console.log(others); //=> [30, 40]; the rest of the values!
Finally, the spread operator can also be used to specify that a function can take an inconsistent number of arguments, and to gather all of these values into a single array. You do this by including the spreading operator in the function declaration. The arguments will then be gathered into that single variable—almost the opposite of destructuring! (And yes the fact that the “spread” operator is “gathering” values is confusing).
//a function that logs out all of the arguments
function printArgs(...argArray){
//all the arguments will be grouped into a single array `args`
for(const arg of argArray) {
console.log(arg); //can log out all of them, no matter how many!
}
}
printArgs('a', 'b', 'c'); //=> "a" "b" "c"
printArgs(1, 2, 3, 4, 5, 6); //=> "1" "2" "3" "4" "5" "6"
//a function that adds up all the arguments (no matter how many!)
function sum(...numbers) {
//numbers is an array, so we can `reduce()` it!
const total = numbers.reduce((runningTotal, num) => {
return runningTotal + num; //new total
}, 0); //start at 0
return total;
//or as one line with a concise arrow function (not pretty!)
return numbers.reduce((total, n) => total+n);
}
sum(3 ,4, 3); // => 10
sum(10, 20, 30, 40); // => 100
The spread operator is not frequently used this way with React, but it’s handy to know about!
Template Strings
In ES6, you can declare Strings that contain embedded expressions, allowing you to “inject” an expression directly into a string (rather than needing to concatenate the String with that expression). These are known as template strings (or template literals). Template strings are written in back ticks (``
) rather than quotes, with the injected expressions written inside of a ${}
token:
const name = 'world';
const greeting = `Hello, ${name}!`; //template string
console.log(greeting); //=> "Hello, world!"
Template strings can also include line breaks, allowing you to make multi-line strings!
Note that you can put any expression inside the ${}
token; however, it’s best practice to keep the expression as simple as possible (such as by using a local variable) to support readability:
const name = 'world';
//greeting with capitalization. Don't do this!
const greeting = `Hello, ${name.substr(0,1).toUpperCase() + name.substr(1)}!`
console.log(greeting); //=> "Hello, World!";
//do this instead!
const capitalizedName = name.substr(0,1).toUpperCase() + name.substr(1);
const greeting = `Hello, ${capitalizedName}`
console.log(greeting); //=> "Hello, World!";
One more example:
//function expects a name, an animal, and a verb
function makeExcuse(name, animal, verb) {
const email = `Hello Professor ${name},
Please excuse my missing assignment, as my ${animal} ate it.
${verb} you later,
A Student`;
return email;
}
const excuse = makeExcuse('Ross', 'Lemur', 'Smell');
14.2 Modules
So far, you’ve mostly been writing all of your JavaScript code in a single script file (e.g., index.js
), even if you have included some other libraries like Bootstrap or JQuery via additional <script>
tags. But as applications get larger and more complex, this single script file can quickly become unwieldy with hundreds or thousands of lines of code implementing dozens of features. Such large files become difficult to read and debug (“where was that function defined?”), can introduce problems with the shared global namespace (“did I already declare a user
variable?”), and overall mixes code in a way that violates the Separation of Concerns principle.
The solution to this problem is to split up your application’s code into separate modules (scripts), each of which is responsible for a separate piece of functionality. And rather than loading each module through a separate <script>
tag (potentially leading to ordering and dependency issues while continuing to pollute the global namespace), you can define each module as a self-contained script that explicitly “imports” (loads) the functions and variables it needs from other modules. This allows you to better organize your program as it gets large.
While separating code into modules is a common in the Node.js environment, ES6 adds syntax that allows you to treat individual .js
files as modules that can communicate with one another. These are known as ES6 Modules.
A browser loads modules differently than “regular” scripts; to load some code as a module, you specify a type="module"
attribute in the <script>
tag for that module:
<!-- load scripts that can import from other modules -->
<script type="module" src="path/to/firstModule.js"></script>
<script type="module" src="path/to/secondModule.js"></script>
For security reasons, modules are not able to communicate when a web page is accessed via the file://
protocol. In order to use modules, you will need to access the page through a a web server, such as by using a development server like live-server
.
However, most commonly you will not use modules directly inside of the browser. Instead, modules will be loaded and compiled through an external build tool such as webpack. In these situations, you’ll be using the module loader provided by the Node JavaScript runtime. This loader does introduce a few quirks in syntax, which are noted in the syntax explanation.
require()
to load a module (which returns a single “exported” variable as a result). Values are exported from a module by assigning them to the module.exports
global. This course will exclusively utilize ES6 Modules—you should not use require()
—but it’s good to be aware of the alternate CommonJS approach when searching for help.
Module Syntax
As in Java or Python, a JavaScript module is able to “load” external modules or libraries by using the import
keyword:
//Java example: import the `Random` variable from `java.util` module
import java.util.Random;
# Python example: import the `randint` variable from `random` module
from random import randint
//JavaScript:
//import the `Random` variable from a `util.js` module
import { Random } from './util.js';
//import the `randint` and `randrange` variables from a `random.js` module
import { randint, randrange } from './random.js'
This is most common version of the ES6 import
syntax (called a named import): you write the keyword import
, following by a set of braces { }
containing a comma-separated sequence of variables you wish to “import” from a particular module. This is followed by the from
keyword, followed by a string containing the relative path to the module script to import. Note that in Node-based systems (such as the React build environment) including the extension of the module script is optional (by default, the module loader will look for files ending in .js
).
Be sure to include the
./
to indicate that you’re loading a file from a particular directory. If you leave that off, the Node module loader will look for a module installed on the load path (e.g., innode_modules/
). You do omit the./
when you load third-party libraries such as jQuery or React://with jquery installed in `node_modules/` //import the `$` and `jQuery` variables import { $, jQuery } from 'jquery'; //no `./` in front of library name
Note that the variables imported from a modules can be of any type—strings, objects, and most commonly functions.
Any variable that a module import
s need to be declared as available in the module it is coming from—simlar to using the public
keyword in Java. To do this, you export the variable (so that it can be “imported” elsewhere) by using the export
keyword, placed in front of the variable declaration:
/*** my-module.js ***/
//define and export variables for use elsewhere
export const question = "Why'd the chicken cross the road?";
export const answer = "To get to the other side";
export function laugh() { //an exported function
console.log("hahaha");
}
This is called a named export (in contrast to a default export described below). Once variables have been exported, they can be imported by another script file:
/*** index.js ***/
//import all 3 variables from `my-modules.js`
//now all 3 variables exist in this module and can be used
import { question, answer, laugh } from './my-module.js';
console.log(question); //=> "Why'd the chicken cross the road?"
console.log(answer); //=> "To get to the other side"
laugh(); //"hahaha"
Importantly, only the variables you export
are made available to other modules! Anything that does not have that keyword will not be available—it will remain “private” to its own module. The best practice is to only export
values that you are sure need to be imported and used by other modules.
Above is the basic syntax for using named imports and exports. But there are other syntactical options that can be included when using import
and export
.
For example, you can use the as
keyword to “alias” a value either when it is exported (so it is shared with a different name) or when it is imported (so it is loaded and assigned a different name). This is particularly useful when trying to produce a clean API for a module (so you export
values with consistent names, even if you don’t use those internally), or when you want to import
a variable with a very long name.
/*** my-module.js ***/
export function foo() { return 'foo'; } //standard named export
//provide an "alias" (consumer name) for value when exporting
export { bar as yourBar };
//will not be available (a "private" function)
function baz() { return 'baz'; }
/*** index.js ***/
//provide "alias" for value when importing!
import {foo as myFoo} from './my-module.js';
myFoo(); //=> 'foo'
foo(); //error, does not exist (aliased the variable as `myFoo`)
import { yourBar } from './my-module.js'; //import value by exported name
yourBar() //=> 'bar'
import { bar } from './my-module.js'; //error, no value `bar` exported
It is possible to import
everything that was exported by a module using import * as
syntax. You specify an object name that will represent the values exported module (e.g., theModule
in the below example), and each exported variable will be a property of that object. This can be useful when you may be adding extra exports
to a module during development, but don’t want to keep adjusting the import
statement, but it does mean that the rest of the code may be harder to read since you need to unpack the properties from the object.
/*** index.js ***/
import * as theModule from './my-module'; //import everything that was exported
//loads as a single object with values
//as properties
theModule.foo(); //=> 'foo'
theModule.yourBar(); //=> 'bar'
theModule.baz(); //Error (private function so wasn't exported)
Finally, each module can also export
a single (just one!) default variable, which provides a slight shortcut when importing the variable from that module. You specify the default export by including the default
keyword immediately after export
:
/*** my-module.js ***/
//this function is the "default" export
export default function sayHello() {
return 'Hello world!';
}
/*** index.js ***/
import greet from './my-module'; //import the default value
//`greet` is alias defined in this module
greet(); //=> "Hello world!"
When importing a default
export, you don’t include the {}
with the name of the variable, but instead provide a variable name (“alias”) you wish to refer to that exported value by.
Note that it is also possible to make anonymous values into default
exports:
/*** animals.js ***/
export default ['lion', 'tiger', 'bear']; //export anonymous array
The default
export technique is particularly common in object-oriented frameworks like React, or in large frameworks like Bootstrap with lots of different inddependent modules. Using default exports allows you to make each JavaScript module contain the code for a single function or class
; that single function then is made the default
export, allowing other modules to import it quickly and easily as e.g., import MyComponent from './MyComponent.js'
.
That said, in general, I recommend you use named imports in most situations—it’s less syntax to remember and reduces the number of decisions made around whether something is default or not. You need to be aware of default
exports because many third-party libraries use them, but for your own work its often best to stick with named imports.
14.3 Classes
Classes are no longer commonly used in React and are somewhat discouraged in JavaScript. This section is retained for historical and informational purposes, but can be considered “deprecated” for this course.
While JavaScript is primarily a scripting and functional language, it does support a form of Object Oriented Programming like that used in the Java language. That is, we are able to define classes of data and methods that act on that data, and then instantiate those classes into objects that can be manipulated. ES6 introduces a new class
syntax so that creating classes in JavaScript even looks like how you make classes in Java!
Why Classes?
The whole point of using classes in programming—whether Java or JavaScript—is to perform abstraction: we want to be able to encapsulate (“group”) together parts of our code so we can talk about it at a higher level. So rather than needing to think about the program purely in terms of Numbers
, Strings
, and Arrays
, we can think about it in terms of Dogs
, Cats
or Persons
.
In particular, classes encapsulate two things:
The data (variables) that describe the thing. These are known as attributes, fields or instance variables (variables that belong to a particular instance, or example, of the class). For example, we might talk about a
Person
object’sname
(a String),age
(a Number), and Halloween haul (an array of candy).The behaviors (functions) that operate on, utilize, or change that data. These are known as methods (technically instance methods, since they operate on a particular instance of the class). For example, a
Person
may be able tosayHello()
,trickOrTreat()
, oreatCandy()
.
In JavaScript, an Object’s properties can be seen as the attributes of that object. For example:
const person = {
name: 'Ada',
age: 21,
costume: 'Cheshire Cat'
trickOrTreat: function(newCandy){
this.candy.push(newCandy);
}
}
//tell me about this person!
console.log(person.name + " is a " + person.costume);
This Object represents a thing with name
, age
and costume
attributes (but we haven’t yet indicated that this Object has the classification of “Person”). The value of the trickOrTreat()
property is a function (remember: functions are values!), and is an example of how an Object can “have” a function.
- JavaScript even uses the
this
keyword to refer to which object that function being called on, just like Java! See below for more on thethis
keyword and its quirks.
A Class (classification) acts as template/recipe/blueprint for individual objects. It defines what data (attributes) and behaviors (methods) they have. An object is an “instance of” (example of) a class: we instantiate an object from a class. This lets you easily create multiple objects, each of which can track and modify its own data. ES6 classes provide a syntax by which these “templates” can be defined.
React’s preferred styling no longer relies heavily on class syntax, but it’s still a good idea to be familiar with it to help with your understanding and in case you need to do more complex work.
Review: Classes in Java
First, consider the following simple class defined in Java (which should be review from earlier programming courses):
//class declaration
public class Person {
//attributes (private)
private String firstName;
private int age;
//a Constructor method
//this is called when the class is instantiated (with `new`)
//and has the job of initializing the attributes
public Person(String newName, int newAge){
//assign parameters to the attributes
this.firstName = newName;
this.age = newAge;
}
//return this Person's name
public String getName() {
return this.firstName; //return own attribute
}
//grow a year
public void haveBirthday() {
this.age++; //increase this person's age (accessing own attribute)
}
//a method that takes in a Person type as a parameter
public void sayHello(Person otherPerson) {
//call method on parameter object for printing
System.out.println("Hello, " + otherPerson.getName());
//access own attribute for printing
System.out.println("I am " + this.age + " years old");
}
}
You can of course utilize this class (instantiate it and call its methods) as follows:
public static void main(String[] args) {
//instantiate two new People objects
Person aliceObj = new Person("Alice", 21);
Person bobObj = new Person("Bob", 42);
//call method on Alice (changing her fields)
aliceObj.haveBirthday();
//call the method ON Alice, and PASS Bob as a param to it
aliceObj.sayHello(bobObj);
}
A few things to note about this syntax:
- You declare (announce) that you’re defining a class by using the
class
keyword. - Java attributes are declared at the top of the class block (but assigned in the constructor).
- Classes have constructor methods that are used to instantiate the attributes.
- Class methods are declared inside the class declaration (inside the block, indenting one step).
- Class methods can access (use) the object’s attribute variables by referring to them as
this.attributeName
. - You instantiate objects of the class’s type by using the
new
keyword and then calling a method with the name of the class (e.g.,new Person()
). That method is the constructor, so is passed the constructor’s parameters. - You call methods on objects by using dot notation (e.g.,
object.methodName()
). - Instantiated objects are just variables, and so can be passed into other methods.
ES6 Class Syntax
Here is how you would create the exact same class in JavaScript using ES6 syntax:
//class declaration
class Person {
//a Constructor method
//this is called when the class is instantiated (with `new`)
//and has the job of initializing the attributes
constructor(newName, newAge) {
//assign parameters to the attributes
this.firstName = newName;
this.age = newAge;
}
//return this Person's name
getName() {
return this.firstName; //return own attribute
}
//grow a year
haveBirthday() {
this.age++; //increase this person's age (accessing own attribute)
}
//a method that takes in a Person type as a parameter
sayHello(otherPerson) {
//call method on parameter object for printing
console.log("Hello, " + otherPerson.getName());
//access own attribute for printing
console.log("I am " + this.age + " years old");
}
}
And here is how you would use this class:
//instantiate two new People objects
const aliceObj = new Person("Alice", 21);
const bobObj = new Person("Bob", 42);
//call method on Alice (changing her attributes)
aliceObj.haveBirthday();
//call the method ON Alice, and PASS Bob as a param to it
aliceObj.sayHello(bobObj);
As you can see, this syntax is very, very similar to Java! Just like with JavaScript functions, most of the changes have involved getting rid of type declarations. In fact, you can write a class in Java and then just delete a few words to make it an ES6 class.
Things to notice:
Just like in Java, JavaScript classes are declared using the
class
keyword (this is what was introduced in ES6).Always name classes in PascalCase (starting with an Upper case letter)!
JavaScript classes do not declare attributes ahead of time (at the top of the class). Unlike Java, JavaScript variables always “exist”, they’re just
undefined
until assigned, so you don’t need to explicitly declare them.- In JavaScript, nothing is private; you effectively have
public
access to all attributes and functions.
- In JavaScript, nothing is private; you effectively have
JavaScript classes always have only one constructor (if any), and the function is simply called
constructor()
.- That’s even clearer than Java, where you only know it’s a constructor because it lacks a return type.
Just like in Java, JavaScript class methods are declared inside the class declaration (inside the block, indenting one step).
But note that you don’t need to use the word
function
to indicate that a method is a function; just provide the name & parameters. This is because the only things in the class are functions, so declaring it as such would be redundant.Just like in Java, JavaScript class methods can access (use) the object’s attribute variables by referring to them as
this.attributeName
.Just like in Java, you instantiate objects of the class’s type by using the
new
keyword and then calling a method with the name of the class (e.g.,new Person()
). That method is theconstructor()
, so is passed the constructor’s parameters.Just like in Java, you call methods on objects by using dot notation (e.g.,
object.methodName()
).Just like in Java, instantiated objects are just variables, and so can be passed into other methods.
So really, it’s just like Java—except that for the differences in how you declare functions and the fact that we use the word constructor
to name the constructor methods.
The other difference is that while in Java we usually define each class inside it’s own file, in JavaScript you often create multiple classes in a single file, at the same global “level” as you declared other, non-class functions:
//script.js
'use strict';
//declare a class
class Dog {
bark() { /*...*/ }
}
//declare another class
class Cat {
meow() { /*...*/ }
}
//declare a (non-class) function
function petAnimal(animal) { /*...*/ }
//at the "main" level, instantiate the classes and call the functions
const fido = new Dog();
petAnimal(fido); //pass this Dog object to the function
Although the above syntax looks like Java, it’s important to remember that JavaScript class instances are still just normal Objects like any other. For example, you can add new properties and functions to that object, or overwrite the value of any property. Although it looks like a Java class, it doesn’t really behave like one.
Inheritance
The ES6 class
syntax also allows you to specify class inheritance, by which one class can extend
another. Inheritance allows you to specify that one class is a more specialized version of another: that is, a version of that class with “extra abilities” (such as additional methods).
As in Java, you use the extends
keyword to indicate that one class should inherit from another:
//The "parent/super" class
class Dog {
constructor(name) {
this.name = name;
}
sit() {
console.log('The dog '+this.name+' sits. Good boy.');
}
bark() {
console.log('"Woof!"');
}
}
//The "child/sub" class (inherits abilities from Dog)
class Husky extends Dog {
constructor(name, distance) {
super(name); //call parent constructor
this.distance = distance;
}
//a new method ("special ability")
throwFootball() {
console.log('Husky '+this.name+' throws '+this.dist+' yards');
}
//override (replace) parent's method
bark() {
super.bark(); //call parent method
console.log("(Go huskies!)");
}
}
//usage
const dog = new Husky("Harry", 60); //make a Husky
dog.sit(); //call inherited method
dog.throwFootball(); //call own method
dog.bark(); //call own (overridden) method
In this case, the class Husky
is a specialized version of the class Dog
: it is a Dog
that has a few special abilities (e.g., it can throw a football). We refer to the base, less specialized class (Dog
) as the parent or super class, and the derived, more specialized class (Husky
) as the child or sub-class.
The sub-class Husky
class inherits the methods defined in its parent: even though the Husky
class didn’t define a sit()
method, it still has that method define because the parent has that method defined! By extending an existing class, you get can get a lot of methods for free!
The Husky
class is also able to override its parents methods, defining it’s own specialized version (e.g., bark()
). This is useful for adding customization, or for providing specific implementations of callbacks that may be utilized by a framework—a pattern that you’ll see in React.
Note that despite this discussion, JavaScript is not actually an object-oriented language. JavaScript instead uses a prototype system for defining types of Objects, which allows what is called prototypical inheritance. The ES6 class
keyword doesn’t change that: instead, it is simply a “shortcut syntax” for specifying Object prototypes in the same way that has been supported since the first version of JavaScript. The class
keyword makes it easy to define something that looks and acts like an OOP class, but JavaScript isn’t object-oriented! See this (detailed) explanation for further discussion.
Working with this
In JavaScript, functions are called on Objects by using dot notation (e.g., myObject.myFunction()
). Inside a function, you can refer to the Object that the function was called on by using the this
keyword. this
is a local variable that is implicitly assigned the Object as a value.
const doggy = {
name: "Fido",
bark: function() {
console.log(this.name, "woofs"); //`this` is object the function was called on
}
}
doggy.bark(); //=> "Fido woofs"
- Here the
this
is assigned the objectdoggy
, what.bark()
was called on.
But because functions are values and so can be assigned to multiple variables (given multiple labels), the object that a function is called on may not necessarily be the object that it was first assigned to as a property. this
refers to object the function is called on at execution time, not at the time of definition:
//An object representing a Dog
const doggy = {
name: "Fido",
bark: function() { console.log(this.name + " woofs"); }
}
// An object representing another Dog
const doggo = {
name: "Spot",
bark: function() { console.log(this.name + " yips")}
}
//This is Fido barking
doggy.bark( /*this = doggy*/ ); //=> "Fido woofs"
//This is Spot barking
doggo.bark( /*this = doggo*/ ); //=> "Spot yips"
//This is Fido using Spot's bark!
doggy.bark = doggo.bark; //assign the function value to `doggy`
doggy.bark( /*this = doggy*/) //=> "Fido yips"
- Notice how the
this
variable is implicitly assigned a value of whatever object it was called on—even the function is assigned to a new object later!
But because the this
variable refers to the object the function is called on, problems can arise for anonymous callback functions that are not called on any object in particular:
class Person {
constructor(name){ this.name = name; } //basic constructor
//greet each person in the given array
greetAll(peopleArray) {
//loop through each Person using a callback
peopleArray.forEach(function(person) {
console.log("Hi"+person.name+", I'm "+this.name);
});
}
}
In this example, the greetAll()
function will produce an error: TypeError: Cannot read property 'name' of undefined
. That is because the this
is being called from within an anonymous callback function (the function(person){...}
)—and that callback isn’t being called on any particular object (notice the lack of dot notation). Since the anonymous callback isn’t being executed on an object, this
is assigned a value of undefined
(and you can’t access undefined.name
).
The solution to this problem is to use arrow functions. An arrow function has the special feature that it shares the same lexical this
as its surrounded code: that is, the this
will not be reassigned to a (non-existent) object when used within an arrow function:
class Person {
constructor(name){ this.name = name; }
greetAll(peopleArray) {
peopleArray.forEach((person) => { //arrow function (subtle difference)
console.log("Hi"+person.name+", I'm "+this.name); //works correctly!
});
}
}
This property makes arrow functions invaluable when specifying callback functions, particularly once classes and objects are involved. Always use arrow functions for anonymous callbacks!
Alternatively, it is possible to “permanently” associate a particular this
value with a function, no matter what object that function is called on. This is called binding the this
, and is done by calling the .bind()
method on the function and passing in the value you want to be assigned to this
. The .bind()
method will return a new function that has the value bound to it; often you will then take this new function and “re-assign” it to the old function variable:
//re-assign function
myFunction = myFunction.bind(thisValue);
This is a common pattern in React (and has some minuscule performance benefits), but for this class you should stick with arrow functions for cleanliness and readability.
This chapter has described a few of the more common and potentially useful ES6 features. However, remember that most of these are just “syntactic shortcuts” for behaviors and functionality you can already achieve using ES5-style JavaScript. Thus you don’t need to use these features in your code—though they can be helpful, and they will often show up in how we use libraries such as React.