Выявлены все запутанные моменты об объектах JavaScript, так что вы можете перестать их так сильно ненавидеть

Многим новым разработчикам мир JavaScript может показаться довольно утомительным, особенно тем, кто имеет традиционный объектно-ориентированный опыт. Если просто поискать в Google какой-нибудь код JavaScript и объяснения, можно выявить довольно запутанный код и запутанную терминологию. Чтобы упростить задачу, в этой статье будет представлен обзор простого наследования на основе прототипов, чтобы у вас была хорошая основа для программирования с помощью JavaScript.

Просто хочу отметить, что я знаю, что статьи, пытающиеся объяснить объекты JavaScript, были написаны до смерти, но я чувствовал, что никогда не читал статей, которые объясняли бы их полностью или ясно. Они часто сосредотачиваются исключительно на наследовании, упуская из виду другие важные моменты или не приводя достаточного количества примеров. Цель здесь - дать вам краткий обзор объектов JavaScript и наследования прототипов самым простым из возможных способов.

Что такое объекты?

Объекты на самом базовом уровне можно представить себе как список пар ключ / значение, где ключ всегда является строкой, а значение -… ну, чем угодно. Это похоже на то, что вы могли бы назвать «картой» на других языках. Все, что вы вводите в JavaScript, но не является примитивом (см. Последний раздел для получения дополнительной информации о примитивах), является объектом. Объекты упрощают упаковку и перемещение данных, а создание новых более тривиально, чем в других объектно-ориентированных языках, таких как Java / C #.

Говоря об объектах, вы часто можете услышать термин «свойство». Это относится к определенной паре ключ / значение на объекте. Чтобы дать вам представление о том, как выглядят объекты, мы начнем с простого примера объекта с двумя свойствами: age и weight.

var Dog = {
    age: 8,
    weight: 65
}

Примечание. Наш фрагмент кода только что был примером использования литерала объекта. Объектный литерал строго относится к коду внутри фигурных скобок, включая их. Литерал объекта не является переменной или возвращаемым значением.

Функции - это объекты

Как уже упоминалось, все, что не является примитивом, является объектом, который также включает в себя функции ... Я знаю, странно, правда? Трудно представить себе функции как группу пар ключ / значение. Поскольку функции являются объектами, их часто называют объектами-функциями. Функция-объект - это специальная группа пар ключ / значение с определенными свойствами для выполнения кода и передачи значений. В следующем разделе мы рассмотрим, что это за свойства. Сначала давайте поговорим о том, почему функции важны.

Можно сказать, что у функциональных объектов две основные цели. Если мы хотим создать кусок логики, которая выполняется, мы можем использовать объект-функцию: точно так же, как «методы» в любом другом языке программирования. Следующая цель - это когда JavaScript становится немного напуганным. Если мы хотим создавать объекты и со значениями, и с методами, и, возможно, с некоторой логикой для установки этих значений, мы также будем использовать объекты-функции. Здесь вы можете думать о функциях-объектах как о «классах» в объектно-ориентированных языках (например, Java / C #).

Примечание. Вы услышите термин «метод», который иногда используется в JavaScript. В JavaScript метод относится к объекту-функции, который существует как свойство содержащего объекта.

В стандартном случае функции в JavaScript выглядят как функции на любом другом языке; они выполняют логику фрагментов для выполнения конкретной задачи. В следующем фрагменте bark - это код, выполняющий объект-функцию.

function bark() {
    console.log('woof woof')
}
bark() => 'woof woof'

Если мы хотели упаковать небольшую группу данных, например, наши 2 свойства в объекте Dog из предыдущего, тогда простой список пар ключ / значение будет работать нормально. Что, если мы хотим создать несколько объектов Dog? Возможно, одни значения должны быть статическими, а другие - динамическими. Вот здесь и пригодятся функциональные объекты. Когда мы вызываем функцию с new, возвращается объект (также известный как instance-object) со свойствами, установленными с помощью ключевого слова this внутри функции.

function Dog(age, weight) {
    this.species = 'Canis Familiaris'
    this.age = age
    this.weight = weight
    this.bark = bark <-- bark() from prev snippet
}
// Spot and Bingo are 'instance-objects' of Dog
var Spot = new Dog(8, 65)
var Bingo = new Dog(10, 70)
Spot.species => 'Canis Familiaris'
// bark is a 'method' of Dog
Bingo.bark() => 'woof woof'

Объекты против прототипов

Теперь, когда у вас есть хорошее представление об объектах и, что более важно, об объектах-функциях, давайте поговорим о прототипах. Вы часто слышали, что JavaScript - это язык, основанный на прототипах. Значит ли это, что объекты и прототипы - это одно и то же? Не совсем так. Прототипы - это особый тип объекта, который существует как свойство для объектов-функций. Когда мы пытаемся получить доступ к ключу объекта-функции, JavaScript проверяет его свойство prototype, чтобы узнать, есть ли оно там. В противном случае он пойдет вверх по цепочке прототипов, чтобы попытаться найти его. Чтобы понять цепочку прототипов, нам нужно узнать о функциях и наследовании.

Функции и наследование

Каждый раз, когда объект-экземпляр возвращается из вызова функции с использованием new, ему присваивается свойство с ключом __proto__. Значением этого свойства является свойство prototype функции, которая его создала.

Bingo.__proto__ === Dog.prototype
Spot.__proto__ === Dog.prototype

Если мы попытаемся получить доступ к свойству объекта-экземпляра, а его там нет, JavaScript сначала перейдет к __proto__ и проверит, есть ли оно в прототипе родительской функции. Чтобы увидеть это в действии, давайте установим свойство для ключа prototype для нашего объекта Dog, и когда мы вызовем Spot['whatever the key name is'] или Bingo['whatever the key name is'], мы получим то же значение. Это будет работать даже после того, как будут созданы оба экземпляра-объекта dog.

Dog.prototype.bark = function() {
    console.log('woof woof')
}
Spot.bark() // => 'woof woof'
Bingo.bark() // => 'woof woof'

Установка методов таким образом (в отличие от использования this внутри функций) особенно полезна, потому что реализация метода будет выполняться только один раз, а не каждый раз, когда вызывается new. Это сэкономит память и увеличит производительность.

Давайте теперь углубимся в вопросы наследования!

В основе всего наследования JavaScript лежит ключевое слово Object, которое является объектом-функцией. Все экземпляры-объекты наследуются от него. Когда мы создаем объектный литерал, скрытый JavaScript фактически вызывает new Object(). __proto__ нового объекта будет указывать на прототип Object. Таким образом, все объекты, созданные из объектных литералов, на самом деле являются объектами экземпляра Object. Это предоставляет нам множество полезных методов, таких как hasOwnProperty, которые могут сказать нам, существует ли свойство для объекта. Если мы попытаемся получить доступ к свойству непосредственно в объекте-функции, JavaScript сначала посмотрит на prototype, а затем продвинется вверх по цепочке, используя __proto__ на прототипе.

Давайте рассмотрим несколько примеров того, как JavaScript перемещается вверх по цепочке прототипов для доступа к hasOwnProperty некоторым объектам.

Объект-литерал:

var insect = {legs: 6}
// insect.__proto__ === Object.prototype
// insect.hasOwnProperty === Object.prototype.hasOwnProperty
insect.hasOwnProperty('legs') // => true

Экземпляр-объект:

var Bingo = new Dog()
// Bingo.__proto__ === Dog.prototype
// Dog.prototype.__proto__ === Object.prototype
Bingo.hasOwnProperty('weight') // => true

Непосредственно на Функциональном объекте:

function Foo() {
    this.something = 'blah'
}
// Foo.prototype.__proto__ === Object.prototype
Foo.hasOwnProperty('name') // => true
Foo.hasOwnProperty('something') // => false, set on instance-object not on the function

А как насчет __proto__ объектов-функций?

Как объяснялось, __proto__ помогает связывать объекты с прототипами, от которых они наследуются. А как насчет вызова __proto__ непосредственно на объектах-функциях? На самом деле в JavaScript есть встроенный объект-функция с именем Function. Свойство __proto__ каждой функции указывает на Function.prototype, который является функцией, но не имеет свойства prototype и возвращает undefined. Function.prototype определяет поведение по умолчанию, от которого наследуются все функции. Как и все prototype свойства объектов-функций, он по-прежнему имеет__proto__, что указывает на Object.prototype.

Dog.__proto__ === Function.prototype       
Object.__proto__ === Function.prototype    
Function.__proto__ === Function.prototype  
Function.prototype.__proto__ === Object.prototype

Уф, это было много ...

Все это немного сбивало с толку, правда? Может быть, этот рисунок поможет упростить задачу. Обратите внимание, что Object.prototype - вот откуда все происходит.

Многоуровневое наследование

Когда мы говорим о наследовании, мы обычно думаем об объектах-экземплярах, возвращаемых функциями. С prototype вы также можете выполнять несколько уровней наследования и наследовать объекты-функции от других объектов-функций. Все, что вам нужно сделать, это установить прототип дочернего объекта-функции на другой экземпляр прототипа родительского объекта-функции. Тогда все свойства родителя будут скопированы. Если родительская функция получает аргументы, такие как возраст и вес в Dog, используйте .call, чтобы установить свойство this для дочернего объекта.

Labrador наследуется от Dog:

function Labrador(furColor, age, weight) {
    this.furColor = furColor
    this.breed = 'labrador'
    Dog.call(this, age, weight)
}
Labrador.prototype = Object.create(Dog.prototype)
var Fido = new Labrador('white', 4, 41)
Fido.bark()

Классы

Классы в JavaScript, которые были созданы для ES6, являются просто синтаксическим сахаром поверх объектов-функций. Вместо того, чтобы набирать prototype снова и снова для определения методов функций, с помощью ключевого слова class мы можем просто определить группу методов внутри класса. С ключевым словом extends классы могут наследовать от других классов без необходимости делать Object.create и Object.call. Мне больше нравится использовать классы, но имейте в виду, что не все браузеры могут их поддерживать (ES6). Вот почему были созданы такие инструменты, как Babel.

Использование функциональных объектов:

function Dog(age, weight) {
    this.age = age
    this.weight = weight
}
Dog.prototype.bark = function() {console.log('woof woof')}

function Labrador(furColor, age, weight) {
    this.furColor = furColor
    this.breed = 'labrador'
    Dog.call(this, age, weight)
}
Labrador.prototype = Object.create(Dog.prototype)

Эквивалентно с использованием классов:

class Dog {
    constructor(age, weight) {
        this.age = age
        this.weight = weight
    }
    bark() {
        console.log('woof woof')
    }
}
class Labrador extends Dog {
    constructor(furColor, age, weight) {
        super(age, weight)
        this.furColor = furColor
        this.breed = 'labrador'
    }
}

Объекты против примитивов

Код JavaScript по сути сводится к двум основным типам: примитивы и объекты. В JavaScript есть 5 примитивов: boolean, number, string, null и undefined. Примитивы - это просто простые значения без каких-либо свойств. У трех примитивов: boolean, number и string есть аналоги объектов, которые JavaScript будет принудительно использовать во время определенных операций. Например, "some string".length вызовет new String() под капотом и обернет объект-экземпляр, возвращаемый строковым примитивом, чтобы можно было получить доступ к свойству length. Как уже упоминалось, все объекты-экземпляры наследуются от Object. Таким образом, со строкой вы все равно можете использовать такие методы, как hasOwnProperty.

// String.prototype.__proto__ === Object.prototype
String.hasOwnProperty('length')  // => true

Заключение

Да, да, я знаю, что это была головная боль; это казалось бесконечным шквалом prototype этого и __proto__ того. Лично я считаю, что наследование в JavaScript намного сложнее, чем должно быть, поэтому я использую TypeScript. Тем не менее, если вы работаете над тем, чтобы стать надежным фронтенд-разработчиком, будет полезно знать и заставить вас понять, почему существуют супернаборы JavaScript.

Пожалуйста, нажмите кнопку хлопка, если вы нашли эту статью полезной. Удачного веб-поиска!