This commit is contained in:
Ilya Kantor 2015-07-13 11:24:25 +03:00
parent 413e552c29
commit 549af795e0
2 changed files with 143 additions and 43 deletions

View file

@ -76,26 +76,23 @@ alert( user["мой зелёный крокодил"] ); // Вася
<li>`Object.getPrototypeOf(obj)`</li> <li>`Object.getPrototypeOf(obj)`</li>
</ul> </ul>
В современной JavaScript также добавился сеттер: В ES-2015 также добавился сеттер:
<ul> <ul>
<li>`Object.setPrototypeOf(obj, newProto)`</li> <li>`Object.setPrototypeOf(obj, newProto)`</li>
</ul> </ul>
...А также "узаконено" свойство `__proto__`, которое даёт прямой доступ к прототипу. Его, в качестве "нестандартного", но удобного способа работы с прототипом реализовали почти все браузеры (кроме IE10-), так что было принято решение добавить его в стандарт. ...А также "узаконено" свойство `__proto__`, которое даёт прямой доступ к прототипу. Его, в качестве "нестандартного", но удобного способа работы с прототипом реализовали почти все браузеры (кроме IE10-), так что было принято решение добавить его в стандарт.
По стандарту оно реализовано через геттеры-сеттеры `Object.getPrototypeOf/setPrototypeOf`.
## Object.assign ## Object.assign
Функция `Object.assign` получает список объектов и копирует в первый `target` свойства из остальных.
Синтаксис: Синтаксис:
```js ```js
Object.assign(target, src1, src2...) Object.assign(target, src1, src2...)
``` ```
Функция `Object.assign` получает список объектов и копирует в первый `target` свойства из остальных. При этом последующие свойства перезаписывают предыдущие.
Последующие свойства перезаписывают предыдущие.
Например: Например:
@ -109,14 +106,31 @@ let admin = { isAdmin: true };
Object.assign(user, visitor, admin); Object.assign(user, visitor, admin);
// user <- visitor <- admin
alert( JSON.stringify(user) ); // user: Вася, visits: true, isAdmin: true alert( JSON.stringify(user) ); // user: Вася, visits: true, isAdmin: true
``` ```
Его также можно использовать для 1-уровневого клонирования объекта:
```js
'use strict';
let user = { name: "Вася", isAdmin: false };
*!*
// clone = пустой объект + все свойства user
let clone = Object.assign({}, user);
*/!*
```
## Object.is(value1, value2) ## Object.is(value1, value2)
Возвращает `true`, если `value1 === value2`, иначе `false`. Новая функция для проверки равенства значений.
Есть, однако, два отличия от обычного `===`, а именно: Возвращает `true`, если значения `value1` и `value2` равны, иначе `false`.
Она похожа на обычное строгое равенство `===`, но есть отличия:
```js ```js
//+ run //+ run
@ -130,20 +144,20 @@ alert( Object.is(NaN, NaN) ); // true
alert( NaN === NaN ); // false alert( NaN === NaN ); // false
``` ```
При сравнении объектов через `Object.is` успользуется алгоритм [SameValue](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-samevalue), который неявно применяется во многих других местах современного стандарта. Отличия эти в большинстве ситуаций некритичны, так что непохоже, чтобы эта функция вытеснила обычную проверку `===`. Что интересно -- этот алгоритм сравнения, который называется [SameValue](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-samevalue), применяется во внутренних реализациях различных методов современного стандарта.
## Методы объекта ## Методы объекта
Долгое время в JavaScript термин "метод объекта" был просто альтернативным названием для свойства-функции. Долгое время в JavaScript термин "метод объекта" был просто альтернативным названием для свойства-функции.
Теперь это уже не так, добавлены именно "методы объекта". Они, по сути, являются "свойствами-функциями, привязанными к объекту". Теперь это уже не так, добавлены именно "методы объекта", которые, по сути, являются свойствами-функциями, привязанными к объекту.
Их особенности: Их особенности:
<ol> <ol>
<li>Более короткий синтаксис.</li> <li>Более короткий синтаксис объявления.</li>
<li>Наличие в методах специального внутреннего свойства `[[HomeObject]]` ("домашний объект"), ссылающегося на объект, которому метод принадлежит. Мы посмотрим его использование чуть дальше, в разделе про `super`.</li> <li>Наличие в методах специального внутреннего свойства `[[HomeObject]]` ("домашний объект"), ссылающегося на объект, которому метод принадлежит. Мы посмотрим его использование чуть дальше.</li>
</ol> </ol>
Для объявления метода вместо записи `"prop: function() {…}"` нужно написать просто `"prop() { … }"`. Для объявления метода вместо записи `"prop: function() {…}"` нужно написать просто `"prop() { … }"`.
@ -158,7 +172,7 @@ let name = "Вася";
let user = { let user = {
name, name,
*!* *!*
/* вместо sayHi: function() { */ // вместо "sayHi: function() {" пишем "sayHi() {"
sayHi() { sayHi() {
alert(this.name); alert(this.name);
} }
@ -168,7 +182,7 @@ let user = {
user.sayHi(); // Вася user.sayHi(); // Вася
``` ```
Как видно, создание такого метода -- чуть короче, а обращение -- не отличается от обычной функции. Как видно, для создания метода нужно писать меньше букв. Что же касается вызова -- он ничем не отличается от обычной функции. На данном этапе можно считать, что "метод" -- это просто сокращённый синтаксис для свойства-функции. Дополнительные возможности, которые даёт такое объявление, мы рассмотрим позже.
Также методами станут объявления геттеров `get prop()` и сеттеров `set prop()`: Также методами станут объявления геттеров `get prop()` и сеттеров `set prop()`:
@ -207,11 +221,17 @@ let user = {
alert( user.getFirstName() ); // Вася alert( user.getFirstName() ); // Вася
``` ```
Итак, мы рассмотрели синтаксические улучшения. Если коротко, то не надо писать слово "function". Теперь перейдём к другим отличиям.
## super ## super
В ES-2015 появилось новое ключевое слово `super`. Оно предназначено только для использования в методах объекта.
Вызов `super.parentProperty` позволяет из метода объекта получить свойство его прототипа. Вызов `super.parentProperty` позволяет из метода объекта получить свойство его прототипа.
Например, в коде ниже `rabbit` наследует от `animal`. Вызов `super.walk` из метода объекта `rabbit` обращается к `animal.walk`: Например, в коде ниже `rabbit` наследует от `animal`.
Вызов `super.walk()` из метода объекта `rabbit` обращается к `animal.walk()`:
```js ```js
//+ run //+ run
@ -236,9 +256,11 @@ let rabbit = {
rabbit.walk(); rabbit.walk();
``` ```
Как правило, это используется в [классах](/class), которые мы рассмотрим в следующем разделе, но важно понимать, что "классы" здесь на самом деле не при чём. Свойство `super` работает через прототип, на уровне методов объекта.
При обращении через `super` используется `[[HomeObject]]` текущего метода, и от него берётся `__proto__`. Поэтому `super` работает только внутри методов. При обращении через `super` используется `[[HomeObject]]` текущего метода, и от него берётся `__proto__`. Поэтому `super` работает только внутри методов.
Например, если переписать этот код, оформив `rabbit.walk` как обычное свойство-функцию, то будет ошибка: В частности, если переписать этот код, оформив `rabbit.walk` как обычное свойство-функцию, то будет ошибка:
```js ```js
//+ run //+ run
@ -253,7 +275,7 @@ let animal = {
let rabbit = { let rabbit = {
__proto__: animal, __proto__: animal,
*!* *!*
walk: function() { walk: function() { // Надо: walk() {
super.walk(); // Будет ошибка! super.walk(); // Будет ошибка!
} }
*/!* */!*
@ -289,9 +311,11 @@ let rabbit = {
rabbit.walk(); rabbit.walk();
``` ```
Ранее мы говорили о том, что у функций-стрелок нет своего `this`, `arguments`: они используют те, которые во внешней функции. Теперь к этому списку добавился ещё и `super`.
[smart header="Свойство `[[HomeObject]]` -- не изменяемое"] [smart header="Свойство `[[HomeObject]]` -- не изменяемое"]
При создании метода -- он привязан к своему объекту навсегда. Технически можно даже скопировать его и запустить независимо: При создании метода -- он привязан к своему объекту навсегда. Технически можно даже скопировать его и запустить отдельно, и `super` продолжит работать:
```js ```js
//+ run //+ run
@ -310,14 +334,14 @@ let rabbit = {
let walk = rabbit.walk; // скопируем метод в переменную let walk = rabbit.walk; // скопируем метод в переменную
*!* *!*
walk(); walk(); // вызовет animal.walk()
// I'm walking // I'm walking
*/!* */!*
``` ```
В примере выше метод `walk()` запускается отдельно от объекта, но всё равно сохраняется через `super` доступ к его прототипу, благодаря `[[HomeObject]]`. В примере выше метод `walk()` запускается отдельно от объекта, но всё равно сохраняется через `super` доступ к его прототипу, благодаря `[[HomeObject]]`.
Это относится именно к `super`. Правила `this` для методов те же, что и для обычных функций. В примере выше при вызове `walk()` без объекта `this` будет `undefined`. Это -- скорее технический момент, так как методы объекта, всё же, предназначены для вызова в контексте этого объекта. В частности, правила для `this` в методах -- те же, что и для обычных функций. В примере выше при вызове `walk()` без объекта `this` будет `undefined`.
[/smart] [/smart]
## Итого ## Итого

View file

@ -1,11 +1,20 @@
# Классы # Классы
В современном JavaScript появился новый, "более красивый" синтаксис для классов. Он естественным образом продолжает синтаксис для объектов и методов, который мы рассмотрели раньше. В современном JavaScript появился новый, "более красивый" синтаксис для классов.
Новая конструкция `class` -- удобный "синтаксический сахар" для задания конструктора вместе с прототипом.
## Class ## Class
Новая конструкция `class` -- удобный "синтаксический сахар" для задания конструктора вместе с прототипом. Синтаксис для классов выглядит так:
```js
class Название [extends Родитель] {
constructor
методы
}
```
Например: Например:
@ -29,6 +38,8 @@ let user = new User("Вася");
user.sayHi(); // Вася user.sayHi(); // Вася
``` ```
Функция `constructor` запускается при создании `new User`, остальные методы -- записываются в `User.prototype`.
Это объявление примерно аналогично такому: Это объявление примерно аналогично такому:
```js ```js
@ -43,24 +54,24 @@ User.prototype.sayHi = function() {
В обоих случаях `new User` будет создавать объекты. Метод `sayHi` -- также в обоих случаях находится в прототипе. В обоих случаях `new User` будет создавать объекты. Метод `sayHi` -- также в обоих случаях находится в прототипе.
Но есть и отличия при объявлении через `class`: Но при объявлении через `class` есть и ряд отличий:
<ul> <ul>
<li>`User`, объявленный как класс, нельзя вызывать без `new`, будет ошибка.</li> <li>`User` нельзя вызывать без `new`, будет ошибка.</li>
<li>Объявление класса с точки зрения области видимости ведёт себя как `let`.</li> <li>Объявление класса с точки зрения области видимости ведёт себя как `let`. В частности, оно видно только текущем в блоке и только в коде, который находится ниже объявления (Function Declaration видно и до объявления).</li>
</ul> </ul>
Методы, объявленные внутри `class`, также имеют ряд особенностей: Методы, объявленные внутри `class`, также имеют ряд особенностей:
<ul> <ul>
<li>Метод `sayHi` является именно методом, то есть имеет доступ к `super`.</li> <li>Метод `sayHi` является именно методом, то есть имеет доступ к `super`.</li>
<li>Все методы класса работают в `use strict`, даже если он не указан.</li> <li>Все методы класса работают в режиме `use strict`, даже если он не указан.</li>
<li>Все методы класса не перечислимы, то есть в `for..in` по объекту их не будет.</li> <li>Все методы класса не перечислимы. То есть в цикле `for..in` по объекту их не будет.</li>
</ul> </ul>
## Class Expression ## Class Expression
Так же, как и Function Expression, классы можно задавать "инлайн" в выражении. Так же, как и Function Expression, классы можно задавать "инлайн", в любом выражении и внутри вызова функции.
Это называется Class Expression: Это называется Class Expression:
@ -94,6 +105,39 @@ new User(); // ошибка
В примере выше имя `User` будет доступно только внутри класса и может быть использовано, например для создания новых объектов данного типа. В примере выше имя `User` будет доступно только внутри класса и может быть использовано, например для создания новых объектов данного типа.
Наиболее очевидная область применения этой возможности -- создание вспомогательного класса прямо при вызове функции.
Например, функция `createModel` в примере ниже создаёт объект по классу и данным, добавляет ему `_id` и пишет в "реестр" `allModels`:
```js
//+ run
'use strict';
let allModels = {};
function createModel(Model, ...args) {
let model = new Model(...args);
model._id = Math.random().toString(36).slice(2);
allModels[model._id] = model;
return model;
}
let user = createModel(class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}, "Вася");
user.sayHi(); // Вася
alert( allModels[user._id].name ); // Вася
```
## Геттеры, сеттеры и вычисляемые свойства ## Геттеры, сеттеры и вычисляемые свойства
В классах, как и в обычных объектах, можно объявлять геттеры и сеттеры через `get/set`, а также использовать `[…]` для свойств с вычисляемыми именами: В классах, как и в обычных объектах, можно объявлять геттеры и сеттеры через `get/set`, а также использовать `[…]` для свойств с вычисляемыми именами:
@ -108,20 +152,23 @@ class User {
this.lastName = lastName; this.lastName = lastName;
} }
// геттер
*!* *!*
// геттер
*/!*
get fullName() { get fullName() {
return `${this.firstName} ${this.lastName}`; return `${this.firstName} ${this.lastName}`;
} }
*/!*
// сеттер
*!* *!*
// сеттер
*/!*
set fullName(newValue) { set fullName(newValue) {
[this.firstName, this.lastName] = newValue.split(' '); [this.firstName, this.lastName] = newValue.split(' ');
} }
*/!*
*!*
// вычисляемое название метода
*/!*
["test".toUpperCase()]: true ["test".toUpperCase()]: true
}; };
@ -135,9 +182,23 @@ alert( user.TEST ); // true
При чтении `fullName` будет вызван метод `get fullName()`, при присвоении -- метод `set fullName` с новым значением. При чтении `fullName` будет вызван метод `get fullName()`, при присвоении -- метод `set fullName` с новым значением.
[warn header="`class` не позволяет задавать свойства-значения"]
В синтаксисе классов, как мы видели выше, можно создавать методы. Они будут записаны в прототип, как например `User.prototype.sayHi`.
Однако, нет возможности задать в прототипе обычное значение (не функцию), такое как `User.prototype.key = "value"`.
Конечно, никто не мешает после объявления класса в прототип дописать подобные свойства, однако предполагается, что в прототипе должны быть только методы.
Если свойство-значение, всё же, необходимо, то, можно создать геттер, который будет нужное значение возвращать.
[/warn]
## Статические свойства ## Статические свойства
Статические свойства класса -- это свойства непосредственно класса `User`. Класс, как и функция, является объектом. Статические свойства класса `User` -- это свойства непосредственно `User`, то есть доступные из него "через точку".
Для их объявления используется ключевое слово `static`.
Например: Например:
@ -165,9 +226,22 @@ alert( user.firstName ); // Гость
alert( User.createGuest ); // createGuest ... (функция) alert( User.createGuest ); // createGuest ... (функция)
``` ```
Как правило, они используются для операций, не требующих наличия объекта, например -- для фабричных, как в примере выше, то есть как альтернативные варианты конструктора. Как правило, они используются для операций, не требующих наличия объекта, например -- для фабричных, как в примере выше, то есть как альтернативные варианты конструктора. Или же, можно добавить метод `User.compare`, который будет сравнивать двух пользователей для целей сортировки.
Или же, можно добавить метод `User.compare`, который будет сравнивать двух пользователей для целей сортировки. Также статическими удобно делать константы:
```js
//+ run
'use strict';
class Menu {
static get elemClass() {
return "menu"
}
}
alert( Menu.elemClass ); // menu
```
## Наследование ## Наследование
@ -178,7 +252,7 @@ class Child extends Parent {
} }
``` ```
В примере ниже объявлено два класса: `Animal` и наследующий от него `Rabbit`: Посмотрим, как это выглядит на практике. В примере ниже объявлено два класса: `Animal` и наследующий от него `Rabbit`:
```js ```js
//+ run //+ run
@ -208,8 +282,9 @@ new Rabbit("Вася").walk();
// and jump! // and jump!
``` ```
[smart header="Стандартная цепочка прототипов"] Как видим, в `new Rabbit` доступны как свои методы, так и (через `super`) методы родителя.
При наследовании формируется стандартная цепочка прототипов: методы `Rabbit` находятся в `Rabbit.prototype`, методы `Animal` -- в `Animal.prototype`, и они связаны через `__proto__`:
Это потому, что при наследовании через `extends` формируется стандартная цепочка прототипов: методы `Rabbit` находятся в `Rabbit.prototype`, методы `Animal` -- в `Animal.prototype`, и они связаны через `__proto__`:
```js ```js
//+ run //+ run
@ -220,15 +295,14 @@ class Rabbit extends Animal { }
alert( Rabbit.prototype.__proto__ == Animal.prototype ); // true alert( Rabbit.prototype.__proto__ == Animal.prototype ); // true
``` ```
[/smart]
Как видно из примера выше, методы родителя можно переопределить в наследнике. При этом для обращения к родительскому методу используют `super.method()`. Как видно из примера выше, методы родителя (`walk`) можно переопределить в наследнике. При этом для обращения к родительскому методу используют `super.walk()`.
Немного особая история -- с конструктором. Немного особая история -- с конструктором.
Конструктор `constructor` родителя наследуется автоматически. То есть, если в потомке не указан свой `constructor`, то используется родительский. В примере выше `Rabbit`, таким образом, использует `constructor` от `Animal`. Конструктор `constructor` родителя наследуется автоматически. То есть, если в потомке не указан свой `constructor`, то используется родительский. В примере выше `Rabbit`, таким образом, использует `constructor` от `Animal`.
Если `constructor` переопределить, то чтобы в нём вызвать конструктор родителя -- используется синтаксис `super()` с аргументами для родителя. Если же у потомка свой `constructor`, то чтобы в нём вызвать конструктор родителя -- используется синтаксис `super()` с аргументами для родителя.
Например, вызовем конструктор `Animal` в `Rabbit`: Например, вызовем конструктор `Animal` в `Rabbit`:
@ -258,7 +332,7 @@ class Rabbit extends Animal {
new Rabbit().walk(); // I walk: Кроль new Rabbit().walk(); // I walk: Кроль
``` ```
...Однако, здесь есть небольшие ограничения: Для такого вызова есть небольшие ограничения:
<ul> <ul>
<li>Вызвать конструктор родителя можно только изнутри конструктора потомка. В частности, `super()` нельзя вызвать из произвольного метода.</li> <li>Вызвать конструктор родителя можно только изнутри конструктора потомка. В частности, `super()` нельзя вызвать из произвольного метода.</li>
<li>В конструкторе потомка мы обязаны вызвать `super()` до обращения к `this`. До вызова `super` не существует `this`, так как по спецификации в этом случае именно `super` инициализует `this`.</li> <li>В конструкторе потомка мы обязаны вызвать `super()` до обращения к `this`. До вызова `super` не существует `this`, так как по спецификации в этом случае именно `super` инициализует `this`.</li>
@ -281,6 +355,8 @@ class Rabbit extends Animal {
constructor() { constructor() {
alert(this); // ошибка, this не определён! alert(this); // ошибка, this не определён!
// обязаны вызвать super() до обращения к this // обязаны вызвать super() до обращения к this
super();
// а вот здесь уже можно использовать this
} }
*/!* */!*
} }