Enterra
Компания
35,28
рейтинг
5 октября 2012 в 07:03

Разработка → Понимание ООП в JavaScript [Часть 1] перевод tutorial

— Прототипное наследование — это прекрасно
JavaScript — это объектно-ориентированный (ОО) язык, уходящий корнями в язык Self, несмотря на то, что внешне он выглядит как Java. Это обстоятельство делает язык действительно мощным благодаря некоторым приятным особенностям.

Одна из таких особенностей — это реализация прототипного наследования. Этот простой концепт является гибким и мощным. Он позволяет сделать наследование и поведение сущностями первого класса, также как и функции являются объектами первого класса в функциональных языках (включая JavaScript).

К счастью, в ECMAScript 5 появилось множество вещей, которые позволили поставить язык на правильный путь (некоторые из них раскрыты в этой статье). Также будет рассказано о недостатках дизайна JavaScript и будет произведено небольшое сравнение с классической моделью прототипного ОО (включая его достоинства и недостатки).

Статья предполагает, что вы уже знакомы с основами JavaScript, имеете представление о функциях (включая концепты замыкания и функций первого класса), примитивных значениях, операторах и т.д.

1. Объекты


Объект в JavaScript — это просто коллекция пар ключ-значение (и иногда немного внутренней магии).

Однако, в JavaScript нет концепции класса. К примеру, объект с свойствами {name: Linda, age: 21} не является экземпляром какого-либо класса или класса Object. И Object, и Linda являются экземплярами самих себя. Они определяются непосредственно собственным поведением. Тут нет слоя мета-данных (т.е. классов), которые говорили бы этим объектам как нужно себя вести.

Вы можете спросить: «Да как так?», особенно если вы пришли из мира классических объектно-ориентированных языков (таких как Java или C#). «Но если каждый объект обладает собственным поведением (вместо того чтобы наследовать его от общего класса), то если у меня 100 объектов, то им соответствует 100 разных методов? Разве это не опасно? А как мне узнать, что, например, объект действительно является Array-ем?»

Чтобы ответить на все эти вопросы необходимо забыть о классическом ОО-подходе и начать всё с нуля. Поверьте, оно того стоит.

Модель прототипного ОО приносит несколько новых динамичных и экспрессивынх путей решения старых проблем. В ней также представлены мощные модели расширения и повторного использования кода (а это и интересует людей, которые говорят об объектно-ориентированном программировании). Однако, эта модель даёт меньше гарантий. Например, нельзя полагаться, что объект x всегда будет иметь один и тот же набор свойств.

1.1. А что такое объекты?

Ранее упоминалось, что объекты — это просто пары уникальных ключей с соответствующими значениями — такие пары называются свойства. К примеру, вы хотите описать несколько аспектов своего старого друга (назовём его Мишей, он же Mikhail), таких как возраст, имя и пол:

Объект в JavaScript создаётся с помощью функции Object.create. Эта функция из родителя и опционального набора свойств создаёт новую сущность. Пока что мы не будем беспокоиться о параметрах.

Пустой объект — это объект без родителя, без свойств. Посмотрим на синтакс создания такого объекта в JavaScript:
var mikhail = Object.create(null)


1.2. Создание свойств

Так, значит у нас уже есть объект, но у него пока нет свойств — мы должны исправить эту ситуацию для описания нашего объекта Mikhail.

Свойства в JavaScript являются динамическими. Это означает, что мы их можем создавать или удалять в любое время. Свойства уникальны в том смысле, что ключ свойства внутри объекта соответствует ровно одному значению.

Создадим новые свойства через функцию Object.defineProperty, которая в качестве аргументов использует объект, имя свойства для создания и дескриптор, описывающий семантику свойства.
Object.defineProperty(mikhail, 'name', { value:        'Mikhail'
                                       , writable:     true
                                       , configurable: true
                                       , enumerable:   true })

Object.defineProperty(mikhail, 'age', { value:        19
                                      , writable:     true
                                      , configurable: true
                                      , enumerable:   true })

Object.defineProperty(mikhail, 'gender', { value:        'Male'
                                         , writable:     true
                                         , configurable: true
                                         , enumerable:   true })

Функция Object.defineProperty создаёт новое свойство, если свойство с данным ключём ранее не существовало (в противном случае произойдёт обновление семантики и значения существующего свойства).

Кстати, вы также можете использовать Object.defineProperties когда необходимо добавить больше одного свойства в объект:
Object.defineProperties(mikhail, { name:   { value:        'Mikhail'
                                           , writable:     true
                                           , configurable: true
                                           , enumerable:   true }

                                 , age:    { value:        19
                                           , writable:     true
                                           , configurable: true
                                           , enumerable:   true }

                                 , gender: { value:        'Male'
                                           , writable:     true
                                           , configurable: true
                                           , enumerable:   true }})

Очевидно, что оба вызова аналогичны, они вполне конфигурируемы, но не предназначены для конечного пользователя кода. Лучше создать уровень абстракции над ними.

1.3. Дескрипторы

Маленькие объекты, которые содержат в себе семантику, называются дескрипторами (мы их использовали при вызове Object.defineProperty). Дескрипторы бывают одного из двух типов — дескрипторы данных и дескрипторы доступа.

Оба типа дескрипторов содержат флаги, которые определяют как свойство будет рассматриваться языком. Если флаг не установлен, то его значение по умолчанию false (к сожалению это не всегда хорошее значение по умолчанию, что влечёт возрастание объёма описания дескрипторов).

Рассмотрим некоторые флаги:
  • writable — значение свойства может быть изменено, используется только для дескрипторов данных.
  • configurable — тип свойства может быть изменён или свойство может быть удалено.
  • enumerable — свойство используется в общем перечислении.
    Дескрипторы данных таковы, что определяют конкретное значение, которое соответствует дополнительному value-параметру, описывающему конкретные данные, привязанные к свойству:
  • value — значение свойства


Дескрипторы доступа определяют доступ к конкретному значению через getter-ы и setter-ы функций. Если не установлены, то по умолчанию равны undefined.
  • get() — функция вызывается без аргументов, когда происходит запрос к значению свойства.
  • set(new_value) — функция вызывается с аргументом — новым значением для свойства, когда пользователь пытается

модифицировать значение свойства.

1.4. Стремимся к лаконичности

К счастью, дескрипторы свойств — это не единственный путь работать со свойствами в JavaScript — их можно создавать более лаконично.

JavaScript также понимает ссылки на свойства, используя так называемую скобочную запись. Основное правило записывается следующим образом:
<bracket-access> ::= <identifier> "[" <expression> "]"

Тут identifier — это переменная, которая хранит объект, содержащий свойство, значение которого мы хотим установить, а expression — любое валидное JavaScript-выражение, определяющее имя свойства. Нет ограничений на то, какое имя может иметь свойство, всё позволяется.

Таким образом, мы можем переписать предыдущий пример:
mikhail['name']   = 'Mikhail'
mikhail['age']    = 19
mikhail['gender'] = 'Male'

На заметку: все имена свойств в конечном счёте конвертируются в строку, т.е. записи object[1], object[[1]], object['1'] и object[variable] (где значение variable равно 1) эквивалентны.

Существует другой способ обращения к свойству, который называется точечной записью. Он выглядит проще и лаконичнее, чем скобочная альтернатива. Однако, при этом способе имя свойство должно соответствовать правилам валидного JavaScript-идентификатора и не может быть представлено выражением (т.е. нельзя использовать переменные).

Общее правило для точечной записи:
<dot-access> ::= <identifier> "." <identifier-name>

Таким образом, предыдущий пример стал ещё более красивым:
mikhail.name   = 'Mikhail'
mikhail.age    = 19
mikhail.gender = 'Male'

Оба варианта синтаксиса выполняют эквивалентный процесс создания свойств, с выставлением семантических флагов в значение true.

1.5. Доступ к свойствам

Очень просто получить значение, хранящиеся в заданном свойстве — синтаксис очень похож на создание свойства с той лишь разницей, что в нём нет присваивания.
Например, если мы хотим узнать возраст Миши, то мы напишем:
mikhail['age']
// => 19

Но если мы попробуем получить значение свойства, которого не существует в нашем объекте, то мы получим undefined:
mikhail['address']
// => undefined


1.6. Удаление свойств

Для удаления свойства из объекта в JavaSCript предусмотрен оператор delete. К примеру, если вы хотите удалить свойство gender из нашего объекта mikhail:
delete mikhail['gender']
// => true

mikhail['gender']
// => undefined

Оператор delete вернёт true, если свойство удалено, и falseв противном случае. Не будем углубляться в то, как работает этот оператор. Но если вам всё-таки интересно, то вы можете почитать самую прекрасную статью о том как работает delete.

1.6. Getter-ы и setter-ы

Getter-ы и setter-ы обычно используются в классических объектно-ориентированных языках для обеспечения инкапсуляции. Они не особо нужны в JavaScript, но, у нас динамический язык, и я против этой функциональности.

Но, с любой точки зрения, они позволяет обеспечить proxy для запросов на чтение и запись свойств. Например, у нас были отдельные слоты для имени и фамилии, но мы хотим иметь удобный способ читать и устанавливать их.

Для начала, создадим имя и фамилию нашего друга, описав соответствующие свойства:
Object.defineProperty(mikhail, 'first_name', { value:    'Mikhail'
                                             , writable: true })

Object.defineProperty(mikhail, 'last_name', { value:    'Weiß'
                                            , writable: true })

Затем мы опишем общий способ получения и установки сразу двух свойств за один раз — назовём их объединение name:
// () → String
// Returns the full name of object.
function get_full_name() {
    return this.first_name + ' ' + this.last_name
}

// (new_name:String) → undefined
// Sets the name components of the object, from a full name.
function set_full_name(new_name) { var names
    names = new_name.trim().split(/\s+/)
    this.first_name = names[⁣'0'] || ''
    this.last_name  = names['1'] || ''
}

Object.defineProperty(mikhail, 'name', { get: get_full_name
                                       , set: set_full_name
                                       , configurable: true
                                       , enumerable:   true })

Теперь, каждый раз когда мы попытаемся узнать значение свойства name нашего друга на самом деле вызовется функция get_full_name:
mikhail.name
// => 'Mikhail Weiß'

mikhail.first_name
// => 'Mikhail'

mikhail.last_name
// => 'Weiß'

mikhail.last_name = 'White'
mikhail.name
// => 'Mikhail White'

Мы также можем установить name объекта, обратившись к соответствующему свойству, но на самом деле вызов set_full_name выполнит всю грязную работу:
mikhail.name = 'Michael White'

mikhail.name
// => 'Michael White'

mikhail.first_name
// => 'Michael'

mikhail.last_name
// => 'White'

Есть сценарии, в которых действительно удобно так делать, но стоит помнить, что такой механизм работает очень медленно.
Кроме того, следует учитывать что getter-ы и setter-ы обычно используются в других языках для инкапсуляции, а в ECMAScript 5 вы всё ещё не можете так делать — все свойства объекта являются публичными.

1.8. Перечисление свойств

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

Первый способ заключается в вызове функции Object.getOwnPropertyNames, которая вернёт вам Array, содержащий имена всех свойств, установленных для данного объекта — мы будет называть эти свойства собственными. Например, посмотрим, что мы знаем о Мише:
Object.getOwnPropertyNames(mikhail)
// => [ 'name', 'age', 'gender', 'first_name', 'last_name' ]

Второй способ заключается в использовании Object.keys, который вернёт список собственных свойств, которые помечены флагом enumerable :
Object.keys(mikhail)
// => [ 'name', 'age', 'gender' ]


1.9. Литералы

Простой способ создать объект заключается в использовании литерального синтаксиса JavaScript. Литеральный объект определяет новый объект, родитель которого Object.prototype (о родителях поговорим немного позже).

В любом случае, синтаксис литеральных объектов позволяет определять простые объекты и инициализировать их свойства. Перепишем пример создания объекта Mikhail:
var mikhail = { first_name: 'Mikhail'
              , last_name:  'Weiß'
              , age:        19
              , gender:     'Male'

              // () → String
              // Returns the full name of object.
              , get name() {
                    return this.first_name + ' ' + this.last_name }

              // (new_name:String) → undefined
              // Sets the name components of the object,
              // from a full name.
              , set name(new_name) { var names
                    names = new_name.trim().split(/\s+/)
                    this.first_name = names['0'] || ''
                    this.last_name  = names['1'] || '' }
              }


Невалидные имена свойств могут быть заключены в кавычки. Учитывайте, что запись для getter/setter в литеральном виде определяется анонимными функциями. Если вы хотите связать ранее объявленную функцию с getter/setter, то вы должны использовать метод Object.defineProperty.

Посмотрим на общее правила литерального синтаксиса:
<object-literal>  ::= "{" <property-list> "}"
                    ;
<property-list>   ::= <property> ["," <property>]*
                    ;
<property>        ::= <data-property>
                    | <getter-property>
                    | <setter-property>
                    ;
<data-property>   ::= <property-name> ":" <expression>
                    ;
<getter-property> ::= "get" <identifier>
                    :       <function-parameters>
                    :       <function-block>
                    ;
<setter-property> ::= "set" <identifier>
                    :       <function-parameters>
                    :       <function-block>
                    ;
<property-name>   ::= <identifier>
                    | <quoted-identifier>
                    ;

Литеральные объекты могут появляться внутри выражений в JavaScript. Из-за некоторой неоднозначности новички иногда путаются:
// This is a block statement, with a label:
{ foo: 'bar' }
// => 'bar'

// This is a syntax error (labels can't be quoted):
{ "foo": 'bar' }
// => SyntaxError: Invalid label

// This is an object literal (note the parenthesis to force
// parsing the contents as an expression):
({ "foo": 'bar' })
// => { foo: 'bar' }

// Where the parser is already expecting expressions,
// object literals don't need to be forced. E.g.:
var x = { foo: 'bar' }
fn({foo: 'bar'})
return { foo: 'bar' }
1, { foo:


2. Методы


До сих пор объект Mikhail имел только слоты для хранения данных (ну, за исключением getter/setter для свойства name). Описание действий, которые можно делать с объектом делается в JavaScript очень просто. Просто — потому что в JavaScript нет разницы между манипулированием такими вещами, как Function, Number, Object. Всё делается одинаково (не забываем, что функции в JavaScript являются сущностями первого класса).

Опишем действие над данным объектом, просто установив функцию, как значение нашего свойства. К примеру, мы хотим, чтобы Миша мог приветствовать других людей:
// (person:String) → String
// Greets a random person
mikhail.greet = function(person) {
    return this.name + ': Why, hello there, ' + person + '.'
}

После выставления значения свойства, мы можем использовать аналогичный способ для выставления конкретных данных, связанных с объектом. Таким образом, доступ к свойствам будет возвращать ссылку на функцию, хранящуюся в нём, которую мы можем вызвать:
mikhail.greet('you')
// => 'Michael White: Why, hello there, you.'

mikhail.greet('Kristin')
// => 'Michael White: Why, hello there, Kristin.'


2.1. Динамический this

Следует учитывать одну вещь при описании функции greet — эта функция должна обращаться к getter/setter свойства name, а для этого она использует магическую переменную this.

Она хранит в себе ссылку на объект, которому принадлежит исполняющаяся функция. Это не обязательно означает, что this всегда равно объекту, в котором функция хранится. Нет, JavaScript не настолько эгоистичен.

Функции являются generic-ами. Т.е. в JavaScript переменная this определяет динамическую ссылку, которая разрешается в момент исполнения функции.

Процесс динамического разрешения this обеспечивает невероятно мощный механизм для динамизации объектной ориентированности JavaScript и компенсирует отсутствие строгого соответствия заданным структурам (т.е. классам). Это означает, что можно применить функцию к любому объекту, который отвечает требованиям запуска, независимо от того, как устроен объект (как и в CLOS).

2.2. Разрешение this

Существует четыре различных способа разрешения this в функции, зависящие от того, как функция вызывается: непосредственно, как метод, явно применяется, как конструктор. Мы посмотрим первые три, а к конструкторам вернёмся позже.

Для следующих примеров вы примем:
// Returns the sum of the object's value with the given Number
function add(other, yet_another) {
    return this.value + other + (yet_another || 0)
}

var one = { value: 1, add: add }
var two = { value: 2, add: add }


2.2.1 Вызов как метод

Если функция вызывается, как метод объекта, то this внутри функции ссылается на сам объект. Т.е. когда мы явно указываем какой объект выполняет действие, то объект и будет значением this в нашей функции.

Это произойдёт, когда мы вызовем mikhail.greet(). Эта запись говорит JavaScript-у, что мы хотим применить действие greet к объекту mikhail.
one.add(two.value) // this === one
// => 3

two.add(3)         // this === two
// => 5

one['add'](two.value) // brackets are cool too
// => 3


2.2.2 Непосредственный вызов

Когда функция вызывается непосредственно, то this разрешается в глобальный объект движка (window в браузере, global в Node.js)
add(two.value)  // this === global
// => NaN

// The global object still has no `value' property, let's fix that.
value = 2
add(two.value)  // this === global
// => 4


2.2.3. Явное применение

В заключении, функция может быть явно применена к любому объекту, несмотря на то, есть ли в объекте соответствующее свойство или нет. Эта функциональность достигается с помощью методов call или apply.

Различие между двумя методами заключается в параметрах передаваемых в функцию и времени исполнения — apply работает примерно в 55 раз медленнее, чем непосредственный вызов, а вот call обычно не особо хуже. Всё очень зависит от текущего движка, так что используйте Perf test, чтобы быть уверенными — не оптимизируйте код раньше времени.

В любом случае, call ожидает объект, как первый параметр функции, за которым следуют обычные аргументы исходной функции:
add.call(two, 2, 2)      // this === two
// => 6

add.call(window, 4)      // this === global
// => 6

add.call(one, one.value) // this === one
// => 2

С другой стороны, apply позволяет описывать вторым параметром массив параметров исходной функции:
add.apply(two, [2, 2])       // equivalent to two.add(2, 2)
// => 6

add.apply(window, [ 4 ])       // equivalent to add(4)
// => 6

add.apply(one, [one.value])  // equivalent to one.add(one.value)
// => 2

На заметку. Учтите, что разрешение this в null или undefined зависит от семантики используемого движка. Обычно результат бывает таким же, как и применение функции к глобальному объекту. Но если движок работает в strict mode, то this будет разрешено как и ожидается — ровно в ту вещь, к которой применяется:
window.value = 2
add.call(undefined, 1) // this === window
// => 3

void function() {
  "use strict"
  add.call(undefined, 1) // this === undefined
  // => NaN
  // Since primitives can't hold properties.
}()


2.3. Связывание методов

Отвлечёмся от динамической сущности функций в JavaScript, пойдём по пути создания функций, связывая их с определёнными объектами, так чтобы this внутри функции всегда указывал на данный объект, несмотря на то, как он вызывается — как метод объекта или непосредственно.

Функция обеспечивает функциональность, называемую bind: берётся объект и дополнительный параметр (очень похоже на вызов call) и возвращается новая функция, которая будет применять параметры к исходной функции при вызове:
var one_add = add.bind(one)

one_add(2) // this === one
// => 3

two.one_adder = one_add
two.one_adder(2) // this === one
// => 3

one_add.call(two) // this === one
// => 3


3. Наследование


До сих пор мы видели, как объекты могут определять своё поведение и как мы можем использовать их действия на других объектах, но, мы до сих пор не увидели нормального пути для повторного использования кода и его расширяемости.

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

Модель прототипирования идёт дальше. Хоть она и поддерживает такие технологии, как «selective extensibility» и «behaviour sharing», но мы их не будем особо изучать. Печальная вещь: конкретные модели прототипного ОО, реализованные в JavaScript несколько ограниченны. Мы можем обойти эти ограничения, но накладные расходы будут велики.

3.1. Прототипы

Наследование в JavaScript осуществляется через клонирование поведения объекта и расширение его специализированным поведением. Объект, поведение которого клонируют, называется прототипом.

Прототип — это обычный объект, который делится своим поведением с другими объектами — в этом случае он выступает в качестве родителя.

Концепт клонирования поведения не означает, что вы будете иметь две различные копии одной и той же функции или данных. На самом деле JavaScript реализует наследование через делегирование, т.е. все свойства хранятся в родителе, а доступ к ним расширен через ребёнка.

Как упоминалось ранее, родитель (или [[Prototype]]) объекта определяется вызовом Object.create с первым аргументом, ссылающимся на объект-родитель.

Вернёмся к примеру с Мишей. Выделим его имя и способность приветствовать людей в отдельный объект, который поделится с Мишей своим поведением. Вот как будет выглядеть наша модель:

Реализуем её на JavaScript:
var person = Object.create(null)

// Here we are reusing the previous getter/setter functions
Object.defineProperty(person, 'name', { get: get_full_name
                                      , set: set_full_name
                                      , configurable: true
                                      , enumerable:   true })

// And adding the `greet' function
person.greet = function (person) {
    return this.name + ': Why, hello there, ' + person + '.'
}

// Then we can share those behaviours with Mikhail
// By creating a new object that has it's [[Prototype]] property
// pointing to `person'.
var mikhail = Object.create(person)
mikhail.first_name = 'Mikhail'
mikhail.last_name  = 'Weiß'
mikhail.age        = 19
mikhail.gender     = 'Male'

// And we can test whether things are actually working.
// First, `name' should be looked on `person'
mikhail.name
// => 'Mikhail Weiß'

// Setting `name' should trigger the setter
mikhail.name = 'Michael White'

// Such that `first_name' and `last_name' now reflect the
// previously name setting.
mikhail.first_name
// => 'Michael'
mikhail.last_name
// => 'White'

// `greet' is also inherited from `person'.
mikhail.greet('you')
// => 'Michael White: Why, hello there, you.'

// And just to be sure, we can check which properties actually
// belong to `mikhail'
Object.keys(mikhail)
// => [ 'first_name', 'last_name', 'age', 'gender' ]


3.2 Но как же [⁣[Prototype]⁣] работает?

Как вы видели в прошлом примере, ни одно из свойств, определённых в Person мы не определяли явно в Mikhail, но всё же смогли получить к ним доступ. Это произошло благодаря тому, что JavaScript реализует делегирование доступа к свойствам, т.е. свойство ищется через всех родителей объекта.

Эта цепь родителей определяется скрытым слотом в каждом объекте, который называется [⁣[Prototype]⁣]. Вы не можете изменить его непосредственно, существует только один способ задать ему значение — при создании нового объекта.

Когда свойство запрашивается из объекта, движок сначала пытается получить свойство из целевого объекта. Если свойство не найдено, то рассматривается непосредственный родитель объекта, затем родитель родителя и т.д.

Это означает, что мы можем изменить поведение прототипа в середине программы, то автоматически изменится поведение всех объектов, которые были от него унаследованы. Например, пусть мы хотим изменить приветствие по умолчанию:
// (person:String) → String
// Greets the given person
person.greet = function(person) {
    return this.name + ': Harro, ' + person + '.'
}

mikhail.greet('you')
// => 'Michael White: Harro, you.'


3.3. Перегрузка свойств

Так, прототипирование (т.е. наследование) используется для того, чтобы можно было поделиться данными с другими объектами. Причём этот способ работает быстро и экономичен по отношению к памяти, т.к. мы всегда имеем только один экземпляр используемых данных.

Но что если мы хотим добавить специализированное поведение, которым можно было бы делиться с другим объектами? Мы уже видели как объекты определяют своё поведение с помощью свойств, специализированное поведение определяется аналогичным образом — вы просто устанавливаете значение нужному свойству.

Для лучшей демонстрации положим, что Person реализует только обобщённое приветствие, а каждый наследник Person будет реализовывать своё уникальное приветствие. Также добавим новую персону в наш сценарий, чтобы лучше показать как расширяется объект:


Учтите, что и mikhail, и kristin определяют собственную версию метода greet. В этом случае мы вызовем метод greet из собственной версии поведения объекта, а не обобщённый метод greet, унаследованный от Person:
// Here we set up the greeting for a generic person

// (person:String) → String
// Greets the given person, formally
person.greet = function(person) {
    return this.name + ': Hello, ' + (person || 'you')
}

// And a greeting for our protagonist, Mikhail

// (person:String) → String
// Greets the given person, like a bro
mikhail.greet = function(person) {
    return this.name + ': \'sup, ' + (person || 'dude')
}

// And define our new protagonist, Kristin
var kristin = Object.create(person)
kristin.first_name = 'Kristin'
kristin.last_name  = 'Weiß'
kristin.age        = 19
kristin.gender     = 'Female'

// Alongside with her specific greeting manners

// (person:String) → String
// Greets the given person, sweetly
kristin.greet = function(person) {
    return this.name + ': \'ello, ' + (person || 'sweetie')
}

// Finally, we test if everything works according to the expected

mikhail.greet(kristin.first_name)
// => 'Michael White: \'sup, Kristin'

mikhail.greet()
// => 'Michael White: \'sup, dude'

kristin.greet(mikhail.first_name)
// => 'Kristin Weiß: \'ello, Michael'

// And just so we check how cool this [[Prototype]] thing is,
// let's get Kristin back to the generic behaviour

delete kristin.greet
// => true

kristin.greet(mikhail.first_name)
// => 'Kristin Weiß: Hello, Michael'


Продолжение следует...
Автор: @DreamWalker Quildreen Motta
Enterra
рейтинг 35,28
Компания прекратила активность на сайте

Похожие публикации

Комментарии (70)

  • +3
    Сильно! Спасибо вам, читаем и ждем продолжения!
  • +1
    Javascript с неявными геттерами и сеттерами уже не выглядит как java, это фишка как в Object Pascal, ну или как операторы в C++.

  • +10
    Утром решил перед работать прочитать интересную статью на хабре, пока пью кофе… эх затянулось, горите сроки синем пламенем!

    Побольше бы таких статей, а не рекламы и холивара.

    P.S. «Продолжение следует...» хех
  • +3
    Теперь всех кто пытается нагородить классов в Javascript, буду тыкать носом в эту статью
    • –4
      простите за небольшой флуд, но в личке с тов. andybel произошла интересная дискуссия. Процитирую:
      andybel:
      > Вот у меня есть имя, отчество и фамилия. У испанцев часто по нескольку имён. У казахов — имя и, кажется — отчество.
      > Поэтому идея классов — бредовая по определению. Она ничего кроме надуманной сложности не приносит, как в С++, так и в JS.
      > Я думаю, должно быть так
      > Испанец.имя = «Вася»
      > Испанец.имя2 = «Петя»
      > Испанец.имя3 = «Коля»
      > Язык должен позволять добавлять свойства вместе с присвоением значения.

      я:

      > По вашему примеру можно сделать свойство name массивом:
      > Испанец.name[0] = «Хуан»;
      > Испанец.name[1] = «Педрович»;
      > Испанец.name[2] = «Санта-Мария»;
      > Испанец.name[3] = «Гомес»;

      > И ходить по массиву циклом если что.

      andybel:
      > Можно и так и так, на то он и великий JS, разницы аж никакой.
      > Просто самое передовое там, где молодёжь. А в С++ и JAVA — одни пердуны с засохшими мозгами.
      > Не вина JS, что он вырастает из дерьма клиентских програмулин для сайтегов.
      • +3
        Только действительно недалекий человек может так резко отзываться о программистах на C++ и Java.
        • 0
          Ну капец, и мне в итоге досталось((
    • 0
      Всех, кто против оберток для создания «классов», давно тыкают носом в эту статью. Данная статья, кстати, ничем им не противоречит.
      • 0
        Ну да, сахарок, не более. Я же говорю про случаи когда пытаются портировать наследование из других языков.
  • 0
    Я конечно понимаю, что это перевод но сначала Вы пишите:
    : Однако, в JavaScript нет концепции класса. К примеру, объект с свойствами {name: Linda, age: 21} не является экземпляром какого-либо класса или класса Object.

    А далее по тексту:
    : Литеральный объект определяет новый объект, родитель которого Object.prototype (о родителях поговорим немного позже).

    Дак все-таки является ли литеральный объект экземпляром какого-либо класса?
    • 0
      Отвечу сам с примером.
      Базовым классам литерального объекта является Object.prototype: это можно легко проверить:

      Иначе откуда тогда появились методы .toString(), .hasOwnProperty(), valueOf() и т.д.

  • +1
    Спасибо за перевод.
    Особо приглянулось разжевывание поведения «this». Распечатаю и раздам коллегам-кодерам, которые «ой, да вообще он странный, ваш этот JS, особенно эта магия с this».
    Отдельное спасибо за ссылку на «Understanding delete»
    • +2
      Писал когда то статью про this в javascript — habrahabr.ru/post/149516/
      Там есть и возможность проверить свои навыки и потренироваться в угадывании результата
  • +3
    >Всё, включая Strings, Arrays, Numbers, Functions, и, очевидно, т.н. Object — это примитивы, но они конвертируются в объекты, когда вы пытаетесь оперировать ими.

    Подавился чаем и полез в спецификацию.
    • +3
      • 0
        Мм… Ну, мне сложно с этим поспорить. Однако это лишь перевод статьи. В оригинале написано следующим образом: «This includes Strings, Arrays, Numbers, Functions, and, obviously, the so-called Object — there are primitives, but they're converted to an object when you need to operate upon them. „
        • +2
          Речь про элементарные типы вероятно, но в любом случае автор неправ. Массивы и функции ну никак к ним не относятся. Это больше похоже на легкую степень бреда.
          Более детально можно почитать здесь.
          • 0
            Я после этих слов стал читать статью по диагонали.
            • 0
              Я после этого вообще прочтение отложил в долгий ящик…
            • +1
              Всё, с чем вы работаете в JavaScript является объектами. Всё, включая Strings, Arrays, Numbers, Functions — это примитивы, но они конвертируются в объекты, когда вы пытаетесь оперировать ими.

              Примитивные значения не являются объектами.

              var x=1; 
              

              У глобального объекта window появится свойство x, которое будет иметь значение 1, а не ссылку на объект Number.

              x+=1; 
              

              Тут будут присходить математические операции с примитивными значениями, а не с объектами, хотя мы и оперируем ими.
              • –2
                У глобального объекта window появится свойство x, которое будет иметь значение 1, а не ссылку на объект Number.


                Да?
                А если вызвать x.toString()? Отработает ведь? И что? Не объект, все равно?
                Выведите в консоли x.constructor.prototype и успокойтесь.

                Примитивы по природе своей immutable, поэтому работа с ними выглядит также как с обычными стековыми переменными.

                • 0
                  Все равно не объект! Когда вы напишете x.toString() будет вызван конструктор Number, который создаст объект-обертку, который после выполнения будет уничтожен. И да x.constructor.prototype выведет то что вы ожидаете, но после вывода объект снова будет уничтожен.
                  Но если вы все еще считаете что от этого число становится объектом, то просто попробуйте добавить ему новое свойство и затем выведите это свойство в консоль. Думаю вас ждет большой сюрприз:

                  var a = 1;
                  console.log(a.constructor.prototype);
                  a.color='green';
                  console.log(a.color); // OMG! undefined!!!
                  


                  P.S. надесь вы распознали скрытый тэг <сарказм>
                  • 0
                    Грёбаный автобоксинг :-)
                  • 0
                    А вот так объект:

                    var numberObject = new Number(1602);
                    
                    • 0
                      Так объект.
                • 0
                  Давайте начнем с прочтения спецификации

                  4.2 Language Overview

                  Properties are containers that hold other objects, primitive values, or functions. A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, and String; an object is a member of the remaining built-in type Object; and a function is a callable object.

                  Итак, свойствами могут быть объекты, примитивные значения или функции.

                  Далее, смотрим 11.2.1 Property Accessors
                  8. Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

                  Cвойства могут быть только у типа Reference (что логично). Для этого нужно примитивное значение предварительно превратить в объект, что и описано в 8.7.1 GetValue (V)
        • 0
          " — there are primitives, but..." переводится не как «это примитивы», а скорее как " — существуют (также) примитивы, но..." т.е. автор не виновен в том, в чём вы его обвиняете.
          • –1
            По просьбам трудящихся внёс небольшие корректировки в текст, чтобы больше никто не возмущался.
          • 0
            В чем не виноват автор? В том что утверждает что массивы и функции являются примитивами но они конвертируются в объекты при обращении к ним?
            Я видел много разных классификаций, но это вообще бред сивой кобылы. Почитайте спецификацию:
            Функция (function) # Ⓣ
            – элемент типа Object, являющийся экземпляром стандартного встроенного конструктора Function, который может быть вызван в качестве подпрограммы.


            • 0
              Убрал это предложение вообще.
              • 0
                Понимаете, здесь вопрос не в том что неверно одно предложение. Вопрос в том можно ли вообще доверять автору который пишет такое (речь про автора оригинальной статьи)?
                • 0
                  Согласен. Но от ошибок никто не застрахован. Однако, данную статью в целом я нахожу достаточно полезной. Если автор не совсем правильно написал про некоторый факт, то это не значит, что можно считать неверной всё статью.
      • 0
        Примитив или нет проверить довольно просто и без спецификации. Достаточно попробовать присвоить «объекту» новый член:

        var x = 1;
        x.hello = 'world';
        console.log(x.hello); // undefined
        


        Значит примитив. То же самое будет и со строками. И с Boolean. А вот регулярные выражения, объекты и массивы это объекты, а не примитивы, хоть и могут задаваться литералами.

        И, кстати, интересный факт, и число, и строка, и булевское значение может быть объектом, а не примитивом, но только в случае если оно создано как объект:

        > var x = new Number(1)
        undefined
        > x
        {}
        > x.toString()
        '1'
        > x.ha = 1
        1
        > x
        { ha: 1 }
        > typeof x
        'object'
        > x instanceof Number
        true
        > isNaN(x)
        false
        > x.toNumber
        undefined
        > Object.getOwnPropertyNames(x)
        [ 'ha' ]
        > Object.getOwnPropertyNames(x.__proto__)
        [ 'constructor',
          'toLocaleString',
          'valueOf',
          'toString',
          'toPrecision',
          'toFixed',
          'toExponential' ]
        > x.toFixed()
        '1'
        > x.toPrecision()
        '1'
        > x.toExponential()
        '1e+0'
        > x.valueOf()
        1
        
        • 0
          Спасибо, Кэп, я в курсе:)
  • –1
    Я бы хотел почитать про инкапсуляцию в js. Вроде как это один из пунктов ООП. Ждем от Вас еще статей.
    • +2
      По-моему основная проблема инкапсуляции в том, что её неправильно понимают.
      Издревне вбивалось в голову, что инкапсуляция — это возможность скрытия переменных через «приват» и всё такое.
      Но блин, инкапсуляция, как принцип ООП — это нифига не синтаксический сахар для ограничения доступа. Ограничение доступа вообще никак не влияет на архитектуру и на объектность.
      Имхо, инкапсуляция — это сокрытие реализации. То есть мы обращаемся к методу, функции и чему угодно, используем её публичный интерфейс и получаем результат. Не обращая внимание на её внутренности и ветвления. Это и есть инкапсуляция.
  • +3
    Минусую только за «Всё, с чем вы работаете в JavaScript является объектами.». Прекратите вводить людей в заблуждение.
  • –1
    чОрт, в конце злополучное «Продолжение следует»…
  • 0
    Об оригинале статьи уже где-то писал, повторюсь:
    Статейка неплохая, но либо на будущее, либо для тех, кто может полностью забить на браузер, только нода — ES5 по полной.
    В IE младше 9 недоступно Object.defineProperty, не эмулировать с «writable»,«enumerable»,«configurable», это главное.
    Без defineProperty Object.create можно эмулировать, но только с первым параметром.
    Ну и вообще в ней довольно странный подход к ООП.
  • +1
    Кстати, я тут подумал, тут бы оплачиваемый контент, который обсуждается в соседней теме о гугле… я бы заплатил не жалея ни копейки)
  • +1
    1. Очень хотелось бы увидеть пояснения, в каких браузерах работают описанные возможности. Насколько я знаю в IE Object.defineProperty и Function.bind работают только с 9 версии.

    2. Вы рписываете наследование с использованием Object.create. Поясните, пожалуйста, отличие наследования с применением Object.create от наследования с использованием конструкторов:

    function A() {
        this.x = 10; 
    }
    A.prototype = {
        constructor: A,
        y: 100
    };
    var a = new A();
    a.x; // 10
    a.y // 100 (из прототипа)
    


    На практике встречаю только способ наследования через конструкторы.

    3. Вы пишете:
    >>> Пустой объект — это объект без родителя, без свойств. Посмотрим на синтакс создания такого объекта в JavaScript:
    >>> var mikhail = Object.create(null)
    Что вижу я в Firebug:

    Можете пояснить подобное поведение?
    • +2
      К сожалению, я не автор этого текста, а только автор перевода. Я не являюсь гуру JavaScript. Сам много нового узнал из этой статьи — вот и решил поделиться с сообществом. Боюсь, что не смогу полно и правильно ответить на ваши вопросы.
    • 0
      Про конструкторы — в 4ой части оригинала, соответственно, наверное, в продолжении перевода:)
      • 0
        Огромное спасибо за таблицу совместимости. Да, про эмуляцию bind даже на хабре нашел.

        А можете 3-й пункт прояснить? То что создается с помощью Object.create ( []{} ) не похоже на обычный объект ( {} ). А typeof говорит что объект.
        var x = Object.create(null);
        typeof x   // "object"
        

        • 0
          Object.create(null) создает объект, у которого в __proto__ будет null, соответственно от не будет иметь методов из Object.prototype, в отличии от {}.
          • 0
            То есть чтобы с помощью Object.create создать объект, аналогичный {}, нужно написать следующим образом?

            var x = Object.create(Object.prototype)
            
            • 0
              Да. Вот только зачем?) Как по мне — он хорош либо в связке со втором параметром, либо для наследование, либо для клонирования.
              • 0
                Просто теоретический вопрос :)
                Спасибо за пояснения.
    • 0
      По третьему пункту:

      Когда создается объект через Object.create(use_as_proto), то в свойство __proto__ записывается ссылка на use_as_proto. При создании объекта через литерал в его __proto__ записывается Object.prototype. А Object.prototype содежит в себе методы типа toString, valueOf ну или типа того. При создании объекта с null ссылкой в прототипе, этих методов нет. А фаербаг использует их для вывода информации в консоль.

      В вашем коде схематически цепочки прототипов выглядят так:
      mikhail -> null
      mikhail2 -> Object.prototype -> null
  • +1
    Есть сценарии, в которых действительно удобно так делать, но стоит помнить, что такой механизм работает очень медленно.


    Запустив этот тест 20 раз, я получил 20 разных результатов на одном и том же браузере. Причем четыре раза именно этот способ и оказывался самым быстрым
  • 0
    а почему комментарии в коде не переведены?
  • 0
    К сожалению, объектная система JS чрезмерно запутана, но при этом не содержит совершенно очевидных и необходимых вещей. Например, почему язык не даёт простой возможности реализовать перегрузку методов? Вот, например, в C++ я могу сделать несколько конструкторов с одинаковыми именами. В JS же мне придётся изощряться — createFromHtml, createFromXml, createFromSql и т.д.
    Где возможность перегрузки операторов? Почему, работая с WebGL, до сих пор надо городить конструкции типа mulMatrices(m1,addVector(m2,v3)), вместо того, чтобы перегрузить операторы +,-,*?
    Где, наконец, нормальная модульность и пространства имён? Почему до сих пор private и protected-поля объектов приходится реализовывать через разнообразные костыли типа замыканий?
    Да и вообще любая JS-библиотека содержит такое количество вложенных скобок, что невольно вспоминаешь о Лиспе.
    • –1
      Могу посоветовать вам TypeScript — он избавит вас от вышеперечисленных проблем.
    • +1
      Вот, например, в C++ я могу сделать несколько конструкторов с одинаковыми именами.


      Улыбнуло про одинаковые имена — Как будто можно с разными сделать. :-)

      Это две стороны одной медали. В Си нет RTTI, нет рефлексии, потому что код генерируемый компилятором — это инструкции для конкретной железки, и потому он быстр.

      А в javascript как и в java есть instanceof, с его помощью можно сделать аналог override.
    • +1
      Вы встали не на тот путь. В классическом понимании перегрузку функция в JS осуществить нельзя, но не кто вам не мешает менять реализацию функции в зависимости от ваших желаний (в том числе и в зависимости от типов аргументов). Но этот недостаток с лихвой компенсируется динамической типизацией а так же тем что количество аргументов функции может быть произвольным! Скажем так, вы просто не можете отказаться от своих стереотипов организации перегрузки, поэтому и не видите других очевидных плюсов JS.

      Ну а что касается замыканий, кто вам сказал что они являются костылями? Вы просто не умеете их готовить ©.

      Мне вот интересно, зачем вы приходите в топик о JS и начинаете делать подобные вбросы?
      • 0
        но не кто вам не мешает

        Вот это даже меня, далеко не закоренелого граммар-наци, уже убивает, я секунд 20 размышлял, что вы хотите сказать, пришлось предложение до конца пару раз прочитать даже.
        • 0
          Это уже совсем другая тема. Хотите об этом поговорить? Я скажу только одно: возможно русский язык я знаю хуже чем некоторые другие, ну и что?
    • +1
      Да, вы мыслите стереотипами C++.
      Ещё скажите, что динамическая типизация — зло, в JS нужна статическая, а там её нету, потому он плохой.

      В C++ есть перегрузка функций. В JS её нету, но там она и не нужна. Давно используются другие приёмы. На то она и динамическая типизация, что количество параметров и их тип может быть любой. Посмотрите на jQuery:
      > element.attr('title'); // аналог getAttribute
      > element.attr('title', 'bar'); // аналог setAttribute
      > element.attr({ title: 'bar', src: 'baz.jpg' }); // аналог setAttributes

      Перегрузки операторов — порой не хватает, да. Но во-первых, её вполне могут добавить в будущих версиях ECMAScript. А уже есть такой интересный объект, как Proxy. Во-вторых, кто-нибудь обязательно додумается перегрузить операторы для стандартных классов (а подобная возможность, готов поспорить, будет), например Number, и получит кучу проблем ;).

      Далее. Замыкания — это не костыли. А, наоборот, важное достоинство языка. Ими и реализуется пространство имён.
  • +2
    Заканчивается 2012 год, а на хабре всё еще пытаются понять ООП в JS. Пичаль-пичаль.
    • 0
      Моя дочка начнет понимать ООП в JS только лет через 10-15 (если она конечно решить заняться программированием), в чем пичаль то? :)
      • –2
        В том, что раз в полгода на хабре появляется очередная публикация подобного рода. Это сродни публикации «Земля — круглая» на каком-нить nat-geo.ru. Мы же не на дошкольном ресурсе, где актуально рассказывать про основы мироздания, правда?
        • +1
          Так расскажите нам чего-нибудь не дошкольного.
        • –2
          Раз в полгода? По-моему, каждые две недели)
  • 0
    На заметку: все имена свойств в конечном счёте конвертируются в строку, т.е. записи object[1], object[[1]], object['1'] и object[variable] (где значение variable равно 1) эквивалентны.


    object[[1]] — а вот так вот вообще никогда не делайте, вообще даже в одну линейку с object[1] и object['1'], при сравнении синтаксиса, ставить не стоит.
    Объяснение простое — массив с одним элементом привелся к выражению со значением первого элемента.
    Но такой код обычно является причиной головной боли.
  • 0
    Статья выглядит хорошей. Но наличие недочетов или недосказанностей делает ее не безопасной. Не рассказана история цепочки прототипов, про скрытое (по стандарту) __proto__. Перемешаны физическое (непосредственно код) и абстрактное (упоминание [[prototype]]) устройство объектов. Из-за таких статей новичики вытаются сделать что-то вроде my_object.prototype.

    По составу статьи: половина — документация, пересказанная своими словами, половина — теория (без четкого, как я уже упомянул, разделения).
  • 0
    Отличная статья, благодарю за перевод.
    Узнал много полезного для себя.

    Из пожеланий — думаю будет полезно в статье проставить ссылки на такие понятия как "литеральный синтаксис" и "сущности первого класса" и "анонимные функции", для того чтобы менее опытным читателям было проще разобраться в статье, не застревая в терминах.
  • 0
    Не понравилось то, что автор уделил слишком мало внимания литеральной форме записи объектов, больше внимания ES5, получается в итоге нечто оторванное от практики из-за неважной поддержки в еще не слишком старых браузерах. Кроме того, если статья рассчитана на новичков, зачем их учить не ставить точки с запятыми? Это может привести к проблемам в некоторых случаях. Ну и ошибки то ли в самой статье, то ли в переводе, о которых говорили выше.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка