JavaScript ES6 Class Syntax
ECMAScript 2015 or more well known as ES6 is the next specification for JavaScript. ES6 brings exciting features to JavaScript including new syntax improvements. This post I am going to cover the new Class syntax. JavaScript has been a prototypal based language using object prototypes to create object inheritance and code reuse. The new ES6 Class adds a new syntax on top of traditional prototypes.
Something I cannot stress enough is the new Class is syntactic sugar on prototypes. Under the hood, ES6 Classes are still using prototypal inheritance. If you are unfamiliar with prototypes I would suggest you read my previous post on JavaScript Prototypal Inheritance.
Constructors
In ES5 or the current widely supported version of JavaScript, we use prototypes to create object inheritance. Before ES6, we used function constructors similar to this.
// ES5 Constructor Function
function Person(name)
this.name = name;
>
var bob = new Person('Bob');
console.log(bob.name); // Outputs 'Bob'
ES2015/ES6 has the new reserved keyword Class with a constructor statement. So the ES2015/ES6 equivalent of our Person function constructor would be the following.
// ES2015/ES6 Class
class Person
constructor(name)
this.name = name;
>
>
let bob = new Person('Bob');
console.log(bob.name); // Outputs 'Bob'
The new syntax gives us a dedicated constructor statement that runs on object
creation. Constructors are helpful for any object initialization logic.
Methods
Next, let’s look at adding a function to our Person. In ES5 we would have had
something like this.
// ES5 adding a method to the Person prototype
Person.prototype.walk = function()
console.log(this.name + ' is walking.');
>;
var bob = new Person('Bob');
bob.walk(); // Outputs 'Bob is walking.'
ES6 offers us a much more terse and clean syntax to achieve the same goal.
// ES6 Class adding a method to the Person prototype
class Person
constructor(name)
this.name = name;
>
walk()
console.log(this.name + ' is walking.');
>
>
let bob = new Person('Bob');
console.log(bob.name); // Outputs 'Bob is walking'
Get and Set
ES6 classes brings a new syntax for getters and setters on object properties Get and set allows us to run code on the reading or writing of a property. ES5 had getters and setters as well but was not widely used because of older IE browsers. ES5 getters and setters did not have as nice of a syntax that ES6 brings us. So let’s create a get and set for our name property.
// ES6 get and set
class Person
constructor(name)
this._name = name;
>
get name()
return this._name.toUpperCase();
>
set name(newName)
this._name = newName; // validation could be checked here such as only allowing non numerical values
>
walk()
console.log(this._name + ' is walking.');
>
>
let bob = new Person('Bob');
console.log(bob.name); // Outputs 'BOB'
In our class above we have a getter and setter for our name property. We use _ convention to create a backing field to store our name property. Without this every time get or set is called it would cause a stack overflow. The get would be called and which would cause the get to be called again over and over creating an infinite loop.
Something to note is that our backing field this._name is not private. Someone could still access bob._name and retrieve the property. To achieve private state on objects, you would use ES6 symbol and module to create true encapsulation and private state. Private methods can be created using module or traditional closures using an IIFE. Using languages like TypeScript you can get compile-time enforcement of private properties and methods.
Inheritance
Now let’s look into inheritance using traditional prototypes in ES5 syntax. We will create a Programmer object to inherit our Person object. Our programmer object will inherit person and also have a writeCode() method.
// ES5 Prototype inheritance
function Programmer(name, programmingLanguage)
this.name = name;
this.programmingLanguage = programmingLanguage;
>
Programmer.prototype = Object.create(Person.prototype);
Programmer.prototype.constructor = Programmer;
Programmer.prototype.writeCode = function()
console.log(this.name + ' is coding in ' + this.programmingLanguage + '.');
>;
var cory = new Programmer('Cory', 'JavaScript');
cory.walk(); // Outputs 'Cory is walking.'
cory.writeCode(); // Outputs 'Cory is coding in JavaScript.'
Now let’s look at the new ES6 Class syntax for inheritance using the extend keyword.
class Programmer extends Person
constructor(name, programmingLanguage)
super(name);
this.programmingLanguage = programmingLanguage;
>
writeCode()
console.log(
this._name + ' is coding in ' + this._programmingLanguage + '.'
);
>
>
let cory = new Programmer('Cory', 'JavaScript');
cory.walk(); // Outputs 'Cory is walking.'
cory.writeCode(); // Outputs 'Cory is coding in JavaScript.'
You can see the class syntax offers a clean syntax for prototypal inheritance. One detail you may notice is the super() keyword. The super keyword lets us call the parent object that is being inherited. It is good advice to avoid this as this can cause an even tighter coupling between your objects, but there are occasions where it is appropriate to use. In this case, it can be used in the constructor to assign to the super constructor. If the Person constructor contained any logic, custom getters or setters for the name property we would want to use the super and not duplicate the logic in the Programmer class. If a constructor is not defined on a child class the super class constructor will be invoked by default.
Overview
Here is one final look at our Person and Programmer classes. The getters and setters are not necessary in this use case but are there to demonstrate the new syntax.
class Person
constructor(name)
this._name = name;
>
get name()
return this._name;
>
set name(newName)
this._name = newName;
>
walk()
console.log(this._name + ' is walking.');
>
>
class Programmer extends Person
constructor(name, programmingLanguage)
super(name);
this._programmingLanguage = programmingLanguage;
>
get programmingLanguage()
return this._programmingLanguage;
>
set programmingLanguage(newprogrammingLanguage)
this._programmingLanguage = newprogrammingLanguage;
>
writeCode()
console.log(
this._name + ' is coding in ' + this._programmingLanguage + '.'
);
>
>
let bob = new Person('Bob');
bob.walk();
let cory = new Programmer('Cory', 'JavaScript');
cory.walk();
cory.writeCode();
console.log(cory.name);
A codepen.io demo of the code above can be found here. ES6 classes bring some syntactical sugar to prototypes.Just remember that is all ES6 classes are, syntactic sugar. Remember classes are just one of many options to organize and structure code. There are many other great design patterns for code reuse such as the module pattern.
ES6 brings some great improvements to making JavaScript a more productive programming language and is already being implemented in browsers today. To start writing ES6 today check out Babel JS (formerly 6to5) a transpiler that transpile ES6 JavaScript to ES5.
Web Component Essentials
Save development time, improve product consistency and ship everywhere. With this new Course and E-Book learn how to build UI components that work in any JavaScript framework such as Angular, Vue, React, and more!
Web Component Essentials
Reusable UI Components for all your Web Applications
Свойства — геттеры и сеттеры
Первый тип это свойства-данные (data properties). Мы уже знаем, как работать с ними. Все свойства, которые мы использовали до текущего момента, были свойствами-данными.
Второй тип свойств мы ещё не рассматривали. Это свойства-аксессоры (accessor properties). По своей сути это функции, которые используются для присвоения и получения значения, но во внешнем коде они выглядят как обычные свойства объекта.
Геттеры и сеттеры
Свойства-аксессоры представлены методами: «геттер» – для чтения и «сеттер» – для записи. При литеральном объявлении объекта они обозначаются get и set :
let obj = < get propName() < // геттер, срабатывает при чтении obj.propName >, set propName(value) < // сеттер, срабатывает при записи obj.propName = value >>;
Геттер срабатывает, когда obj.propName читается, сеттер – когда значение присваивается.
Например, у нас есть объект user со свойствами name и surname :
А теперь добавим свойство объекта fullName для полного имени, которое в нашем случае «John Smith» . Само собой, мы не хотим дублировать уже имеющуюся информацию, так что реализуем его при помощи аксессора:
let user = < name: "John", surname: "Smith", get fullName() < return `$$`; > >; alert(user.fullName); // John Smith
Снаружи свойство-аксессор выглядит как обычное свойство. В этом и заключается смысл свойств-аксессоров. Мы не вызываем user.fullName как функцию, а читаем как обычное свойство: геттер выполнит всю работу за кулисами.
На данный момент у fullName есть только геттер. Если мы попытаемся назначить user.fullName= , произойдёт ошибка:
let user = < get fullName() < return `. `; >>; user.fullName = "Тест"; // Ошибка (у свойства есть только геттер)
Давайте исправим это, добавив сеттер для user.fullName :
let user = < name: "John", surname: "Smith", get fullName() < return `$$`; >, set fullName(value) < [this.name, this.surname] = value.split(" "); >>; // set fullName запустится с данным значением user.fullName = "Alice Cooper"; alert(user.name); // Alice alert(user.surname); // Cooper
В итоге мы получили «виртуальное» свойство fullName . Его можно прочитать и изменить.
Дескрипторы свойств доступа
Дескрипторы свойств-аксессоров отличаются от «обычных» свойств-данных.
Свойства-аксессоры не имеют value и writable , но взамен предлагают функции get и set .
То есть, дескриптор аксессора может иметь:
- get – функция без аргументов, которая сработает при чтении свойства,
- set – функция, принимающая один аргумент, вызываемая при присвоении свойства,
- enumerable – то же самое, что и для свойств-данных,
- configurable – то же самое, что и для свойств-данных.
Например, для создания аксессора fullName при помощи defineProperty мы можем передать дескриптор с использованием get и set :
let user = < name: "John", surname: "Smith" >; Object.defineProperty(user, 'fullName', < get() < return `$$`; >, set(value) < [this.name, this.surname] = value.split(" "); >>); alert(user.fullName); // John Smith for(let key in user) alert(key); // name, surname
Ещё раз заметим, что свойство объекта может быть либо свойством-аксессором (с методами get/set ), либо свойством-данным (со значением value ).
При попытке указать и get , и value в одном дескрипторе будет ошибка:
// Error: Invalid property descriptor. Object.defineProperty(<>, 'prop', < get() < return 1 >, value: 2 >);
Умные геттеры/сеттеры
Геттеры/сеттеры можно использовать как обёртки над «реальными» значениями свойств, чтобы получить больше контроля над операциями с ними.
Например, если мы хотим запретить устанавливать короткое имя для user , мы можем использовать сеттер name для проверки, а само значение хранить в отдельном свойстве _name :
let user = < get name() < return this._name; >, set name(value) < if (value.length < 4) < alert("Имя слишком короткое, должно быть более 4 символов"); return; >this._name = value; > >; user.name = "Pete"; alert(user.name); // Pete user.name = ""; // Имя слишком короткое.
Таким образом, само имя хранится в _name , доступ к которому производится через геттер и сеттер.
Технически, внешний код всё ещё может получить доступ к имени напрямую с помощью user._name , но существует широко известное соглашение о том, что свойства, которые начинаются с символа «_» , являются внутренними, и к ним не следует обращаться из-за пределов объекта.
Использование для совместимости
У аксессоров есть интересная область применения – они позволяют в любой момент взять «обычное» свойство и изменить его поведение, поменяв на геттер и сеттер.
Например, представим, что мы начали реализовывать объект user , используя свойства-данные имя name и возраст age :
function User(name, age) < this.name = name; this.age = age; >let john = new User("John", 25); alert( john.age ); // 25
…Но рано или поздно всё может измениться. Взамен возраста age мы можем решить хранить дату рождения birthday , потому что так более точно и удобно:
function User(name, birthday) < this.name = name; this.birthday = birthday; >let john = new User("John", new Date(1992, 6, 1));
Что нам делать со старым кодом, который использует свойство age ?
Мы можем попытаться найти все такие места и изменить их, но это отнимает время и может быть невыполнимо, если код используется другими людьми. И кроме того, age – это отличное свойство для user , верно?
Добавление геттера для age решит проблему:
function User(name, birthday) < this.name = name; this.birthday = birthday; // возраст рассчитывается из текущей даты и дня рождения Object.defineProperty(this, "age", < get() < let todayYear = new Date().getFullYear(); return todayYear - this.birthday.getFullYear(); >>); > let john = new User("John", new Date(1992, 6, 1)); alert( john.birthday ); // доступен как день рождения alert( john.age ); // . так и возраст
Теперь старый код тоже работает, и у нас есть отличное дополнительное свойство!