JavaScript парадигма объектов и прототипов. Простое объяснение.
Многим новым разработчикам, особенно тем, кто привык работать с традиционным ООП, работать в мире JavaScript может показаться не удобно и не привычно. Для них код на JavaScript может выглядеть грязным и запутанным. Данная статья это попытка объяснить, максимально простым языком, что такое объекты в JavaScript и рассказать о механизме наследования на основе прототипов.
Хочу отметить, что я читал, множество статей, пытающихся объяснить что такое объекты в JavaScript, но я, никогда не читал те, которые объясняли бы их полностью или четко. Они часто сосредотачиваются исключительно на наследовании, оставляя без внимания другие важные моменты или не приводят достаточного количества примеров.
Что такое объекты?
Объекты на самом базовом уровне можно представить как список пар ключ / значение, причем ключ всегда является строкой, …а значение…. может быть чем-то иным. Это похоже на то, что вы можете назвать «картой» или «словарем» на других языках. Все, базовые сущности которые вы обычно создаете в JavaScript, и которое не является примитивами, является объектами. Объекты облегчают упаковку и перемещение данных, а создание новых объектов в JavaScript более тривиально, чем в других объектно-ориентированных, таких как Java / C #.
Когда говорят об объектах часто упоминают термин “свойство”. Этот термин означает определенную пару ключ/значение. Чтобы дать вам представление о том, как выглядят объекты, мы начнем с простого примера объекта с двумя свойствами: age и weight.
var Dog = { age: 8, weight: 65 }
Это фрагмент кода демонстрирует использования литеральной (инициирующей) нотации объекта. Объектный литерал не является переменной или возвращаемым значением.
Функции являются объектами
Как было замечено раньше, в Javascript все что не примитивы являются объектами, включая функции… Я знаю это может показаться странным. Сложно думать о функции как о группе пар ключ/значение. Так как функции объекты, их часто еще называют как объекты-фукнции. Это специальные группа пар ключ/значение с особыми свойствами для выполнения кода и передачи значений. Мы рассмотрим эти свойства в следующем разделе. В начале давайте поговорим почему функции так важны.
Можно сказать, что объекты-функции имеют две основные цели. Если мы хотим создать блок логики, который выполняется, мы можем использовать объект-функцию: точно так же, как «методы» в любом другом языке программирования. Другой целью является то, что если мы захотим создать объекты со значениями и методами, и возможно, с некоторой логикой для установки этих значений, мы также будем использовать объекты-функции. Здесь вы можете думать о объектах-функциях как о «классах», которые ведут себя как объектно-ориентированные языки (то есть Java / C #).
В JavaScript вы часто встречаете термин “метод” . Термин метод относится к объекту-функции, которая является свойством другого объекта.
В стандартном случае функции в JavaScript выглядят как функции на любом другом языке; они выполняют логику для выполнения конкретной задачи.
function bark() { console.log('woof woof') } bark() => 'woof woof'
Если мы хотим упаковать небольшую группу данных, как, например, два наших свойства в объекте Dog, то достаточно простой список пар ключ / значение. Но что если мы захотим создать несколько объектов Dog? Может быть, некоторые значения должны быть статическими, а другие динамическими. В данном случае нам понадобятся объекты-функции. Когда мы вызываем функцию с помощью new, объект (он же экземпляр-объект) вернется со свойствами установленными с помощью ключевого слова 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, которое является объектом-функцией. Все экземпляры-объекты наследуются от него. Да же когда мы литерально создаем объект в действительности вызывается new Object(). Свойство нового объекта __proto__ будет указывать на прототип родительского объекта (Object().__proto__) . Таким образом, все объекты, создание из литералов объектов, на самом деле являются объектами экземпляра Object. Это предоставляет нам множество полезных родительских методов. Например hasOwnProperty, которое может сообщить нам, существует ли определенное свойство у объекта. Если мы попытаемся получить доступ к свойству непосредственно у объекта-функции, JavaScript сначала будет смотреть на prototype, а затем перемещаться вверх по цепочке, используя __proto__ у родительского prototype.
Давайте рассмотрим несколько примеров JavaScript, движущихся вверх по цепочке прототипов для доступа к hasOwnProperty у некоторых объектов.
Литеральный объект (Object-literal):
var insect = {legs: 6} // insect.__proto__ === Object.prototype // insect.hasOwnProperty === Object.prototype.hasOwnProperty insect.hasOwnProperty('legs') // => true
Объект Экземпляр (Instance-Object):
var Bingo = new Dog() // Bingo.__proto__ === Dog.prototype // Dog.prototype.__proto__ === Object.prototype Bingo.hasOwnProperty('weight') // => true
Объект Функция (Function-object):
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 и возвращает неопределенное значение. Function.prototype определяет поведение по умолчанию, от которого наследуются все функции. Как и все свойства прототипов функциональных объектов, он по-прежнему имеет __proto__, который указывает на Object.prototype.
Dog.__proto__ === Function.prototype Object.__proto__ === Function.prototype Function.__proto__ === Function.prototype Function.prototype.__proto__ === Object.prototype
Выше сказанное можно продемонстрировать следующей картинкой. Обратите внимание, что Object.prototype — это то, откуда все происходит.
Многоуровневое наследование
Когда мы говорим о наследовании, мы обычно думаем об объектах экземпляра, возвращаемых из функций. С прототипом вы также можете создать несколько уровней наследования и иметь объекты-функции, наследуемые от других объектов-функций. Все, что вам нужно сделать, это установить прототип дочернего объекта-функции в другой экземпляр прототипа родительского объекта-функции. Тогда все свойства родителя будут скопированы. Если родительская функция получает аргументы, такие как age и weight у 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, являются просто синтаксическим сахаром над объектами-функциями. Вместо того, чтобы набирать прототип снова и снова, чтобы определить методы для функций, с ключевым словом class мы можем просто определить группу методов внутри класса. С помощью ключевого слова extends классы могут наследоваться от других классов без необходимости использовать Object.create и Object.call. Лично мне больше нравится использовать классы, но имейте в виду, что старые браузеры могут их не поддерживать. Для решения этой проблемы есть такие инструменты, как 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
Спасибо. Полезно.