От автора: в минувшие выходные я закончил работу над JavaScript: The Hard Parts Уилла Сентанса. Это может показаться не самым великолепным способом провести выходные, но я действительно развлекся и расслабился, заканчивая курс. Он коснулся функционального программирования, функций более высокого порядка, замыканий и асинхронного JavaScript.
Для меня основной темой курса было то, как он расширил подходы JavaScript к объектно-ориентированному программированию (ООП) и демистифицировал магию оператора new. Теперь у меня есть хорошо продуманное понимание того, что происходит под капотом, когда используется оператор new JavaScript.
Объектно-ориентированное программирование в JavaScript
Объектно-ориентированное программирование (ООП) — это парадигма программирования, основанная на концепции «объектов». Данные и функции (атрибуты и методы) объединены внутри объекта.
Объект в JavaScript представляет собой набор пар ключ-значение. Эти пары ключ-значение являются свойствами объекта. Свойством может быть массив, функция, сам объект или любой примитивный тип данных, такой как строки или целые числа.
Какие методы мы используем в нашем инструменте JavaScript для создания объекта?
Предположим, что мы создаем пользователей в игре, которую мы только что разработали. Как мы будем хранить данные пользователя, такие как их имена, очки и методы реализации, такие как увеличение очков? Вот два варианта создания базового объекта.
Вариант 1 – обозначение объектного литерала
1 2 3 4 5 6 7 |
let user1 = { name: "Taylor", points: 5, increment: function() { user1.points++; } }; |
Объектный литерал JavaScript — это список пар имя-значение, завернутый в фигурные скобки. В приведенном выше примере создается объект user1, и связанные с ним данные хранятся внутри него.
Вариант 2 — Object.create()
1 |
Object.create(proto, [ propertiesObject ]) |
Методы Object.create принимают 2 аргумента:
proto: объект, который должен быть прототипом вновь созданного объекта. Это должен быть object или null.
propertiesObject: свойства нового объекта. Необязательный аргумент.
По факту, вы передаете в Object.create объект, который вы хотите наследовать, и он возвращает новый объект, который наследуется от объекта, который вы ему передали.
1 2 3 4 5 6 |
let user2 = Object.create(null); user2.name = "Cam"; user2.points = 8; user2.increment = function() { user2.points++; } |
Варианты создания базового объекта выше повторяются. Для этого требуется, чтобы каждый из них был создан вручную.
Как это обойти?
Решения
Решение 1 – генерация объектов через функцию
Простое решение – написать функцию для создания новых пользователей.
1 2 3 4 5 6 7 8 9 |
function createUser(name, points) { let newUser = {}; newUser.name = name; newUser.points = points; newUser.increment = function() { newUser.points++; }; return newUser; } |
Чтобы создать пользователя, вы должны ввести информацию в параметры функции.
1 2 |
let user1 = createUser("Bob", 5); user1.increment(); |
Однако функция increment в приведенном выше примере является только копией исходной функции increment. Это не очень хороший способ написать код, так как любые потенциальные изменения в функции должны выполняться вручную для каждого объекта.
Решение 2 – использование прототипной природы JavaScript
В отличие от объектно-ориентированных языков, таких как Python и Java, JavaScript не имеет классов. Он использует концепцию прототипов и цепочек прототипов для наследования.
Когда вы создаете новый массив, вы автоматически получаете доступ к встроенным методам, таким как Array.join, Array.sort и Array.filter. Это связано с тем, что объекты массива наследуют свойства от Array.prototype.
Каждая функция JavaScript имеет свойство prototype, которое по умолчанию пусто. Вы можете добавить функции к этому свойству прототипа, и в этой форме он известен как метод. Когда унаследованная функция выполняется, значение этого указывает на наследующий объект.
1 2 3 4 5 6 7 8 9 10 11 12 |
function createUser(name, points) { let newUser = Object.create(userFunction); newUser.name = name; newUser.points = points; return newUser; } let userFunction = { increment: function() {this.points++}; login: function() {console.log("Please login.")}; } let user1 = createUser("Bob", 5); user1.increment(); |
Когда был создан объект user1, была создана цепочка прототипов с функцией userFunction.
Когда user1.increment () находится в стеке вызовов, интерпретатор будет искать user1 в глобальной памяти. Затем он будет искать функцию increment, но не найдет ее. Интерпретатор рассмотрит следующий объект в цепочке прототипов и найдет там функцию increment.
Решение 3 – new и this
Оператор new используется для создания экземпляра объекта, который имеет функцию-конструктор.
Когда мы вызываем функцию конструктора с помощью new, мы автоматизируем следующие действия:
создание нового объекта
привязку this к объекту
Объектом прототипа функции конструктора становится свойство __proto__ нового объекта
Возвращается объект из функции
Это фантастика, потому что автоматизация приводит к менее повторяемому коду!
1 2 3 4 5 6 7 8 9 10 11 12 |
function User(name, points) { this.name = name; this.points = points; } User.prototype.increment = function(){ this.points++; } User.prototype.login = function() { console.log(“Please login.”) } let user1 = new User(“Dylan”, 6); user1.increment(); |
Используя шаблон прототипа, каждый метод и свойство добавляются непосредственно на прототипе объекта.
Интерпретатор пробежит по прототипной цепочке и найдет функцию increment под свойством прототипа User, который сам по себе также является объектом с информацией внутри него. Помните — все функции в JavaScript также являются объектами. Теперь, когда интерпретатор нашел то, что ему нужно, он может создать новый локальный контекст выполнения для запуска user1.increment ().
Замечание: Разница между __proto__ и прототипом
Если вы уже запутались в __proto__ и прототипе, не волнуйтесь! Далеко не вы один запутались. Прототип — это свойство функции-конструктора, которое определяет, что станет свойством __proto__ на построенном объекте. Таким образом, __proto__ является созданной ссылкой, и эта ссылка называется прототипом цепной связи.
Решение 4 – синтаксический сахар ES6
Другие языки позволяют нам писать наши общие методы внутри самого объекта-конструктора. В ECMAScript6 введено ключевое слово class, которое позволяет нам писать классы, которые походят на обычные классы других классических языков. На самом деле это синтаксический сахар над прототипным поведением JavaScript.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class User { constructor(name, points) { this.name = name; this.points = points; } increment () { this.points++; } login () { console.log("Please login.") } } let user1 = new User("John", 12); user1.increment(); |
В решении 3 соответствующие методы были точно реализованы с использованием User.prototype.functionName. В этом решении те же результаты достигаются, но синтаксис выглядит более чистым.
Заключение
Теперь мы узнали больше о различных вариантах, которые мы имеем в JavaScript для создания объектов. Хотя объявления классов и оператор new относительно просты в использовании, важно понять, что автоматизировано.
Повторим, следующие действия автоматизируются при вызове функции конструктора с помощью new:
создание нового объекта
привязку this к объекту
Объектом прототипа функции конструктора становится свойство __proto__ нового объекта
Возвращается объект из функции
Автор: Cynthia Lee
Источник: //medium.freecodecamp.org/
Редакция: Команда webformyself.