Раннее использование ООП в JavScript ES5 базировалась на прототипной реализации ОО модели и описывается в данной статье для лучшего понимания ООП в JavaScript ES6.
«Старая версия» — условное понимание, потому что, во многом, реализация ООП в ES6 — это синтаксический сахар поверх реализации ES5 и имеется обратная совместимость кода.
Очень важно понимать, при чтении данной статьи, что все описание ООП в данном контексте относится к ES5.
1. Объекты в JavaScript
Объект в JavaScript — это просто коллекция пар ключ-значение и в отличии от классической парадигмы ООП в JavaScript объект не является экземпляром определенного класса, а являются экземплярами самих себя.
Объект — это пара ключ-значение, где имена ключей уникальны, а пустой объект в JavaScript — объект без родителя и без свойств (параметров).
В JavaScript версии ES5 объект создается передачей параметров родителя и свойств в метод глобального объекта Object.create(params). Если параметры не заданы и задан прототип, как null, то создается пустой объект без родительского объекта и свойств. Объекты можно создать литеральным способ, который неявно будет наследоваться от Object.prototype или явно через метод Object.create
var person = Object.create(null)
console.log(person) // [Object: null prototype] {}
person = {}
console.log(person) // {}
2. Свойства объектов
Если кратко, то свойства — это ключи или свойства объекта, которые уникальны в пределах данного объекта.
2.1. Прототип и свойства
В предыдущем подпункте мы указали, что при создании нового объекта нам нужно указать параметры в виде родительского объекта и свойств.
Грубо говоря, в парадигме ООП в JavaScript, принято говорить, что:
- родительский объект — это прототип, т.е. объект, с которого делается копия или наследуется
- свойства — это поля и методы нового объекта.
Свойства в JavaScript являются динамическими, т.е. их можно создавать на лету даже после создания объекта и для этого используются методы
- Object.defineProperty(prototype, property, descriptor)
- Object.defineProperties({prototype, { property, descriptor}, … )
Если при создании свойство уже имеется в прототипе объекта, то старое свойство будет затираться новым значением и новым дескриптором описания свойства
var person = {
name: "Sara",
profession: "Student",
age: 19,
}
Object.defineProperties(person, { name: { value: 'Sara'
, writable: true
, configurable: true
, enumerable: true }
, age: { value: 20
, writable: true
, configurable: true
, enumerable: true }
, gender: { value: 'Female'
, writable: true
, configurable: true
, enumerable: true }})
console.log(person) // { name: 'Sara', profession: 'Student', age: 20, gender: 'Female' }
2.2. Дескрипторы свойств
Дескриптор — это объект, который описывает поведение создаваемого или переопределяемого свойства. Свойства дескриптора или флаги управляют данными и доступом к этим данных. Флаги задаются значением true или false, а свойство данных принимает определенное значение
Рассмотрим некоторые флаги:
- writable — значение свойства может быть изменено, используется только для дескрипторов данных.
- configurable — тип свойства может быть изменён или свойство может быть удалено.
- enumerable — свойство используется в общем перечислении.
- value — значение свойства. Если новому свойству не задано значение, то оно будет undefined
- get — принимает функцию без аргументов, которая возвращает значение свойства после обработки.
- set — принимает функцию с аргументом, который становится значением свойства после обработки.
2.3. Другие способы создания свойств
Способ создания и переопределения свойств объекта при помощи Object.defineProperty/Object.defineProperties не удобен для повседневного использования программистом, поэтому, чтобы облегчить динамическое создание свойств в JavaScript есть возможность использовать скобочный и точечный способы определения.
2.3.1. Скобочный способ определения свойств объекта
Первый способ — это скобочный способ создания или переопределения значения свойств объекта, который заключается в использовании квадратных скобок
var person = {
name: "Sara",
profession: "Student",
age: 19,
}
person["gender"] = "Female"
person["age"] = 20
console.log(person) // { name: 'Sara', profession: 'Student', age: 20, gender: 'Female' }
2.3.2. Точечный способ определения свойств объекта
Данный способ заключается в использовании точки в виде разделителя между свойством и объектом
var person = {
name: "Sara",
profession: "Student",
age: 19,
}
person.gender = "Female"
person.age = 20
console.log(person) // { name: 'Sara', profession: 'Student', age: 20, gender: 'Female' }
2.4. Удаление свойств объекта
Удаление свойств объекта производится ключевым словом delete, после чего значение удаленного свойства вновь становится undefined
var person = {
name: "Sara",
profession: "Student",
age: 19,
}
person.gender = "Female"
person["age"] = 20
delete person.gender
delete person["age"]
console.log(person) // { name: 'Sara', profession: 'Student' }
Оператор delete вернёт true, если свойство удалено, и false — в противном случае.
2.5. Геттеры и сеттеры
В разделе 1.3 мы не разъяснили суть 2-х дополнительных флагов get и set. Если кратко, то суть их заключается в создании прокси блока, в котором принимаемый или устанавливаемый свойству значение проходит дополнительный фильтр обработки.
var person = {
firstName: "Sara",
lastName: "Rain",
profession: "Student",
age: 19,
}
function setName (name){
var n = name.trim().split(/\s+/)
this.firstName = n[0]
this.lastName = n[1]
}
function getName (){
return this.firstName + ' ' + this.lastName
}
Object.defineProperty(person, 'name', {
get: getName,
set: setName,
//writable: true,
configurable: true,
enumerable: true })
console.log(person) // {firstName:'Sara',lastName:'Rain',profession:'Student',age:19,name:[Getter/Setter]}
person.name = "Erika Johnson"
console.log(person) // {firstName:'Erika',lastName:'Johnson',profession:'Student',age:19,name:[Getter/Setter]}
console.log(person.name) // Erika Johnson
нужно заметить, что в случае указания значений для свойств дескриптора set и get нужно убрать флаг writable, иначе выйдет ошибка неоднозначности.
2.6. Перечисление свойств
Учитывая то, что свойства в JavaScript являются динамическими коллекциями пары ключ-значение, то в языке предусмотрен механизм, который следит за тем, чтобы контролировать процесс перечисления.
Чтобы перечислить все свойства, или почти все, в составе объекта Object имеются 2 метода
- Object.getOwnPropertyNames(person) — вернёт вам Array, содержащий имена всех свойств, установленных для данного объекта.
- Object.keys(person) — вернёт список собственных свойств, которые помечены флагом enumerable
var person = {
name: "Sara",
profession: "Student",
age: 19,
}
Object.defineProperties(person, { name: {
enumerable: true }
, age: {
enumerable: true }
, gender: {
enumerable: false }})
console.log(Object.getOwnPropertyNames(person)) // [ 'name', 'profession', 'age', 'gender' ]
console.log(Object.keys(person)) // [ 'name', 'profession', 'age' ]
3. Методы
Не вдаваясь в дебри, скажем, что в отличии от свойств, методы — это объекты, которые предполагают некоторые действия со значениями свойств.
3.1. Динамическое создание методов
Динамическое создание методов ничем не отличается от создания обычных свойств, учитывая, что функции в JavaScript — это те же объекты, которые можно присваивать переменным
var person = {
name: "Sara Rain",
age: 19,
}
person['sayHello'] = function(){
console.log(this.name + ' say hello!')
}
person.sayBye = function(){
console.log(this.name + ' say bye!')
}
person.sayHello() // Sara Rain say hello!
person.sayBye() // Sara Rain say bye!
3.2. Динамический this
Переменная внутри функции this хранит ссылку на текущий экземпляр, на котором данный метод вызывается, но это не всегда означает, что данная переменная всегда в себе хранит ссылку на объект. Поэтому в JavaScript переменная this определяет динамическую ссылку, которая разрешается в момент исполнения функции и в этом заключается ее динамичность.
Есть 4 способа разрешения this внутри функции:
- как метод
- непосредственно
- явное применение
- как конструктор
Пока рассмотрим первые 3 способа. Чтобы рассмотреть каждый способ разрешения напишем тестовый код
function add(other) {
var result = this.value + ' ' + other
console.log(result)
return result
}
var objectOne = { value: 'objectOne', add: add }
var objectTwo = { value: 'objectTwo', add: add }
3.2.1. Разрешение this, как метод
Если функция вызывается, как метод объекта, то this внутри функции ссылается на сам объект. Т.е. когда мы явно указываем какой объект выполняет действие, то объект и будет значением this в нашей функции
...
objectOne.add(objectTwo.value) // this === objectOne
// => objectOne objectTwo
objectTwo.add('someValue') // this === objectTwo
// => objectTwo someValue
3.2.2. Непосредственное разрешение this
Когда функция вызывается непосредственно, то this разрешается в глобальный объект движка (window в браузере, global в Node.js)
...
add(objectTwo.value) // this === global
// => undefined objectTwo"
// В глобальной области нет переменной `value', давайте решим это
value = 'someValue'
add(objectTwo.value) // this === global
// => someValue objectTwo
3.2.3. Явное разрешение this
Функция может быть явно применена к любому объекту, несмотря на то, есть ли в объекте соответствующее свойство или нет. Эта функциональность достигается с помощью методов call или apply.
Функция call ожидает объект, как первый параметр функции, за которым следуют обычные аргументы исходной функции:
...
add.call(objectTwo, 'someValue1', 'someValue2') // this === objectTwo
// => objectTwo someValue1
add.call(window, 'someValue3') // this === global
// => undefined someValue3
add.call(objectOne, objectOne.value) // this === objectOne
// => objectOne objectOne
Функция apply позволяет описывать вторым параметром массив параметров исходной функции:
...
add.call(objectTwo, ['someValue1', 'someValue2']) // this === objectTwo
// => objectTwo someValue1
add.call(window, ['someValue3']) // this === global
// => undefined someValue3
add.call(objectOne, [objectOne.value]) // this === objectOne
// => objectOne objectOne