work
|
@ -0,0 +1,4 @@
|
|||
|
||||
1. `true`, taken from `rabbit`.
|
||||
2. `null`, taken from `animal`.
|
||||
3. `undefined`, there's no such property any more.
|
|
@ -0,0 +1,31 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Working with prototype
|
||||
|
||||
Here's the code that creates a pair of object, then alters them.
|
||||
|
||||
Which values will be shown in the process?
|
||||
|
||||
```js
|
||||
let animal = {
|
||||
jumps: null
|
||||
};
|
||||
let rabbit = {
|
||||
__proto__: animal,
|
||||
jumps: true
|
||||
};
|
||||
|
||||
alert( rabbit.jumps ); // ? (1)
|
||||
|
||||
delete rabbit.jumps;
|
||||
|
||||
alert( rabbit.jumps ); // ? (2)
|
||||
|
||||
delete animal.jumps;
|
||||
|
||||
alert( rabbit.jumps ); // ? (3)
|
||||
```
|
||||
|
||||
There should be 3 answers.
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
1. Let's add `__proto__`:
|
||||
|
||||
```js run
|
||||
let head = {
|
||||
glasses: 1
|
||||
};
|
||||
|
||||
let table = {
|
||||
pen: 3,
|
||||
__proto__: head
|
||||
};
|
||||
|
||||
let bed = {
|
||||
sheet: 1,
|
||||
pillow: 2,
|
||||
__proto__: table
|
||||
};
|
||||
|
||||
let pockets = {
|
||||
money: 2000,
|
||||
__proto__: bed
|
||||
};
|
||||
|
||||
alert( pockets.pen ); // 3
|
||||
alert( bed.glasses ); // 1
|
||||
alert( table.money ); // undefined
|
||||
```
|
||||
|
||||
2. In modern engines, performance-wise, there's no difference whether we take a property from an object or its prototype. They remember where the property was found and reuse it in the next request.
|
||||
|
||||
For instance, for `pockets.glasses` they remember where they found `glasses` (in `head`), and next time will search right there. They are also smart enough to update internal caches if something changes, so that optimization is safe.
|
|
@ -0,0 +1,33 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Searching algorithm
|
||||
|
||||
We have object:
|
||||
|
||||
```js
|
||||
let head = {
|
||||
glasses: 1
|
||||
};
|
||||
|
||||
let table = {
|
||||
pen: 3
|
||||
};
|
||||
|
||||
let bed = {
|
||||
sheet: 1,
|
||||
pillow: 2
|
||||
};
|
||||
|
||||
let pockets = {
|
||||
money: 2000
|
||||
};
|
||||
```
|
||||
|
||||
The task has two parts:
|
||||
|
||||
1. Use `__proto__` to assign prototypes in a way that any property lookup will follow the path: `pockets -> bed -> table -> head`.
|
||||
|
||||
For instance, `pockets.pen` should be `3` (found in `table`), and `bed.glasses` should be `1` (found in `head`).
|
||||
2. Answer the question: is it faster to get `glasses` as `pocket.glasses` or `head.glasses`? Benchmark if needed.
|
|
@ -0,0 +1,5 @@
|
|||
**The answer: `rabbit`.**
|
||||
|
||||
That's because `this` is an object before the dot, so `rabbit.eat()` naturally means `rabbit`.
|
||||
|
||||
Property lookup and execution are two successive things. The method is found in the prototype, but then is run in the context of `rabbit`.
|
|
@ -0,0 +1,24 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Where it writes?
|
||||
|
||||
We have `rabbit` inheriting from `animal`.
|
||||
|
||||
If we call `rabbit.eat()`, which object receives `full`: `animal` or `rabbit`?
|
||||
|
||||
```js
|
||||
let animal = {
|
||||
eat() {
|
||||
this.full = true;
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
__proto__: animal
|
||||
};
|
||||
|
||||
rabbit.eat();
|
||||
```
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
Let's look carefully at what's going on in the call `speedy.eat("apple")`.
|
||||
|
||||
1. The method `speedy.eat` is found in the prototype (`hamster`), but called in the context of `speedy`. So the value of `this` is correct.
|
||||
|
||||
2. Then `this.stomach.push()` needs to find `stomach` property and call `push` on it. It looks for `stomach` in `this` (`=speedy`), but nothing found.
|
||||
|
||||
3. Then it follows the prototype chain and finds `stomach` in `hamster`.
|
||||
|
||||
4. Then it calls `push` on it, adding the food into *the stomach of the prototype*.
|
||||
|
||||
It turns out that all hamsters share a single stomach!
|
||||
|
||||
Please note that it would not happen in case of a simple assignment `this.stomach=`:
|
||||
|
||||
```js run
|
||||
let hamster = {
|
||||
stomach: [],
|
||||
|
||||
eat(food) {
|
||||
*!*
|
||||
// assign to this.stomach instead of this.stomach.push
|
||||
this.stomach = [food];
|
||||
*/!*
|
||||
}
|
||||
};
|
||||
|
||||
let speedy = { __proto__: hamster };
|
||||
|
||||
let lazy = { __proto__: hamster };
|
||||
|
||||
// Speedy one found the food
|
||||
speedy.eat("apple");
|
||||
alert( speedy.stomach ); // apple
|
||||
|
||||
// Lazy one's stomach is empty
|
||||
alert( lazy.stomach ); // <nothing>
|
||||
```
|
||||
|
||||
Now all works fine, because `this.stomach=` does not perform a lookup of `stomach`. The value is written directly into `this` object. And for a method call `this.stomach.push`, the object is to be found first (in the prototype), then called, that's the difference.
|
||||
|
||||
But more often, we can totally evade the problem by making sure that each hamster has his own stomach, explicitly:
|
||||
|
||||
```js run
|
||||
let hamster = {
|
||||
stomach: [],
|
||||
|
||||
eat(food) {
|
||||
this.stomach.push(food);
|
||||
}
|
||||
};
|
||||
|
||||
let speedy = {
|
||||
__proto__: hamster,
|
||||
*!*
|
||||
stomach: []
|
||||
*/!*
|
||||
};
|
||||
|
||||
let lazy = {
|
||||
__proto__: hamster,
|
||||
*!*
|
||||
stomach: []
|
||||
*/!*
|
||||
};
|
||||
|
||||
// Speedy one found the food
|
||||
speedy.eat("apple");
|
||||
alert( speedy.stomach ); // apple
|
||||
|
||||
// Lazy one's stomach is empty
|
||||
alert( lazy.stomach ); // <nothing>
|
||||
```
|
||||
|
||||
As a common solution, all object properties, like `stomach` above, are usually written into each object. That prevents such problems. From the other hand, methods and primives can safely stay in prototypes.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Why two hamsters are full?
|
||||
|
||||
We have two hamsters: `speedy` and `lazy` inheriting from the general `hamster` object.
|
||||
|
||||
When we feed one of them, the other one is also full. Why? How to fix it?
|
||||
|
||||
```js run
|
||||
let hamster = {
|
||||
stomach: [],
|
||||
|
||||
eat(food) {
|
||||
this.stomach.push(food);
|
||||
}
|
||||
};
|
||||
|
||||
let speedy = {
|
||||
__proto__: hamster
|
||||
};
|
||||
|
||||
let lazy = {
|
||||
__proto__: hamster
|
||||
};
|
||||
|
||||
// This one found the food
|
||||
speedy.eat("apple");
|
||||
alert( speedy.stomach ); // apple
|
||||
|
||||
// This one also has it, why? fix please.
|
||||
alert( lazy.stomach ); // apple
|
||||
```
|
||||
|
227
1-js/9-object-inheritance/01-prototype/article.md
Normal file
|
@ -0,0 +1,227 @@
|
|||
# Object prototype
|
||||
|
||||
In programming, we always make more complex things basing on what we already have.
|
||||
|
||||
*Inheritance* is a language feature that helps in that.
|
||||
|
||||
For instance, we have a `user` object with its properties and methods, and want to make `admin` and `guest` as slightly modified versions of it. We'd like to reuse what we have in `user`, not copy/reimplement its methods, just build a new object on top of it.
|
||||
|
||||
[cut]
|
||||
|
||||
## [[Prototype]]
|
||||
|
||||
In Javascript, objects have a special hidden property `[[Prototype]]`, that is either `null` or references another object. The object referenced by `[[Prototype]]` is called "a prototype". In the code we can use `__proto__` to set it.
|
||||
|
||||
If a property is missing in the object, Javascript automatically searches it in the prototype.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true
|
||||
};
|
||||
let rabbit = {
|
||||
jumps: true
|
||||
};
|
||||
|
||||
*!*
|
||||
rabbit.__proto__ = animal;
|
||||
*/!*
|
||||
|
||||
// we can find both properties in rabbit now:
|
||||
*!*
|
||||
alert( rabbit.eats ); // true
|
||||
*/!*
|
||||
alert( rabbit.jumps ); // true
|
||||
```
|
||||
|
||||
Here `__proto__` sets `animal` to be a prototype of `rabbit`.
|
||||
|
||||
When `alert` tries to read property `rabbit.eats`, it can find it `rabbit`, so it follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up):
|
||||
|
||||

|
||||
|
||||
Here we can say that "`animal` is the prototype of `rabbit`" or "`rabbit` prototypally inherits from `animal`".
|
||||
|
||||
So if `animal` has a lot of useful properties and methods, then they become automatically available in `rabbit`. Such properties are called "inherited".
|
||||
|
||||
Here's an example with an inherited method:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true,
|
||||
*!*
|
||||
walk() {
|
||||
alert("Animal walk");
|
||||
}
|
||||
*/!*
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
jumps: true,
|
||||
__proto__: animal
|
||||
};
|
||||
|
||||
// walk is taken from the prototype
|
||||
*!*
|
||||
rabbit.walk(); // Animal walk
|
||||
*/!*
|
||||
```
|
||||
|
||||
The method is automatically taken from the prototype, like this:
|
||||
|
||||

|
||||
|
||||
The prototype chain can be longer:
|
||||
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true,
|
||||
walk() {
|
||||
alert("Animal walk");
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
jumps: true,
|
||||
__proto__: animal
|
||||
};
|
||||
|
||||
let longEar = {
|
||||
earLength: 10,
|
||||
__proto__: rabbit
|
||||
}
|
||||
|
||||
// walk is taken from the prototype chain
|
||||
longEar.walk(); // Animal walk
|
||||
alert(longEar.jumps); // true (from rabbit)
|
||||
```
|
||||
|
||||

|
||||
|
||||
There are actually only two limitations:
|
||||
|
||||
1. The references can't go in circles. Javascript will throw an error if you try to assign `__proto__` like that.
|
||||
2. The value of `__proto__` can be either an object or `null`. All other values are ignored.
|
||||
|
||||
Also it may be obvious, but still: there can be only one `[[Prototype]]`. An object may not inherit from two others.
|
||||
|
||||
## Read/write rules
|
||||
|
||||
The prototype is only used for reading properties, write/delete works directly with the object.
|
||||
|
||||
In the example below, we assign our own `walk` method to `rabbit`:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true,
|
||||
walk() { /* unused by rabbit, because (see below) it has its own */ }
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
__proto__: animal
|
||||
}
|
||||
|
||||
*!*
|
||||
rabbit.walk = function() {
|
||||
alert("Rabbit! Bounce-bounce!");
|
||||
};
|
||||
*/!*
|
||||
|
||||
rabbit.walk(); // Rabbit! Bounce-bounce!
|
||||
```
|
||||
|
||||
The value `rabbit.walk` is assigned directly into the object.
|
||||
|
||||
Since now, `rabbit.walk()` call finds the method immediately in the object, it doesn't use the prototype:
|
||||
|
||||

|
||||
|
||||
|
||||
The only exception from that rule are property setters. If there's a setter in the prototype, it is called instead of blind writing a value into the object.
|
||||
|
||||
For instance, here `admin.fullName` is an accessor property, with the setter in the prototype:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith",
|
||||
|
||||
*!*
|
||||
set fullName(value) {
|
||||
[this.name, this.surname] = value.split(" ");
|
||||
}
|
||||
*/!*
|
||||
};
|
||||
|
||||
let admin = {
|
||||
__proto__: user,
|
||||
isAdmin: true
|
||||
};
|
||||
|
||||
// setter triggers!
|
||||
*!*
|
||||
admin.fullName = "Alice Cooper";
|
||||
*/!*
|
||||
|
||||
alert(admin.name); // Alice
|
||||
alert(admin.surname); // Cooper
|
||||
```
|
||||
|
||||
Such behavior is somewhat natural, because an assignment `admin.fullName=` is essentially a method call.
|
||||
|
||||
So, the more precise rule would be:
|
||||
|
||||
1. If an assigned property has a setter in the prototype, then use it.
|
||||
2. Otherwise assign directly to the object.
|
||||
|
||||
## The value of "this"
|
||||
|
||||
An interesting question may arise in the example above: what's the value of `this` inside `set fullName`? Where the properties `this.name` and `this.surname` are written: `user` or `admin`?
|
||||
|
||||
The answer is simple: `this` is not affected by prototypes at all.
|
||||
|
||||
**No matter where a method is found: in an object or its prototype. In a method call, `this` is always the object before the dot.**
|
||||
|
||||
So, the setter actually uses `admin` as `this`, not `user`.
|
||||
|
||||
That is actually a super-important thing, because we may have a big object with many methods and inherit from it. Then we can run its methods on inherited objects and they will modify the state of these objects, not the big one.
|
||||
|
||||
For instance, here `animal` represents a "method storage", and `rabbit` makes use of it.
|
||||
|
||||
The call `rabbit.sleep()` modifies only `rabbit` object:
|
||||
|
||||
```js run
|
||||
// animal has methods
|
||||
let animal = {
|
||||
walk() {
|
||||
if (!this.isSleeping) {
|
||||
alert(`I walk`);
|
||||
}
|
||||
},
|
||||
sleep() {
|
||||
this.isSleeping = true;
|
||||
}
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
name: "White Rabbit"
|
||||
__proto__: animal
|
||||
};
|
||||
|
||||
// modifies rabbit.isSleeping
|
||||
rabbit.sleep();
|
||||
|
||||
rabbit.walk(); // does nothing (is sleeping)
|
||||
```
|
||||
|
||||
The resulting picture:
|
||||
|
||||

|
||||
|
||||
If we had other objects like `bird`, `snake` etc inheriting from `animal`, they would also gain access to its methods. But `this` in each method would be the corresponding object, not `animal`.
|
||||
|
||||
In other words, methods will be shared, but the state will be not.
|
||||
|
||||
## Summary [todo]
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 22 KiB |
BIN
1-js/9-object-inheritance/01-prototype/proto-animal-rabbit.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 18 KiB |
BIN
1-js/9-object-inheritance/01-prototype/proto-user-admin.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
1-js/9-object-inheritance/01-prototype/proto-user-admin@2x.png
Normal file
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1,9 @@
|
|||
Результат: `true`, из прототипа
|
||||
|
||||
Результат: `true`. Свойство `prototype` всего лишь задаёт `__proto__` у новых объектов. Так что его изменение не повлияет на `rabbit.__proto__`. Свойство `eats` будет получено из прототипа.
|
||||
|
||||
Результат: `false`. Свойство `Rabbit.prototype` и `rabbit.__proto__` указывают на один и тот же объект. В данном случае изменения вносятся в сам объект.
|
||||
|
||||
Результат: `true`, так как `delete rabbit.eats` попытается удалить `eats` из `rabbit`, где его и так нет. А чтение в `alert` произойдёт из прототипа.
|
||||
|
||||
Результат: `undefined`. Удаление осуществляется из самого прототипа, поэтому свойство `rabbit.eats` больше взять неоткуда.
|
|
@ -0,0 +1,91 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Прототип после создания
|
||||
|
||||
В примерах ниже создаётся объект `new Rabbit`, а затем проводятся различные действия с `prototype`.
|
||||
|
||||
Каковы будут результаты выполнения? Почему?
|
||||
|
||||
Начнём с этого кода. Что он выведет?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
Добавили строку (выделена), что будет теперь?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = {};
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
А если код будет такой? (заменена одна строка):
|
||||
|
||||
```js
|
||||
function Rabbit(name) {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
Rabbit.prototype.eats = false;
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
А такой? (заменена одна строка)
|
||||
|
||||
```js
|
||||
function Rabbit(name) {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
delete rabbit.eats; // (*)
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
И последний вариант:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
delete Rabbit.prototype.eats; // (*)
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
Можно прототипно унаследовать от `options` и добавлять/менять опции в наследнике:
|
||||
|
||||
```js run
|
||||
function Menu(options) {
|
||||
options = Object.create(options);
|
||||
options.width = options.width || 300;
|
||||
|
||||
alert( options.width ); // возьмёт width из наследника
|
||||
alert( options.height ); // возьмёт height из исходного объекта
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Все изменения будут происходить не в самом `options`, а в его наследнике, при этом исходный объект останется незатронутым.
|
|
@ -0,0 +1,29 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Аргументы по умолчанию
|
||||
|
||||
Есть функция `Menu`, которая получает аргументы в виде объекта `options`:
|
||||
|
||||
```js
|
||||
/* options содержит настройки меню: width, height и т.п. */
|
||||
function Menu(options) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте `options`:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
options.width = options.width || 300; // по умолчанию ширина 300
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
...Но такие изменения могут привести к непредвиденным результатам, т.к. объект `options` может быть повторно использован во внешнем коде. Он передается в `Menu` для того, чтобы параметры из него читали, а не писали.
|
||||
|
||||
Один из способов безопасно назначить значения по умолчанию -- скопировать все свойства `options` в локальные переменные и затем уже менять. Другой способ -- клонировать `options` путём копирования всех свойств из него в новый объект, который уже изменяется.
|
||||
|
||||
При помощи наследования и `Object.create` предложите третий способ, который позволяет избежать копирования объекта и не требует новых переменных.
|
|
@ -0,0 +1,30 @@
|
|||
# Разница между вызовами
|
||||
|
||||
Первый вызов ставит `this == rabbit`, остальные ставят `this` равным `Rabbit.prototype`, следуя правилу "`this` -- объект перед точкой".
|
||||
|
||||
Так что только первый вызов выведет `Rabbit`, в остальных он будет `undefined`.
|
||||
|
||||
Код для проверки:
|
||||
|
||||
```js run
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype.sayHi = function() {
|
||||
alert( this.name );
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Rabbit");
|
||||
|
||||
rabbit.sayHi();
|
||||
Rabbit.prototype.sayHi();
|
||||
Object.getPrototypeOf(rabbit).sayHi();
|
||||
rabbit.__proto__.sayHi();
|
||||
```
|
||||
|
||||
# Совместимость
|
||||
|
||||
1. Первый вызов работает везде.
|
||||
2. Второй вызов работает везде.
|
||||
3. Третий вызов не будет работать в IE8-, там нет метода `getPrototypeOf`
|
||||
4. Четвёртый вызов -- самый "несовместимый", он не будет работать в IE10-, ввиду отсутствия свойства `__proto__`.
|
|
@ -0,0 +1,29 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Есть ли разница между вызовами?
|
||||
|
||||
Создадим новый объект, вот такой:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype.sayHi = function() {
|
||||
alert( this.name );
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Rabbit");
|
||||
```
|
||||
|
||||
Одинаково ли сработают эти вызовы?
|
||||
|
||||
```js
|
||||
rabbit.sayHi();
|
||||
Rabbit.prototype.sayHi();
|
||||
Object.getPrototypeOf(rabbit).sayHi();
|
||||
rabbit.__proto__.sayHi();
|
||||
```
|
||||
|
||||
Все ли они являются кросс-браузерными? Если нет -- в каких браузерах сработает каждый?
|
|
@ -0,0 +1,41 @@
|
|||
Да, можем, но только если уверены, что кто-то позаботился о том, чтобы значение `constructor` было верным.
|
||||
|
||||
В частности, без вмешательства в прототип код точно работает, например:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
var obj = new User('Вася');
|
||||
var obj2 = new obj.constructor('Петя');
|
||||
|
||||
alert( obj2.name ); // Петя (сработало)
|
||||
```
|
||||
|
||||
Сработало, так как `User.prototype.constructor == User`.
|
||||
|
||||
Но если кто-то, к примеру, перезапишет `User.prototype` и забудет указать `constructor`, то такой фокус не пройдёт, например:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
*!*
|
||||
User.prototype = {}; // (*)
|
||||
*/!*
|
||||
|
||||
var obj = new User('Вася');
|
||||
var obj2 = new obj.constructor('Петя');
|
||||
|
||||
alert( obj2.name ); // undefined
|
||||
```
|
||||
|
||||
Почему obj2.name равен undefined? Вот как это работает:
|
||||
|
||||
1. При вызове new `obj.constructor('Петя')`, `obj` ищет у себя свойство `constructor` -- не находит.
|
||||
2. Обращается к своему свойству `__proto__`, которое ведёт к прототипу.
|
||||
3. Прототипом будет (*), пустой объект.
|
||||
4. Далее здесь также ищется свойство constructor -- его нет.
|
||||
5. Где ищем дальше? Правильно -- у следующего прототипа выше, а им будет `Object.prototype`.
|
||||
6. Свойство `Object.prototype.constructor` существует, это встроенный конструктор объектов, который, вообще говоря, не предназначен для вызова с аргументом-строкой, поэтому создаст совсем не то, что ожидается, но то же самое, что вызов `new Object('Петя')`, и у такого объекта не будет `name`.
|
|
@ -0,0 +1,15 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Создать объект тем же конструктором
|
||||
|
||||
Пусть у нас есть произвольный объект `obj`, созданный каким-то конструктором, каким -- мы не знаем, но хотели бы создать новый объект с его помощью.
|
||||
|
||||
Сможем ли мы сделать так?
|
||||
|
||||
```js
|
||||
var obj2 = new obj.constructor();
|
||||
```
|
||||
|
||||
Приведите пример конструкторов для `obj`, при которых такой код будет работать верно -- и неверно.
|
520
1-js/9-object-inheritance/02-function-prototype/article.md
Normal file
|
@ -0,0 +1,520 @@
|
|||
# Managing prototype: the history
|
||||
|
||||
In modern Javascript there are many ways to manipulate object prototype. But it wasn't like that all the time.
|
||||
|
||||
Let's swim a little bit with the flow of history to understand these ways and internals of Javascript.
|
||||
|
||||
## Old times: F.prototype
|
||||
|
||||
JavaScript has prototypal inheritance from the beginning. It was one of the core features of the language.
|
||||
|
||||
But in the old times, the `[[Prototype]]` property was internal. There was no way to read or modify it.
|
||||
|
||||
There was only way to set it: `prototype` property on a constructor function. And it still works.
|
||||
|
||||
**When `new F` is called, `[[Prototype]]` of the new object is set to `F.prototype`.**
|
||||
|
||||
Here `F.prototype` means a regular property named `"prototype"`, we didn't use it before.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = animal;
|
||||
*/!*
|
||||
|
||||
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
|
||||
|
||||
alert( rabbit.eats ); // true
|
||||
```
|
||||
|
||||
Setting `Rabbit.prototype = animal` literally means the following: "When a `new Rabbit` is created, assign its prototype to `animal`".
|
||||
|
||||
Here's the picture:
|
||||
|
||||

|
||||
|
||||
To make it clear: `prototype` is not `[[Prototype]]`. They look similar, but they are totally different. The value of `Rabbit.prototype` is used to set `[[Prototype]]`. That's all. Afterwards, the inhertiance mechanism uses `[[Prototype]]` only.
|
||||
|
||||
```smart header="`prototype` is only \"magical\" for constructors"
|
||||
We can create a property named `"prototype"` in any object. But the special behavior only happens if it's assigned to a constructor function.
|
||||
|
||||
And even if we assign it to a constructor function, it does nothing until we call it with `new`. And only then it will be used to set the prototype for a new object.
|
||||
```
|
||||
|
||||
```warn header="The value of `prototype` should be an object or null"
|
||||
Technically, we can assign anything to `F.prototype`. That's a regular property.
|
||||
|
||||
But the `new` operator will only use it to set `[[Prototype]]` if it's an object or null. Any other value like a string or a number will be ignored.
|
||||
```
|
||||
|
||||
## Native prototypes
|
||||
|
||||
|
||||
|
||||
# Встроенные "классы" в JavaScript
|
||||
|
||||
В JavaScript есть встроенные объекты: `Date`, `Array`, `Object` и другие. Они используют прототипы и демонстрируют организацию "псевдоклассов" на JavaScript, которую мы вполне можем применить и для себя.
|
||||
|
||||
[cut]
|
||||
|
||||
## Откуда методы у {} ?
|
||||
|
||||
Начнём мы с того, что создадим пустой объект и выведем его.
|
||||
|
||||
```js run
|
||||
var obj = {};
|
||||
alert( obj ); // "[object Object]" ?
|
||||
```
|
||||
|
||||
Где код, который генерирует строковое представление для `alert(obj)`? Объект-то ведь пустой.
|
||||
|
||||
## Object.prototype
|
||||
|
||||
...Конечно же, это сделал метод `toString`, который находится... Конечно, не в самом объекте (он пуст), а в его прототипе `obj.__proto__`, можно его даже вывести:
|
||||
|
||||
```js run
|
||||
alert( {}.__proto__.toString ); // function toString
|
||||
```
|
||||
|
||||
Откуда новый объект `obj` получает такой `__proto__`?
|
||||
|
||||
1. Запись `obj = {}` является краткой формой `obj = new Object`, где `Object` -- встроенная функция-конструктор для объектов.
|
||||
2. При выполнении `new Object`, создаваемому объекту ставится `__proto__` по `prototype` конструктора, который в данном случае равен встроенному `Object.prototype`.
|
||||
3. В дальнейшем при обращении к `obj.toString()` -- функция будет взята из `Object.prototype`.
|
||||
|
||||

|
||||
|
||||
Это можно легко проверить:
|
||||
|
||||
```js run
|
||||
var obj = {};
|
||||
|
||||
// метод берётся из прототипа?
|
||||
alert( obj.toString == Object.prototype.toString ); // true, да
|
||||
|
||||
// проверим, правда ли что __proto__ это Object.prototype?
|
||||
alert( obj.__proto__ == Object.prototype ); // true
|
||||
|
||||
// А есть ли __proto__ у Object.prototype?
|
||||
alert( obj.__proto__.__proto__ ); // null, нет
|
||||
```
|
||||
|
||||
## Встроенные "классы" в JavaScript
|
||||
|
||||
Точно такой же подход используется в массивах `Array`, функциях `Function` и других объектах. Встроенные методы для них находятся в `Array.prototype`, `Function.prototype` и т.п.
|
||||
|
||||

|
||||
|
||||
Например, когда мы создаём массив, `[1, 2, 3]`, то это альтернативный вариант синтаксиса `new Array`, так что у массивов есть стандартный прототип `Array.prototype`.
|
||||
|
||||
Но в нём есть методы лишь для массивов, а для общих методов всех объектов есть ссылка `Array.prototype.__proto__`, равная `Object.prototype`.
|
||||
|
||||
Аналогично, для функций.
|
||||
|
||||
Лишь для чисел (как и других примитивов) всё немного иначе, но об этом чуть далее.
|
||||
|
||||
Объект `Object.prototype` -- вершина иерархии, единственный, у которого `__proto__` равно `null`.
|
||||
|
||||
**Поэтому говорят, что "все объекты наследуют от `Object`", а если более точно, то от `Object.prototype`.**
|
||||
|
||||
"Псевдоклассом" или, более коротко, "классом", называют функцию-конструктор вместе с её `prototype`. Такой способ объявления классов называют "прототипным стилем ООП".
|
||||
|
||||
При наследовании часть методов переопределяется, например, у массива `Array` есть свой `toString`, который выводит элементы массива через запятую:
|
||||
|
||||
```js run
|
||||
var arr = [1, 2, 3]
|
||||
alert( arr ); // 1,2,3 <-- результат Array.prototype.toString
|
||||
```
|
||||
|
||||
Как мы видели раньше, у `Object.prototype` есть свой `toString`, но так как в `Array.prototype` он ищется первым, то берётся именно вариант для массивов:
|
||||
|
||||

|
||||
|
||||
````smart header="Вызов методов через `call` и `apply` из прототипа"
|
||||
Ранее мы говорили о применении методов массивов к "псевдомассивам", например, можно использовать `[].join` для `arguments`:
|
||||
|
||||
```js run
|
||||
function showList() {
|
||||
*!*
|
||||
alert( [].join.call(arguments, " - ") );
|
||||
*/!*
|
||||
}
|
||||
|
||||
showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша
|
||||
```
|
||||
|
||||
Так как метод `join` находится в `Array.prototype`, то можно вызвать его оттуда напрямую, вот так:
|
||||
|
||||
```js run
|
||||
function showList() {
|
||||
*!*
|
||||
alert( Array.prototype.join.call(arguments, " - ") );
|
||||
*/!*
|
||||
}
|
||||
|
||||
showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша
|
||||
```
|
||||
|
||||
Это эффективнее, потому что не создаётся лишний объект массива `[]`, хотя, с другой стороны -- больше букв писать.
|
||||
````
|
||||
|
||||
## Примитивы
|
||||
|
||||
Примитивы не являются объектами, но методы берут из соответствующих прототипов: `Number.prototype`, `Boolean.prototype`, `String.prototype`.
|
||||
|
||||
По стандарту, если обратиться к свойству числа, строки или логического значения, то будет создан объект соответствующего типа, например `new String` для строки, `new Number` для чисел, `new Boolean` -- для логических выражений.
|
||||
|
||||
Далее будет произведена операция со свойством или вызов метода по обычным правилам, с поиском в прототипе, а затем этот объект будет уничтожен.
|
||||
|
||||
Именно так работает код ниже:
|
||||
|
||||
```js run
|
||||
var user = "Вася"; // создали строку (примитив)
|
||||
|
||||
*!*
|
||||
alert( user.toUpperCase() ); // ВАСЯ
|
||||
// был создан временный объект new String
|
||||
// вызван метод
|
||||
// new String уничтожен, результат возвращён
|
||||
*/!*
|
||||
```
|
||||
|
||||
Можно даже попробовать записать в этот временный объект свойство:
|
||||
|
||||
```js run
|
||||
// попытаемся записать свойство в строку:
|
||||
var user = "Вася";
|
||||
user.age = 30;
|
||||
|
||||
*!*
|
||||
alert( user.age ); // undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
Свойство `age` было записано во временный объект, который был тут же уничтожен, так что смысла в такой записи немного.
|
||||
|
||||
````warn header="Конструкторы `String/Number/Boolean` -- только для внутреннего использования"
|
||||
Технически, можно создавать объекты для примитивов и вручную, например `new Number`. Но в ряде случаев получится откровенно бредовое поведение. Например:
|
||||
|
||||
```js run
|
||||
alert( typeof 1 ); // "number"
|
||||
|
||||
alert( typeof new Number(1) ); // "object" ?!?
|
||||
```
|
||||
|
||||
Или, ещё страннее:
|
||||
|
||||
```js run
|
||||
var zero = new Number(0);
|
||||
|
||||
if (zero) { // объект - true, так что alert выполнится
|
||||
alert( "число ноль -- true?!?" );
|
||||
}
|
||||
```
|
||||
|
||||
Поэтому в явном виде `new String`, `new Number` и `new Boolean` никогда не вызываются.
|
||||
````
|
||||
|
||||
```warn header="Значения `null` и `undefined` не имеют свойств"
|
||||
Значения `null` и `undefined` стоят особняком. Вышесказанное к ним не относится.
|
||||
|
||||
Для них нет соответствующих классов, в них нельзя записать свойство (будет ошибка), в общем, на конкурсе "самое примитивное значение" они точно разделили бы первое место.
|
||||
```
|
||||
|
||||
## Изменение встроенных прототипов [#native-prototype-change]
|
||||
|
||||
Встроенные прототипы можно изменять. В том числе -- добавлять свои методы.
|
||||
|
||||
Мы можем написать метод для многократного повторения строки, и он тут же станет доступным для всех строк:
|
||||
|
||||
```js run
|
||||
String.prototype.repeat = function(times) {
|
||||
return new Array(times + 1).join(this);
|
||||
};
|
||||
|
||||
alert( "ля".repeat(3) ); // ляляля
|
||||
```
|
||||
|
||||
Аналогично мы могли бы создать метод `Object.prototype.each(func)`, который будет применять `func` к каждому свойству:
|
||||
|
||||
```js run
|
||||
Object.prototype.each = function(f) {
|
||||
for (var prop in this) {
|
||||
var value = this[prop];
|
||||
f.call(value, prop, value); // вызовет f(prop, value), this=value
|
||||
}
|
||||
}
|
||||
|
||||
// Попробуем! (внимание, пока что это работает неверно!)
|
||||
var user = {
|
||||
name: 'Вася',
|
||||
age: 25
|
||||
};
|
||||
|
||||
user.each(function(prop, val) {
|
||||
alert( prop ); // name -> age -> (!) each
|
||||
});
|
||||
```
|
||||
|
||||
Обратите внимание -- пример выше работает не совсем корректно. Вместе со свойствами объекта `user` он выводит и наше свойство `each`. Технически, это правильно, так как цикл `for..in` перебирает свойства и в прототипе тоже, но не очень удобно.
|
||||
|
||||
Конечно, это легко поправить добавлением проверки `hasOwnProperty`:
|
||||
|
||||
```js run
|
||||
Object.prototype.each = function(f) {
|
||||
|
||||
for (var prop in this) {
|
||||
|
||||
*!*
|
||||
// пропускать свойства из прототипа
|
||||
if (!this.hasOwnProperty(prop)) continue;
|
||||
*/!*
|
||||
|
||||
var value = this[prop];
|
||||
f.call(value, prop, value);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Теперь все будет в порядке
|
||||
var obj = {
|
||||
name: 'Вася',
|
||||
age: 25
|
||||
};
|
||||
|
||||
obj.each(function(prop, val) {
|
||||
alert( prop ); // name -> age
|
||||
});
|
||||
```
|
||||
|
||||
Здесь это сработало, теперь код работает верно. Но мы же не хотим добавлять `hasOwnProperty` в цикл по любому объекту! Поэтому либо не добавляйте свойства в `Object.prototype`, либо можно использовать [дескриптор свойства](/descriptors-getters-setters) и флаг `enumerable`.
|
||||
|
||||
Это, конечно, не будет работать в IE8-:
|
||||
|
||||
```js run
|
||||
Object.prototype.each = function(f) {
|
||||
|
||||
for (var prop in this) {
|
||||
var value = this[prop];
|
||||
f.call(value, prop, value);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
*!*
|
||||
// поправить объявление свойства, установив флаг enumerable: false
|
||||
Object.defineProperty(Object.prototype, 'each', {
|
||||
enumerable: false
|
||||
});
|
||||
*/!*
|
||||
|
||||
// Теперь все будет в порядке
|
||||
var obj = {
|
||||
name: 'Вася',
|
||||
age: 25
|
||||
};
|
||||
|
||||
obj.each(function(prop, val) {
|
||||
alert( prop ); // name -> age
|
||||
});
|
||||
```
|
||||
|
||||
Есть несколько "за" и "против" модификации встроенных прототипов:
|
||||
|
||||
```compare
|
||||
+ Методы в прототипе автоматически доступны везде, их вызов прост и красив.
|
||||
- Новые свойства, добавленные в прототип из разных мест, могут конфликтовать между собой. Представьте, что вы подключили две библиотеки, которые добавили одно и то же свойство в прототип, но определили его по-разному. Конфликт неизбежен.
|
||||
- Изменения встроенных прототипов влияют глобально, на все-все скрипты, делать их не очень хорошо с архитектурной точки зрения.
|
||||
```
|
||||
|
||||
Как правило, минусы весомее, но есть одно исключение, когда изменения встроенных прототипов не только разрешены, но и приветствуются.
|
||||
|
||||
**Допустимо изменение прототипа встроенных объектов, которое добавляет поддержку метода из современных стандартов в те браузеры, где её пока нет.**
|
||||
|
||||
Например, добавим `Object.create(proto)` в старые браузеры:
|
||||
|
||||
```js
|
||||
if (!Object.create) {
|
||||
|
||||
Object.create = function(proto) {
|
||||
function F() {}
|
||||
F.prototype = proto;
|
||||
return new F;
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Именно так работает библиотека [es5-shim](https://github.com/kriskowal/es5-shim), которая предоставляет многие функции современного JavaScript для старых браузеров. Они добавляются во встроенные объекты и их прототипы.
|
||||
|
||||
## Итого
|
||||
|
||||
- Методы встроенных объектов хранятся в их прототипах.
|
||||
- Встроенные прототипы можно расширить или поменять.
|
||||
- Добавление методов в `Object.prototype`, если оно не сопровождается `Object.defineProperty` с установкой `enumerable` (IE9+), "сломает" циклы `for..in`, поэтому стараются в этот прототип методы не добавлять.
|
||||
|
||||
Другие прототипы изменять менее опасно, но все же не рекомендуется во избежание конфликтов.
|
||||
|
||||
Отдельно стоит изменение с целью добавления современных методов в старые браузеры, таких как <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create">Object.create</a>, <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys">Object.keys</a>, <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind">Function.prototype.bind</a> и т.п. Это допустимо и как раз делается [es5-shim](https://github.com/kriskowal/es5-shim).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Свойство constructor
|
||||
|
||||
У каждой функции по умолчанию уже есть свойство `prototype`.
|
||||
|
||||
Оно содержит объект такого вида:
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
|
||||
Rabbit.prototype = {
|
||||
constructor: Rabbit
|
||||
};
|
||||
```
|
||||
|
||||
В коде выше я создал `Rabbit.prototype` вручную, но ровно такой же -- генерируется автоматически.
|
||||
|
||||
Проверим:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
|
||||
// в Rabbit.prototype есть одно свойство: constructor
|
||||
alert( Object.getOwnPropertyNames(Rabbit.prototype) ); // constructor
|
||||
|
||||
// оно равно Rabbit
|
||||
alert( Rabbit.prototype.constructor == Rabbit ); // true
|
||||
```
|
||||
|
||||
Можно его использовать для создания объекта с тем же конструктором, что и данный:
|
||||
|
||||
```js run
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
alert( name );
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Кроль");
|
||||
|
||||
var rabbit2 = new rabbit.constructor("Крольчиха");
|
||||
```
|
||||
|
||||
Эта возможность бывает полезна, когда, получив объект, мы не знаем в точности, какой у него был конструктор (например, сделан вне нашего кода), а нужно создать такой же.
|
||||
|
||||
````warn header="Свойство `constructor` легко потерять"
|
||||
JavaScript никак не использует свойство `constructor`. То есть, оно создаётся автоматически, а что с ним происходит дальше -- это уже наша забота. В стандарте прописано только его создание.
|
||||
|
||||
В частности, при перезаписи `Rabbit.prototype = { jumps: true }` свойства `constructor` больше не будет.
|
||||
|
||||
Сам интерпретатор JavaScript его в служебных целях не требует, поэтому в работе объектов ничего не "сломается". Но если мы хотим, чтобы возможность получить конструктор, всё же, была, то можно при перезаписи гарантировать наличие `constructor` вручную:
|
||||
```js
|
||||
Rabbit.prototype = {
|
||||
jumps: true,
|
||||
*!*
|
||||
constructor: Rabbit
|
||||
*/!*
|
||||
};
|
||||
```
|
||||
|
||||
Либо можно поступить аккуратно и добавить свойства к встроенному `prototype` без его замены:
|
||||
```js
|
||||
// сохранится встроенный constructor
|
||||
Rabbit.prototype.jumps = true
|
||||
```
|
||||
````
|
||||
|
||||
## Эмуляция Object.create для IE8- [#inherit]
|
||||
|
||||
Как мы только что видели, с конструкторами всё просто, назначить прототип можно кросс-браузерно при помощи `F.prototype`.
|
||||
|
||||
Теперь небольшое "лирическое отступление" в область совместимости.
|
||||
|
||||
Прямые методы работы с прототипом отсутствуют в старых IE, но один из них -- `Object.create(proto)` можно эмулировать, как раз при помощи `prototype`. И он будет работать везде, даже в самых устаревших браузерах.
|
||||
|
||||
Кросс-браузерный аналог -- назовём его `inherit`, состоит буквально из нескольких строк:
|
||||
|
||||
```js
|
||||
function inherit(proto) {
|
||||
function F() {}
|
||||
F.prototype = proto;
|
||||
var object = new F;
|
||||
return object;
|
||||
}
|
||||
```
|
||||
|
||||
Результат вызова `inherit(animal)` идентичен `Object.create(animal)`. Она создаёт новый пустой объект с прототипом `animal`.
|
||||
|
||||
Например:
|
||||
|
||||
```js run
|
||||
var animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = inherit(animal);
|
||||
|
||||
alert( rabbit.eats ); // true
|
||||
```
|
||||
|
||||
Посмотрите внимательно на функцию `inherit` и вы, наверняка, сами поймёте, как она работает...
|
||||
|
||||
Если где-то неясности, то её построчное описание:
|
||||
|
||||
```js no-beautify
|
||||
function inherit(proto) {
|
||||
function F() {} // (1)
|
||||
F.prototype = proto // (2)
|
||||
var object = new F; // (3)
|
||||
return object; // (4)
|
||||
}
|
||||
```
|
||||
|
||||
1. Создана новая функция `F`. Она ничего не делает с `this`, так что если вызвать `new F`, то получим пустой объект.
|
||||
2. Свойство `F.prototype` устанавливается в будущий прототип `proto`
|
||||
3. Результатом вызова `new F` будет пустой объект с `__proto__` равным значению `F.prototype`.
|
||||
4. Мы получили пустой объект с заданным прототипом, как и хотели. Возвратим его.
|
||||
|
||||
Для унификации можно запустить такой код, и метод `Object.create` станет кросс-браузерным:
|
||||
|
||||
```js
|
||||
if (!Object.create) Object.create = inherit; /* определение inherit - выше */
|
||||
```
|
||||
|
||||
В частности, аналогичным образом работает библиотека [es5-shim](https://github.com/es-shims/es5-shim), при подключении которой `Object.create` станет доступен для всех браузеров.
|
||||
|
||||
## Итого
|
||||
|
||||
Для произвольной функции -- назовём её `Person`, верно следующее:
|
||||
|
||||
- Прототип `__proto__` новых объектов, создаваемых через `new Person`, можно задавать при помощи свойства `Person.prototype`.
|
||||
- Значением `Person.prototype` по умолчанию является объект с единственным свойством `constructor`, содержащим ссылку на `Person`. Его можно использовать, чтобы из самого объекта получить функцию, которая его создала. Однако, JavaScript никак не поддерживает корректность этого свойства, поэтому программист может его изменить или удалить.
|
||||
- Современный метод `Object.create(proto)` можно эмулировать при помощи `prototype`, если хочется, чтобы он работал в IE8-.
|
||||
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,9 @@
|
|||
Результат: `true`, из прототипа
|
||||
|
||||
Результат: `true`. Свойство `prototype` всего лишь задаёт `__proto__` у новых объектов. Так что его изменение не повлияет на `rabbit.__proto__`. Свойство `eats` будет получено из прототипа.
|
||||
|
||||
Результат: `false`. Свойство `Rabbit.prototype` и `rabbit.__proto__` указывают на один и тот же объект. В данном случае изменения вносятся в сам объект.
|
||||
|
||||
Результат: `true`, так как `delete rabbit.eats` попытается удалить `eats` из `rabbit`, где его и так нет. А чтение в `alert` произойдёт из прототипа.
|
||||
|
||||
Результат: `undefined`. Удаление осуществляется из самого прототипа, поэтому свойство `rabbit.eats` больше взять неоткуда.
|
|
@ -0,0 +1,91 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Прототип после создания
|
||||
|
||||
В примерах ниже создаётся объект `new Rabbit`, а затем проводятся различные действия с `prototype`.
|
||||
|
||||
Каковы будут результаты выполнения? Почему?
|
||||
|
||||
Начнём с этого кода. Что он выведет?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
Добавили строку (выделена), что будет теперь?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = {};
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
А если код будет такой? (заменена одна строка):
|
||||
|
||||
```js
|
||||
function Rabbit(name) {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
Rabbit.prototype.eats = false;
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
А такой? (заменена одна строка)
|
||||
|
||||
```js
|
||||
function Rabbit(name) {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
delete rabbit.eats; // (*)
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
||||
И последний вариант:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
delete Rabbit.prototype.eats; // (*)
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats );
|
||||
```
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
Можно прототипно унаследовать от `options` и добавлять/менять опции в наследнике:
|
||||
|
||||
```js run
|
||||
function Menu(options) {
|
||||
options = Object.create(options);
|
||||
options.width = options.width || 300;
|
||||
|
||||
alert( options.width ); // возьмёт width из наследника
|
||||
alert( options.height ); // возьмёт height из исходного объекта
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Все изменения будут происходить не в самом `options`, а в его наследнике, при этом исходный объект останется незатронутым.
|
|
@ -0,0 +1,29 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Аргументы по умолчанию
|
||||
|
||||
Есть функция `Menu`, которая получает аргументы в виде объекта `options`:
|
||||
|
||||
```js
|
||||
/* options содержит настройки меню: width, height и т.п. */
|
||||
function Menu(options) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте `options`:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
options.width = options.width || 300; // по умолчанию ширина 300
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
...Но такие изменения могут привести к непредвиденным результатам, т.к. объект `options` может быть повторно использован во внешнем коде. Он передается в `Menu` для того, чтобы параметры из него читали, а не писали.
|
||||
|
||||
Один из способов безопасно назначить значения по умолчанию -- скопировать все свойства `options` в локальные переменные и затем уже менять. Другой способ -- клонировать `options` путём копирования всех свойств из него в новый объект, который уже изменяется.
|
||||
|
||||
При помощи наследования и `Object.create` предложите третий способ, который позволяет избежать копирования объекта и не требует новых переменных.
|
|
@ -0,0 +1,30 @@
|
|||
# Разница между вызовами
|
||||
|
||||
Первый вызов ставит `this == rabbit`, остальные ставят `this` равным `Rabbit.prototype`, следуя правилу "`this` -- объект перед точкой".
|
||||
|
||||
Так что только первый вызов выведет `Rabbit`, в остальных он будет `undefined`.
|
||||
|
||||
Код для проверки:
|
||||
|
||||
```js run
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype.sayHi = function() {
|
||||
alert( this.name );
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Rabbit");
|
||||
|
||||
rabbit.sayHi();
|
||||
Rabbit.prototype.sayHi();
|
||||
Object.getPrototypeOf(rabbit).sayHi();
|
||||
rabbit.__proto__.sayHi();
|
||||
```
|
||||
|
||||
# Совместимость
|
||||
|
||||
1. Первый вызов работает везде.
|
||||
2. Второй вызов работает везде.
|
||||
3. Третий вызов не будет работать в IE8-, там нет метода `getPrototypeOf`
|
||||
4. Четвёртый вызов -- самый "несовместимый", он не будет работать в IE10-, ввиду отсутствия свойства `__proto__`.
|
|
@ -0,0 +1,29 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Есть ли разница между вызовами?
|
||||
|
||||
Создадим новый объект, вот такой:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype.sayHi = function() {
|
||||
alert( this.name );
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Rabbit");
|
||||
```
|
||||
|
||||
Одинаково ли сработают эти вызовы?
|
||||
|
||||
```js
|
||||
rabbit.sayHi();
|
||||
Rabbit.prototype.sayHi();
|
||||
Object.getPrototypeOf(rabbit).sayHi();
|
||||
rabbit.__proto__.sayHi();
|
||||
```
|
||||
|
||||
Все ли они являются кросс-браузерными? Если нет -- в каких браузерах сработает каждый?
|
|
@ -0,0 +1,41 @@
|
|||
Да, можем, но только если уверены, что кто-то позаботился о том, чтобы значение `constructor` было верным.
|
||||
|
||||
В частности, без вмешательства в прототип код точно работает, например:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
var obj = new User('Вася');
|
||||
var obj2 = new obj.constructor('Петя');
|
||||
|
||||
alert( obj2.name ); // Петя (сработало)
|
||||
```
|
||||
|
||||
Сработало, так как `User.prototype.constructor == User`.
|
||||
|
||||
Но если кто-то, к примеру, перезапишет `User.prototype` и забудет указать `constructor`, то такой фокус не пройдёт, например:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
*!*
|
||||
User.prototype = {}; // (*)
|
||||
*/!*
|
||||
|
||||
var obj = new User('Вася');
|
||||
var obj2 = new obj.constructor('Петя');
|
||||
|
||||
alert( obj2.name ); // undefined
|
||||
```
|
||||
|
||||
Почему obj2.name равен undefined? Вот как это работает:
|
||||
|
||||
1. При вызове new `obj.constructor('Петя')`, `obj` ищет у себя свойство `constructor` -- не находит.
|
||||
2. Обращается к своему свойству `__proto__`, которое ведёт к прототипу.
|
||||
3. Прототипом будет (*), пустой объект.
|
||||
4. Далее здесь также ищется свойство constructor -- его нет.
|
||||
5. Где ищем дальше? Правильно -- у следующего прототипа выше, а им будет `Object.prototype`.
|
||||
6. Свойство `Object.prototype.constructor` существует, это встроенный конструктор объектов, который, вообще говоря, не предназначен для вызова с аргументом-строкой, поэтому создаст совсем не то, что ожидается, но то же самое, что вызов `new Object('Петя')`, и у такого объекта не будет `name`.
|
|
@ -0,0 +1,15 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Создать объект тем же конструктором
|
||||
|
||||
Пусть у нас есть произвольный объект `obj`, созданный каким-то конструктором, каким -- мы не знаем, но хотели бы создать новый объект с его помощью.
|
||||
|
||||
Сможем ли мы сделать так?
|
||||
|
||||
```js
|
||||
var obj2 = new obj.constructor();
|
||||
```
|
||||
|
||||
Приведите пример конструкторов для `obj`, при которых такой код будет работать верно -- и неверно.
|
205
1-js/9-object-inheritance/12-new-prototype/article.md
Normal file
|
@ -0,0 +1,205 @@
|
|||
# F.prototype and new
|
||||
|
||||
До этого момента мы говорили о наследовании объектов, объявленных через `{...}`.
|
||||
|
||||
Но в реальных проектах объекты обычно создаются функцией-конструктором через `new`. Посмотрим, как указать прототип в этом случае.
|
||||
|
||||
[cut]
|
||||
|
||||
## Свойство F.prototype
|
||||
|
||||
Самым очевидным решением является назначение `__proto__` в конструкторе.
|
||||
|
||||
Например, если я хочу, чтобы у всех объектов, которые создаются `new Rabbit`, был прототип `animal`, я могу сделать так:
|
||||
|
||||
```js run
|
||||
var animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
*!*
|
||||
this.__proto__ = animal;
|
||||
*/!*
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Кроль");
|
||||
|
||||
alert( rabbit.eats ); // true, из прототипа
|
||||
```
|
||||
|
||||
Недостаток этого подхода -- он не работает в IE10-.
|
||||
|
||||
К счастью, в JavaScript с древнейших времён существует альтернативный, встроенный в язык и полностью кросс-браузерный способ.
|
||||
|
||||
**Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство `prototype`.**
|
||||
|
||||
**При создании объекта через `new`, в его прототип `__proto__` записывается ссылка из `prototype` функции-конструктора.**
|
||||
|
||||
Например, код ниже полностью аналогичен предыдущему, но работает всегда и везде:
|
||||
|
||||
```js run
|
||||
var animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = animal;
|
||||
*/!*
|
||||
|
||||
var rabbit = new Rabbit("Кроль"); // rabbit.__proto__ == animal
|
||||
|
||||
alert( rabbit.eats ); // true
|
||||
```
|
||||
|
||||
Установка `Rabbit.prototype = animal` буквально говорит интерпретатору следующее: *"При создании объекта через `new Rabbit` запиши ему `__proto__ = animal`".*
|
||||
|
||||
```smart header="Свойство `prototype` имеет смысл только у конструктора"
|
||||
Свойство с именем `prototype` можно указать на любом объекте, но особый смысл оно имеет, лишь если назначено функции-конструктору.
|
||||
|
||||
Само по себе, без вызова оператора `new`, оно вообще ничего не делает, его единственное назначение -- указывать `__proto__` для новых объектов.
|
||||
```
|
||||
|
||||
```warn header="Значением `prototype` может быть только объект"
|
||||
Технически, в это свойство можно записать что угодно.
|
||||
|
||||
Однако, при работе `new`, свойство `prototype` будет использовано лишь в том случае, если это объект. Примитивное значение, такое как число или строка, будет проигнорировано.
|
||||
```
|
||||
|
||||
## Свойство constructor
|
||||
|
||||
У каждой функции по умолчанию уже есть свойство `prototype`.
|
||||
|
||||
Оно содержит объект такого вида:
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
|
||||
Rabbit.prototype = {
|
||||
constructor: Rabbit
|
||||
};
|
||||
```
|
||||
|
||||
В коде выше я создал `Rabbit.prototype` вручную, но ровно такой же -- генерируется автоматически.
|
||||
|
||||
Проверим:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
|
||||
// в Rabbit.prototype есть одно свойство: constructor
|
||||
alert( Object.getOwnPropertyNames(Rabbit.prototype) ); // constructor
|
||||
|
||||
// оно равно Rabbit
|
||||
alert( Rabbit.prototype.constructor == Rabbit ); // true
|
||||
```
|
||||
|
||||
Можно его использовать для создания объекта с тем же конструктором, что и данный:
|
||||
|
||||
```js run
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
alert( name );
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit("Кроль");
|
||||
|
||||
var rabbit2 = new rabbit.constructor("Крольчиха");
|
||||
```
|
||||
|
||||
Эта возможность бывает полезна, когда, получив объект, мы не знаем в точности, какой у него был конструктор (например, сделан вне нашего кода), а нужно создать такой же.
|
||||
|
||||
````warn header="Свойство `constructor` легко потерять"
|
||||
JavaScript никак не использует свойство `constructor`. То есть, оно создаётся автоматически, а что с ним происходит дальше -- это уже наша забота. В стандарте прописано только его создание.
|
||||
|
||||
В частности, при перезаписи `Rabbit.prototype = { jumps: true }` свойства `constructor` больше не будет.
|
||||
|
||||
Сам интерпретатор JavaScript его в служебных целях не требует, поэтому в работе объектов ничего не "сломается". Но если мы хотим, чтобы возможность получить конструктор, всё же, была, то можно при перезаписи гарантировать наличие `constructor` вручную:
|
||||
```js
|
||||
Rabbit.prototype = {
|
||||
jumps: true,
|
||||
*!*
|
||||
constructor: Rabbit
|
||||
*/!*
|
||||
};
|
||||
```
|
||||
|
||||
Либо можно поступить аккуратно и добавить свойства к встроенному `prototype` без его замены:
|
||||
```js
|
||||
// сохранится встроенный constructor
|
||||
Rabbit.prototype.jumps = true
|
||||
```
|
||||
````
|
||||
|
||||
## Эмуляция Object.create для IE8- [#inherit]
|
||||
|
||||
Как мы только что видели, с конструкторами всё просто, назначить прототип можно кросс-браузерно при помощи `F.prototype`.
|
||||
|
||||
Теперь небольшое "лирическое отступление" в область совместимости.
|
||||
|
||||
Прямые методы работы с прототипом отсутствуют в старых IE, но один из них -- `Object.create(proto)` можно эмулировать, как раз при помощи `prototype`. И он будет работать везде, даже в самых устаревших браузерах.
|
||||
|
||||
Кросс-браузерный аналог -- назовём его `inherit`, состоит буквально из нескольких строк:
|
||||
|
||||
```js
|
||||
function inherit(proto) {
|
||||
function F() {}
|
||||
F.prototype = proto;
|
||||
var object = new F;
|
||||
return object;
|
||||
}
|
||||
```
|
||||
|
||||
Результат вызова `inherit(animal)` идентичен `Object.create(animal)`. Она создаёт новый пустой объект с прототипом `animal`.
|
||||
|
||||
Например:
|
||||
|
||||
```js run
|
||||
var animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
var rabbit = inherit(animal);
|
||||
|
||||
alert( rabbit.eats ); // true
|
||||
```
|
||||
|
||||
Посмотрите внимательно на функцию `inherit` и вы, наверняка, сами поймёте, как она работает...
|
||||
|
||||
Если где-то неясности, то её построчное описание:
|
||||
|
||||
```js no-beautify
|
||||
function inherit(proto) {
|
||||
function F() {} // (1)
|
||||
F.prototype = proto // (2)
|
||||
var object = new F; // (3)
|
||||
return object; // (4)
|
||||
}
|
||||
```
|
||||
|
||||
1. Создана новая функция `F`. Она ничего не делает с `this`, так что если вызвать `new F`, то получим пустой объект.
|
||||
2. Свойство `F.prototype` устанавливается в будущий прототип `proto`
|
||||
3. Результатом вызова `new F` будет пустой объект с `__proto__` равным значению `F.prototype`.
|
||||
4. Мы получили пустой объект с заданным прототипом, как и хотели. Возвратим его.
|
||||
|
||||
Для унификации можно запустить такой код, и метод `Object.create` станет кросс-браузерным:
|
||||
|
||||
```js
|
||||
if (!Object.create) Object.create = inherit; /* определение inherit - выше */
|
||||
```
|
||||
|
||||
В частности, аналогичным образом работает библиотека [es5-shim](https://github.com/es-shims/es5-shim), при подключении которой `Object.create` станет доступен для всех браузеров.
|
||||
|
||||
## Итого
|
||||
|
||||
Для произвольной функции -- назовём её `Person`, верно следующее:
|
||||
|
||||
- Прототип `__proto__` новых объектов, создаваемых через `new Person`, можно задавать при помощи свойства `Person.prototype`.
|
||||
- Значением `Person.prototype` по умолчанию является объект с единственным свойством `constructor`, содержащим ссылку на `Person`. Его можно использовать, чтобы из самого объекта получить функцию, которая его создала. Однако, JavaScript никак не поддерживает корректность этого свойства, поэтому программист может его изменить или удалить.
|
||||
- Современный метод `Object.create(proto)` можно эмулировать при помощи `prototype`, если хочется, чтобы он работал в IE8-.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
|
||||
```js run
|
||||
Function.prototype.defer = function(ms) {
|
||||
setTimeout(this, ms);
|
||||
}
|
||||
|
||||
function f() {
|
||||
alert( "привет" );
|
||||
}
|
||||
|
||||
f.defer(1000); // выведет "привет" через 1 секунду
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Добавить функциям defer
|
||||
|
||||
Добавьте всем функциям в прототип метод `defer(ms)`, который откладывает вызов функции на `ms` миллисекунд.
|
||||
|
||||
После этого должен работать такой код:
|
||||
|
||||
```js
|
||||
function f() {
|
||||
alert( "привет" );
|
||||
}
|
||||
|
||||
f.defer(1000); // выведет "привет" через 1 секунду
|
||||
```
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
|
||||
```js run
|
||||
Function.prototype.defer = function(ms) {
|
||||
var f = this;
|
||||
return function() {
|
||||
var args = arguments,
|
||||
context = this;
|
||||
setTimeout(function() {
|
||||
f.apply(context, args);
|
||||
}, ms);
|
||||
}
|
||||
}
|
||||
|
||||
// проверка
|
||||
function f(a, b) {
|
||||
alert( a + b );
|
||||
}
|
||||
|
||||
f.defer(1000)(1, 2); // выведет 3 через 1 секунду.
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Добавить функциям defer с аргументами
|
||||
|
||||
Добавьте всем функциям в прототип метод defer(ms), который возвращает обёртку, откладывающую вызов функции на ms миллисекунд.
|
||||
|
||||
Например, должно работать так:
|
||||
|
||||
```js
|
||||
function f(a, b) {
|
||||
alert( a + b );
|
||||
}
|
||||
|
||||
f.defer(1000)(1, 2); // выведет 3 через 1 секунду.
|
||||
```
|
||||
|
||||
То есть, должны корректно передаваться аргументы.
|
||||
|
307
1-js/9-object-inheritance/13-native-prototypes/article.md
Normal file
|
@ -0,0 +1,307 @@
|
|||
# Встроенные "классы" в JavaScript
|
||||
|
||||
В JavaScript есть встроенные объекты: `Date`, `Array`, `Object` и другие. Они используют прототипы и демонстрируют организацию "псевдоклассов" на JavaScript, которую мы вполне можем применить и для себя.
|
||||
|
||||
[cut]
|
||||
|
||||
## Откуда методы у {} ?
|
||||
|
||||
Начнём мы с того, что создадим пустой объект и выведем его.
|
||||
|
||||
```js run
|
||||
var obj = {};
|
||||
alert( obj ); // "[object Object]" ?
|
||||
```
|
||||
|
||||
Где код, который генерирует строковое представление для `alert(obj)`? Объект-то ведь пустой.
|
||||
|
||||
## Object.prototype
|
||||
|
||||
...Конечно же, это сделал метод `toString`, который находится... Конечно, не в самом объекте (он пуст), а в его прототипе `obj.__proto__`, можно его даже вывести:
|
||||
|
||||
```js run
|
||||
alert( {}.__proto__.toString ); // function toString
|
||||
```
|
||||
|
||||
Откуда новый объект `obj` получает такой `__proto__`?
|
||||
|
||||
1. Запись `obj = {}` является краткой формой `obj = new Object`, где `Object` -- встроенная функция-конструктор для объектов.
|
||||
2. При выполнении `new Object`, создаваемому объекту ставится `__proto__` по `prototype` конструктора, который в данном случае равен встроенному `Object.prototype`.
|
||||
3. В дальнейшем при обращении к `obj.toString()` -- функция будет взята из `Object.prototype`.
|
||||
|
||||

|
||||
|
||||
Это можно легко проверить:
|
||||
|
||||
```js run
|
||||
var obj = {};
|
||||
|
||||
// метод берётся из прототипа?
|
||||
alert( obj.toString == Object.prototype.toString ); // true, да
|
||||
|
||||
// проверим, правда ли что __proto__ это Object.prototype?
|
||||
alert( obj.__proto__ == Object.prototype ); // true
|
||||
|
||||
// А есть ли __proto__ у Object.prototype?
|
||||
alert( obj.__proto__.__proto__ ); // null, нет
|
||||
```
|
||||
|
||||
## Встроенные "классы" в JavaScript
|
||||
|
||||
Точно такой же подход используется в массивах `Array`, функциях `Function` и других объектах. Встроенные методы для них находятся в `Array.prototype`, `Function.prototype` и т.п.
|
||||
|
||||

|
||||
|
||||
Например, когда мы создаём массив, `[1, 2, 3]`, то это альтернативный вариант синтаксиса `new Array`, так что у массивов есть стандартный прототип `Array.prototype`.
|
||||
|
||||
Но в нём есть методы лишь для массивов, а для общих методов всех объектов есть ссылка `Array.prototype.__proto__`, равная `Object.prototype`.
|
||||
|
||||
Аналогично, для функций.
|
||||
|
||||
Лишь для чисел (как и других примитивов) всё немного иначе, но об этом чуть далее.
|
||||
|
||||
Объект `Object.prototype` -- вершина иерархии, единственный, у которого `__proto__` равно `null`.
|
||||
|
||||
**Поэтому говорят, что "все объекты наследуют от `Object`", а если более точно, то от `Object.prototype`.**
|
||||
|
||||
"Псевдоклассом" или, более коротко, "классом", называют функцию-конструктор вместе с её `prototype`. Такой способ объявления классов называют "прототипным стилем ООП".
|
||||
|
||||
При наследовании часть методов переопределяется, например, у массива `Array` есть свой `toString`, который выводит элементы массива через запятую:
|
||||
|
||||
```js run
|
||||
var arr = [1, 2, 3]
|
||||
alert( arr ); // 1,2,3 <-- результат Array.prototype.toString
|
||||
```
|
||||
|
||||
Как мы видели раньше, у `Object.prototype` есть свой `toString`, но так как в `Array.prototype` он ищется первым, то берётся именно вариант для массивов:
|
||||
|
||||

|
||||
|
||||
````smart header="Вызов методов через `call` и `apply` из прототипа"
|
||||
Ранее мы говорили о применении методов массивов к "псевдомассивам", например, можно использовать `[].join` для `arguments`:
|
||||
|
||||
```js run
|
||||
function showList() {
|
||||
*!*
|
||||
alert( [].join.call(arguments, " - ") );
|
||||
*/!*
|
||||
}
|
||||
|
||||
showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша
|
||||
```
|
||||
|
||||
Так как метод `join` находится в `Array.prototype`, то можно вызвать его оттуда напрямую, вот так:
|
||||
|
||||
```js run
|
||||
function showList() {
|
||||
*!*
|
||||
alert( Array.prototype.join.call(arguments, " - ") );
|
||||
*/!*
|
||||
}
|
||||
|
||||
showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша
|
||||
```
|
||||
|
||||
Это эффективнее, потому что не создаётся лишний объект массива `[]`, хотя, с другой стороны -- больше букв писать.
|
||||
````
|
||||
|
||||
## Примитивы
|
||||
|
||||
Примитивы не являются объектами, но методы берут из соответствующих прототипов: `Number.prototype`, `Boolean.prototype`, `String.prototype`.
|
||||
|
||||
По стандарту, если обратиться к свойству числа, строки или логического значения, то будет создан объект соответствующего типа, например `new String` для строки, `new Number` для чисел, `new Boolean` -- для логических выражений.
|
||||
|
||||
Далее будет произведена операция со свойством или вызов метода по обычным правилам, с поиском в прототипе, а затем этот объект будет уничтожен.
|
||||
|
||||
Именно так работает код ниже:
|
||||
|
||||
```js run
|
||||
var user = "Вася"; // создали строку (примитив)
|
||||
|
||||
*!*
|
||||
alert( user.toUpperCase() ); // ВАСЯ
|
||||
// был создан временный объект new String
|
||||
// вызван метод
|
||||
// new String уничтожен, результат возвращён
|
||||
*/!*
|
||||
```
|
||||
|
||||
Можно даже попробовать записать в этот временный объект свойство:
|
||||
|
||||
```js run
|
||||
// попытаемся записать свойство в строку:
|
||||
var user = "Вася";
|
||||
user.age = 30;
|
||||
|
||||
*!*
|
||||
alert( user.age ); // undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
Свойство `age` было записано во временный объект, который был тут же уничтожен, так что смысла в такой записи немного.
|
||||
|
||||
````warn header="Конструкторы `String/Number/Boolean` -- только для внутреннего использования"
|
||||
Технически, можно создавать объекты для примитивов и вручную, например `new Number`. Но в ряде случаев получится откровенно бредовое поведение. Например:
|
||||
|
||||
```js run
|
||||
alert( typeof 1 ); // "number"
|
||||
|
||||
alert( typeof new Number(1) ); // "object" ?!?
|
||||
```
|
||||
|
||||
Или, ещё страннее:
|
||||
|
||||
```js run
|
||||
var zero = new Number(0);
|
||||
|
||||
if (zero) { // объект - true, так что alert выполнится
|
||||
alert( "число ноль -- true?!?" );
|
||||
}
|
||||
```
|
||||
|
||||
Поэтому в явном виде `new String`, `new Number` и `new Boolean` никогда не вызываются.
|
||||
````
|
||||
|
||||
```warn header="Значения `null` и `undefined` не имеют свойств"
|
||||
Значения `null` и `undefined` стоят особняком. Вышесказанное к ним не относится.
|
||||
|
||||
Для них нет соответствующих классов, в них нельзя записать свойство (будет ошибка), в общем, на конкурсе "самое примитивное значение" они точно разделили бы первое место.
|
||||
```
|
||||
|
||||
## Изменение встроенных прототипов [#native-prototype-change]
|
||||
|
||||
Встроенные прототипы можно изменять. В том числе -- добавлять свои методы.
|
||||
|
||||
Мы можем написать метод для многократного повторения строки, и он тут же станет доступным для всех строк:
|
||||
|
||||
```js run
|
||||
String.prototype.repeat = function(times) {
|
||||
return new Array(times + 1).join(this);
|
||||
};
|
||||
|
||||
alert( "ля".repeat(3) ); // ляляля
|
||||
```
|
||||
|
||||
Аналогично мы могли бы создать метод `Object.prototype.each(func)`, который будет применять `func` к каждому свойству:
|
||||
|
||||
```js run
|
||||
Object.prototype.each = function(f) {
|
||||
for (var prop in this) {
|
||||
var value = this[prop];
|
||||
f.call(value, prop, value); // вызовет f(prop, value), this=value
|
||||
}
|
||||
}
|
||||
|
||||
// Попробуем! (внимание, пока что это работает неверно!)
|
||||
var user = {
|
||||
name: 'Вася',
|
||||
age: 25
|
||||
};
|
||||
|
||||
user.each(function(prop, val) {
|
||||
alert( prop ); // name -> age -> (!) each
|
||||
});
|
||||
```
|
||||
|
||||
Обратите внимание -- пример выше работает не совсем корректно. Вместе со свойствами объекта `user` он выводит и наше свойство `each`. Технически, это правильно, так как цикл `for..in` перебирает свойства и в прототипе тоже, но не очень удобно.
|
||||
|
||||
Конечно, это легко поправить добавлением проверки `hasOwnProperty`:
|
||||
|
||||
```js run
|
||||
Object.prototype.each = function(f) {
|
||||
|
||||
for (var prop in this) {
|
||||
|
||||
*!*
|
||||
// пропускать свойства из прототипа
|
||||
if (!this.hasOwnProperty(prop)) continue;
|
||||
*/!*
|
||||
|
||||
var value = this[prop];
|
||||
f.call(value, prop, value);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Теперь все будет в порядке
|
||||
var obj = {
|
||||
name: 'Вася',
|
||||
age: 25
|
||||
};
|
||||
|
||||
obj.each(function(prop, val) {
|
||||
alert( prop ); // name -> age
|
||||
});
|
||||
```
|
||||
|
||||
Здесь это сработало, теперь код работает верно. Но мы же не хотим добавлять `hasOwnProperty` в цикл по любому объекту! Поэтому либо не добавляйте свойства в `Object.prototype`, либо можно использовать [дескриптор свойства](/descriptors-getters-setters) и флаг `enumerable`.
|
||||
|
||||
Это, конечно, не будет работать в IE8-:
|
||||
|
||||
```js run
|
||||
Object.prototype.each = function(f) {
|
||||
|
||||
for (var prop in this) {
|
||||
var value = this[prop];
|
||||
f.call(value, prop, value);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
*!*
|
||||
// поправить объявление свойства, установив флаг enumerable: false
|
||||
Object.defineProperty(Object.prototype, 'each', {
|
||||
enumerable: false
|
||||
});
|
||||
*/!*
|
||||
|
||||
// Теперь все будет в порядке
|
||||
var obj = {
|
||||
name: 'Вася',
|
||||
age: 25
|
||||
};
|
||||
|
||||
obj.each(function(prop, val) {
|
||||
alert( prop ); // name -> age
|
||||
});
|
||||
```
|
||||
|
||||
Есть несколько "за" и "против" модификации встроенных прототипов:
|
||||
|
||||
```compare
|
||||
+ Методы в прототипе автоматически доступны везде, их вызов прост и красив.
|
||||
- Новые свойства, добавленные в прототип из разных мест, могут конфликтовать между собой. Представьте, что вы подключили две библиотеки, которые добавили одно и то же свойство в прототип, но определили его по-разному. Конфликт неизбежен.
|
||||
- Изменения встроенных прототипов влияют глобально, на все-все скрипты, делать их не очень хорошо с архитектурной точки зрения.
|
||||
```
|
||||
|
||||
Как правило, минусы весомее, но есть одно исключение, когда изменения встроенных прототипов не только разрешены, но и приветствуются.
|
||||
|
||||
**Допустимо изменение прототипа встроенных объектов, которое добавляет поддержку метода из современных стандартов в те браузеры, где её пока нет.**
|
||||
|
||||
Например, добавим `Object.create(proto)` в старые браузеры:
|
||||
|
||||
```js
|
||||
if (!Object.create) {
|
||||
|
||||
Object.create = function(proto) {
|
||||
function F() {}
|
||||
F.prototype = proto;
|
||||
return new F;
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Именно так работает библиотека [es5-shim](https://github.com/kriskowal/es5-shim), которая предоставляет многие функции современного JavaScript для старых браузеров. Они добавляются во встроенные объекты и их прототипы.
|
||||
|
||||
## Итого
|
||||
|
||||
- Методы встроенных объектов хранятся в их прототипах.
|
||||
- Встроенные прототипы можно расширить или поменять.
|
||||
- Добавление методов в `Object.prototype`, если оно не сопровождается `Object.defineProperty` с установкой `enumerable` (IE9+), "сломает" циклы `for..in`, поэтому стараются в этот прототип методы не добавлять.
|
||||
|
||||
Другие прототипы изменять менее опасно, но все же не рекомендуется во избежание конфликтов.
|
||||
|
||||
Отдельно стоит изменение с целью добавления современных методов в старые браузеры, таких как <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create">Object.create</a>, <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys">Object.keys</a>, <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind">Function.prototype.bind</a> и т.п. Это допустимо и как раз делается [es5-shim](https://github.com/kriskowal/es5-shim).
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg width="200" height="233" viewBox="0 0 200 233" xmlns="http://www.w3.org/2000/svg"><title>native-prototype-object</title><g fill="none" fill-rule="evenodd"><path stroke="#979797" d="M0 86h200v60H0z"/><text font-family="Consolas" font-size="14" fill="#000" transform="translate(0 69)"><tspan x="11" y="41">toString: function</tspan> <tspan x="11" y="57">другие методы объектов</tspan></text><text font-family="Consolas" font-size="14" font-style="italic" fill="#000" transform="translate(0 69)"><tspan x="0" y="10">Object.prototype</tspan></text><path stroke="#979797" d="M0 203h200v30H0z"/><text font-family="Consolas" font-size="14" font-style="italic" fill="#000" transform="translate(0 186)"><tspan x="0" y="10">obj</tspan></text><path d="M60.5 194.5v-41m0 0l-3 10.8h6l-3-10.8z" stroke="#4990E2" stroke-linecap="square" fill="#4990E2"/><text font-family="Consolas" font-size="14" fill="#4990E2"><tspan x="70" y="175">__proto__</tspan></text><path d="M60.5 62.5v-41m0 0l-3 10.8h6l-3-10.8z" stroke="#4990E2" stroke-linecap="square" fill="#4990E2"/><text font-family="Consolas" font-size="14" fill="#4990E2"><tspan x="70" y="43">__proto__</tspan></text><text font-family="Consolas" font-size="14" fill="#4990E2"><tspan x="46" y="10">null</tspan></text></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,31 @@
|
|||
|
||||
|
||||
```js run
|
||||
function CoffeeMachine(power) {
|
||||
// свойства конкретной кофеварки
|
||||
this._power = power;
|
||||
this._waterAmount = 0;
|
||||
}
|
||||
|
||||
// свойства и методы для всех объектов класса
|
||||
CoffeeMachine.prototype.WATER_HEAT_CAPACITY = 4200;
|
||||
|
||||
CoffeeMachine.prototype._getTimeToBoil = function() {
|
||||
return this._waterAmount * this.WATER_HEAT_CAPACITY * 80 / this._power;
|
||||
};
|
||||
|
||||
CoffeeMachine.prototype.run = function() {
|
||||
setTimeout(function() {
|
||||
alert( 'Кофе готов!' );
|
||||
}, this._getTimeToBoil());
|
||||
};
|
||||
|
||||
CoffeeMachine.prototype.setWaterAmount = function(amount) {
|
||||
this._waterAmount = amount;
|
||||
};
|
||||
|
||||
var coffeeMachine = new CoffeeMachine(10000);
|
||||
coffeeMachine.setWaterAmount(50);
|
||||
coffeeMachine.run();
|
||||
```
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Перепишите в виде класса
|
||||
|
||||
Есть класс `CoffeeMachine`, заданный в функциональном стиле.
|
||||
|
||||
Задача: переписать `CoffeeMachine` в виде класса с использованием прототипа.
|
||||
|
||||
Исходный код:
|
||||
|
||||
```js run
|
||||
function CoffeeMachine(power) {
|
||||
var waterAmount = 0;
|
||||
|
||||
var WATER_HEAT_CAPACITY = 4200;
|
||||
|
||||
function getTimeToBoil() {
|
||||
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
|
||||
}
|
||||
|
||||
this.run = function() {
|
||||
setTimeout(function() {
|
||||
alert( 'Кофе готов!' );
|
||||
}, getTimeToBoil());
|
||||
};
|
||||
|
||||
this.setWaterAmount = function(amount) {
|
||||
waterAmount = amount;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
var coffeeMachine = new CoffeeMachine(10000);
|
||||
coffeeMachine.setWaterAmount(50);
|
||||
coffeeMachine.run();
|
||||
```
|
||||
|
||||
P.S. При описании через прототипы локальные переменные недоступны методам, поэтому нужно будет переделать их в защищённые свойства.
|
|
@ -0,0 +1,46 @@
|
|||
# Почему возникает проблема
|
||||
|
||||
Давайте подробнее разберем происходящее при вызове `speedy.found("яблоко")`:
|
||||
|
||||
1. Интерпретатор ищет свойство `found` в `speedy`. Но `speedy` -- пустой объект, т.к. `new Hamster` ничего не делает с `this`.
|
||||
2. Интерпретатор идёт по ссылке `speedy.__proto__ (==Hamster.prototype)` и находят там метод `found`, запускает его.
|
||||
3. Значение `this` устанавливается в объект перед точкой, т.е. в `speedy`.
|
||||
4. Для выполнения `this.food.push()` нужно найти свойство `this.food`. Оно отсутствует в `speedy`, но есть в `speedy.__proto__`.
|
||||
5. Значение `"яблоко"` добавляется в `speedy.__proto__.food`.
|
||||
|
||||
**У всех хомяков общий живот!** Или, в терминах JavaScript, свойство `food` изменяется в прототипе, который является общим для всех объектов-хомяков.
|
||||
|
||||
Заметим, что этой проблемы не было бы при простом присваивании:
|
||||
|
||||
```js
|
||||
this.food = something;
|
||||
```
|
||||
|
||||
В этом случае значение записалось бы в сам объект, без поиска `found` в прототипе.
|
||||
|
||||
**Проблема возникает только со свойствами-объектами в прототипе.**
|
||||
|
||||
Для исправления проблемы нужно дать каждому хомяку свой живот. Это можно сделать, присвоив его в конструкторе.
|
||||
|
||||
```js run
|
||||
function Hamster() {
|
||||
*!*
|
||||
this.food = [];
|
||||
*/!*
|
||||
}
|
||||
|
||||
Hamster.prototype.found = function(something) {
|
||||
this.food.push(something);
|
||||
};
|
||||
|
||||
speedy = new Hamster();
|
||||
lazy = new Hamster();
|
||||
|
||||
speedy.found("яблоко");
|
||||
speedy.found("орех");
|
||||
|
||||
alert(speedy.food.length) // 2
|
||||
alert(lazy.food.length) // 0(!)
|
||||
```
|
||||
|
||||
Теперь всё в порядке. У каждого хомяка -- свой живот.
|
|
@ -0,0 +1,34 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Хомяки с __proto__
|
||||
|
||||
Вы -- руководитель команды, которая разрабатывает игру, хомяковую ферму. Один из программистов получил задание создать класс "хомяк" (англ - `"Hamster"`).
|
||||
|
||||
Объекты-хомяки должны иметь массив `food` для хранения еды и метод `found` для добавления.
|
||||
|
||||
Ниже -- его решение. При создании двух хомяков, если поел один -- почему-то сытым становится и второй тоже.
|
||||
|
||||
В чём дело? Как поправить?
|
||||
|
||||
```js run
|
||||
function Hamster() {}
|
||||
|
||||
Hamster.prototype.food = []; // пустой "живот"
|
||||
|
||||
Hamster.prototype.found = function(something) {
|
||||
this.food.push(something);
|
||||
};
|
||||
|
||||
// Создаём двух хомяков и кормим первого
|
||||
var speedy = new Hamster();
|
||||
var lazy = new Hamster();
|
||||
|
||||
speedy.found("яблоко");
|
||||
speedy.found("орех");
|
||||
|
||||
alert( speedy.food.length ); // 2
|
||||
alert( lazy.food.length ); // 2 (!??)
|
||||
```
|
||||
|
123
1-js/9-object-inheritance/14-classes/article.md
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Свои классы на прототипах
|
||||
|
||||
Используем ту же структуру, что JavaScript использует внутри себя, для объявления своих классов.
|
||||
|
||||
[cut]
|
||||
|
||||
## Обычный конструктор
|
||||
|
||||
Вспомним, как мы объявляли классы ранее.
|
||||
|
||||
Например, этот код задаёт класс `Animal` в функциональном стиле, без всяких прототипов:
|
||||
|
||||
```js run
|
||||
function Animal(name) {
|
||||
this.speed = 0;
|
||||
this.name = name;
|
||||
|
||||
this.run = function(speed) {
|
||||
this.speed += speed;
|
||||
alert( this.name + ' бежит, скорость ' + this.speed );
|
||||
};
|
||||
|
||||
this.stop = function() {
|
||||
this.speed = 0;
|
||||
alert( this.name + ' стоит' );
|
||||
};
|
||||
};
|
||||
|
||||
var animal = new Animal('Зверь');
|
||||
|
||||
alert( animal.speed ); // 0, начальная скорость
|
||||
animal.run(3); // Зверь бежит, скорость 3
|
||||
animal.run(10); // Зверь бежит, скорость 13
|
||||
animal.stop(); // Зверь стоит
|
||||
```
|
||||
|
||||
## Класс через прототип
|
||||
|
||||
А теперь создадим аналогичный класс, используя прототипы, наподобие того, как сделаны классы `Object`, `Date` и остальные.
|
||||
|
||||
Чтобы объявить свой класс, нужно:
|
||||
|
||||
1. Объявить функцию-конструктор.
|
||||
2. Записать методы и свойства, нужные всем объектам класса, в `prototype`.
|
||||
|
||||
Опишем класс `Animal`:
|
||||
|
||||
```js run
|
||||
// конструктор
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
// методы в прототипе
|
||||
Animal.prototype.run = function(speed) {
|
||||
this.speed += speed;
|
||||
alert( this.name + ' бежит, скорость ' + this.speed );
|
||||
};
|
||||
|
||||
Animal.prototype.stop = function() {
|
||||
this.speed = 0;
|
||||
alert( this.name + ' стоит' );
|
||||
};
|
||||
|
||||
var animal = new Animal('Зверь');
|
||||
|
||||
alert( animal.speed ); // 0, свойство взято из прототипа
|
||||
animal.run(5); // Зверь бежит, скорость 5
|
||||
animal.run(5); // Зверь бежит, скорость 10
|
||||
animal.stop(); // Зверь стоит
|
||||
```
|
||||
|
||||
В объекте `animal` будут храниться свойства конкретного экземпляра: `name` и `speed`, а общие методы -- в прототипе.
|
||||
|
||||
Совершенно такой же подход, как и для встроенных классов в JavaScript.
|
||||
|
||||
## Сравнение
|
||||
|
||||
Чем такое задание класса лучше и хуже функционального стиля?
|
||||
|
||||
```compare
|
||||
+ Функциональный стиль записывает в каждый объект и свойства и методы, а прототипный -- только свойства. Поэтому прототипный стиль -- быстрее и экономнее по памяти.
|
||||
- При создании методов через прототип, мы теряем возможность использовать локальные переменные как приватные свойства, у них больше нет общей области видимости с конструктором.
|
||||
```
|
||||
|
||||
Таким образом, прототипный стиль -- быстрее и экономнее, но немного менее удобен.
|
||||
|
||||
К примеру, есть у нас приватное свойство `name` и метод `sayHi` в функциональном стиле ООП:
|
||||
|
||||
```js run
|
||||
function Animal(name) {
|
||||
this.sayHi = function() {
|
||||
*!*
|
||||
alert( name );
|
||||
*/!*
|
||||
};
|
||||
}
|
||||
|
||||
var animal = new Animal("Зверь");
|
||||
animal.sayHi(); // Зверь
|
||||
```
|
||||
|
||||
При задании методов в прототипе мы не сможем её так оставить, ведь методы находятся *вне* конструктора, у них нет общей области видимости, поэтому приходится записывать `name` в сам объект, обозначив его как защищённое:
|
||||
|
||||
```js run
|
||||
function Animal(name) {
|
||||
*!*
|
||||
this._name = name;
|
||||
*/!*
|
||||
}
|
||||
|
||||
Animal.prototype.sayHi = function() {
|
||||
*!*
|
||||
alert( this._name );
|
||||
*/!*
|
||||
}
|
||||
|
||||
var animal = new Animal("Зверь");
|
||||
animal.sayHi(); // Зверь
|
||||
```
|
||||
|
||||
Впрочем, недостаток этот -- довольно условный. Ведь при наследовании в функциональном стиле также пришлось бы писать `this._name`, чтобы потомок получил доступ к этому значению.
|
|
@ -0,0 +1,43 @@
|
|||
Ошибка в строке:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
```
|
||||
|
||||
Эта ошибка приведёт к тому, что `Rabbit.prototype` и `Animal.prototype` -- один и тот же объект. В результате методы `Rabbit` будут помещены в него и, при совпадении, перезапишут методы `Animal`.
|
||||
|
||||
Получится, что все животные прыгают, вот пример:
|
||||
|
||||
```js run no-beautify
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.walk = function() {
|
||||
alert("ходит " + this.name);
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
*!*
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
*/!*
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert("прыгает! и ходит: " + this.name);
|
||||
};
|
||||
|
||||
*!*
|
||||
var animal = new Animal("Хрюшка");
|
||||
animal.walk(); // прыгает! и ходит Хрюшка
|
||||
*/!*
|
||||
```
|
||||
|
||||
Правильный вариант этой строки:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
```
|
||||
|
||||
Если так написать, то в `Rabbit.prototype` будет отдельный объект, который прототипно наследует от `Animal.prototype`, но может содержать и свои свойства, специфичные для кроликов.
|
|
@ -0,0 +1,27 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Найдите ошибку в наследовании
|
||||
|
||||
Найдите ошибку в прототипном наследовании. К чему она приведёт?
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.walk = function() {
|
||||
alert( "ходит " + this.name );
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert( "прыгает! и ходит: " + this.name );
|
||||
};
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
Ошибка -- в том, что метод `walk` присваивается в конструкторе `Animal` самому объекту вместо прототипа.
|
||||
|
||||
Поэтому, если мы решим перезаписать этот метод своим, специфичным для кролика, то он не сработает:
|
||||
|
||||
```js
|
||||
// ...
|
||||
|
||||
// записывается в прототип
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert( "прыгает " + this.name );
|
||||
};
|
||||
```
|
||||
|
||||
Метод `this.walk` из `Animal` записывается в сам объект, и поэтому он всегда будет первым, игнорируя цепочку прототипов.
|
||||
|
||||
Правильно было бы определять `walk` как `Animal.prototype.walk`.
|
||||
|
||||
Тем более, что этот метод является общим для всех объектов, тратить память и время на запись его в каждый конструктор определённо ни к чему.
|
|
@ -0,0 +1,31 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# В чём ошибка в наследовании
|
||||
|
||||
Найдите ошибку в прототипном наследовании. К чему она приведёт?
|
||||
|
||||
```js run
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
|
||||
this.walk = function() {
|
||||
alert( "ходит " + this.name );
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function Rabbit(name) {
|
||||
Animal.apply(this, arguments);
|
||||
}
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert( "прыгает " + this.name );
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit("Кроль");
|
||||
rabbit.walk();
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
function Clock(options) {
|
||||
this._template = options.template;
|
||||
}
|
||||
|
||||
Clock.prototype._render = function render() {
|
||||
var date = new Date();
|
||||
|
||||
var hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
var min = date.getMinutes();
|
||||
if (min < 10) min = '0' + min;
|
||||
|
||||
var sec = date.getSeconds();
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
|
||||
var output = this._template.replace('h', hours).replace('m', min).replace('s', sec);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this._timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this._render();
|
||||
var self = this;
|
||||
this._timer = setInterval(function() {
|
||||
self._render();
|
||||
}, 1000);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
[js src="clock.js"]
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
function Clock(options) {
|
||||
this._template = options.template;
|
||||
}
|
||||
|
||||
Clock.prototype._render = function render() {
|
||||
var date = new Date();
|
||||
|
||||
var hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
var min = date.getMinutes();
|
||||
if (min < 10) min = '0' + min;
|
||||
|
||||
var sec = date.getSeconds();
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
|
||||
var output = this._template.replace('h', hours).replace('m', min).replace('s', sec);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this._timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this._render();
|
||||
var self = this;
|
||||
this._timer = setInterval(function() {
|
||||
self._render();
|
||||
}, 1000);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Часики в консоли</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
var clock = new Clock({
|
||||
template: 'h:m:s'
|
||||
});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
function Clock(options) {
|
||||
|
||||
var template = options.template;
|
||||
var timer;
|
||||
|
||||
function render() {
|
||||
var date = new Date();
|
||||
|
||||
var hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
var min = date.getMinutes();
|
||||
if (min < 10) min = '0' + min;
|
||||
|
||||
var sec = date.getSeconds();
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
|
||||
var output = template.replace('h', hours).replace('m', min).replace('s', sec);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
this.stop = function() {
|
||||
clearInterval(timer);
|
||||
};
|
||||
|
||||
this.start = function() {
|
||||
render();
|
||||
timer = setInterval(render, 1000);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Часики в консоли</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
var clock = new Clock({
|
||||
template: 'h:m:s'
|
||||
});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Класс "часы"
|
||||
|
||||
Есть реализация часиков, оформленная в виде одной функции-конструктора. У неё есть приватные свойства `timer`, `template` и метод `render`.
|
||||
|
||||
Задача: переписать часы на прототипах. Приватные свойства и методы сделать защищёнными.
|
||||
|
||||
P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть).
|
|
@ -0,0 +1,14 @@
|
|||
function ExtendedClock(options) {
|
||||
Clock.apply(this, arguments);
|
||||
this._precision = +options.precision || 1000;
|
||||
}
|
||||
|
||||
ExtendedClock.prototype = Object.create(Clock.prototype);
|
||||
|
||||
ExtendedClock.prototype.start = function() {
|
||||
this._render();
|
||||
var self = this;
|
||||
this._timer = setInterval(function() {
|
||||
self._render();
|
||||
}, this._precision);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
Наследник:
|
||||
|
||||
[js src="extended-clock.js"]
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
function Clock(options) {
|
||||
this._template = options.template;
|
||||
}
|
||||
|
||||
Clock.prototype._render = function render() {
|
||||
var date = new Date();
|
||||
|
||||
var hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
var min = date.getMinutes();
|
||||
if (min < 10) min = '0' + min;
|
||||
|
||||
var sec = date.getSeconds();
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
|
||||
var output = this._template.replace('h', hours).replace('m', min).replace('s', sec);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this._timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this._render();
|
||||
var self = this;
|
||||
this._timer = setInterval(function() {
|
||||
self._render();
|
||||
}, 1000);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
function ExtendedClock(options) {
|
||||
Clock.apply(this, arguments);
|
||||
this._precision = +options.precision || 1000;
|
||||
}
|
||||
|
||||
ExtendedClock.prototype = Object.create(Clock.prototype);
|
||||
|
||||
ExtendedClock.prototype.start = function() {
|
||||
this._render();
|
||||
var self = this;
|
||||
this._timer = setInterval(function() {
|
||||
self._render();
|
||||
}, this._precision);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Часики в консоли</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
|
||||
<script src="extended-clock.js"></script>
|
||||
|
||||
<script>
|
||||
var lowResolutionClock = new ExtendedClock({
|
||||
template: 'h:m:s',
|
||||
precision: 10000
|
||||
});
|
||||
|
||||
lowResolutionClock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
function Clock(options) {
|
||||
this._template = options.template;
|
||||
}
|
||||
|
||||
Clock.prototype._render = function render() {
|
||||
var date = new Date();
|
||||
|
||||
var hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
var min = date.getMinutes();
|
||||
if (min < 10) min = '0' + min;
|
||||
|
||||
var sec = date.getSeconds();
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
|
||||
var output = this._template.replace('h', hours).replace('m', min).replace('s', sec);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this._timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this._render();
|
||||
var self = this;
|
||||
this._timer = setInterval(function() {
|
||||
self._render();
|
||||
}, 1000);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
function extend(Child, Parent) {
|
||||
Child.prototype = inherit(Parent.prototype);
|
||||
Child.prototype.constructor = Child;
|
||||
Child.parent = Parent.prototype;
|
||||
}
|
||||
|
||||
function inherit(proto) {
|
||||
function F() {}
|
||||
F.prototype = proto;
|
||||
return new F;
|
||||
}
|
||||
|
||||
// ваш код
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Часики в консоли</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- исходные часы, от них нужно унаследовать -->
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
var clock = new Clock({
|
||||
template: 'h:m:s'
|
||||
});
|
||||
clock.start();
|
||||
|
||||
|
||||
/* ... ваш код для ExtendedClock */
|
||||
|
||||
/*
|
||||
Надо: часы, которые тикают раз в 10 секунд (точность 10000)
|
||||
var lowResolutionClock = new ExtendedClock({
|
||||
template: 'h:m:s',
|
||||
precision: 10000
|
||||
});
|
||||
|
||||
lowResolutionClock.start();
|
||||
*/
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,13 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Класс "расширенные часы"
|
||||
|
||||
Есть реализация часиков на прототипах. Создайте класс, расширяющий её, добавляющий поддержку параметра `precision`, который будет задавать частоту тика в `setInterval`. Значение по умолчанию: `1000`.
|
||||
|
||||
- Для этого класс `Clock` надо унаследовать. Пишите ваш новый код в файле `extended-clock.js`.
|
||||
- Исходный класс `Clock` менять нельзя.
|
||||
- Пусть конструктор потомка вызывает конструктор родителя. Это позволит избежать проблем при расширении `Clock` новыми опциями.
|
||||
|
||||
P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть).
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
Обратите внимание: константы состояний перенесены в прототип, чтобы `AnimatingMenu` их тоже унаследовал.
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="menu.js"></script>
|
||||
<script>
|
||||
function AnimatingMenu() {
|
||||
Menu.apply(this, arguments);
|
||||
}
|
||||
|
||||
AnimatingMenu.prototype = Object.create(Menu.prototype);
|
||||
|
||||
AnimatingMenu.prototype.STATE_ANIMATING = 2;
|
||||
|
||||
AnimatingMenu.prototype.open = function() {
|
||||
var self = this;
|
||||
|
||||
this._state = this.STATE_ANIMATING;
|
||||
|
||||
this._timer = setTimeout(function() {
|
||||
Menu.prototype.open.call(self);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
AnimatingMenu.prototype.close = function() {
|
||||
clearTimeout(this._timer);
|
||||
Menu.prototype.close.apply(this);
|
||||
};
|
||||
|
||||
AnimatingMenu.prototype._stateAsString = function() {
|
||||
|
||||
switch (this._state) {
|
||||
case this.STATE_ANIMATING:
|
||||
return 'анимация';
|
||||
|
||||
default:
|
||||
return Menu.prototype._stateAsString.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
// тест, использование..
|
||||
var menu = new AnimatingMenu();
|
||||
|
||||
menu.showState(); // закрыто
|
||||
|
||||
menu.open();
|
||||
menu.showState(); // анимация
|
||||
|
||||
setTimeout(function() { // через 1 секунду
|
||||
menu.showState(); // открыто
|
||||
|
||||
menu.close();
|
||||
menu.showState(); // закрыто
|
||||
}, 1000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,28 @@
|
|||
function Menu(state) {
|
||||
this._state = state || this.STATE_CLOSED;
|
||||
};
|
||||
|
||||
Menu.prototype.STATE_OPEN = 1;
|
||||
Menu.prototype.STATE_CLOSED = 0;
|
||||
|
||||
Menu.prototype.open = function() {
|
||||
this._state = this.STATE_OPEN;
|
||||
};
|
||||
|
||||
Menu.prototype.close = function() {
|
||||
this._state = this.STATE_CLOSED;
|
||||
};
|
||||
|
||||
Menu.prototype._stateAsString = function() {
|
||||
switch (this._state) {
|
||||
case this.STATE_OPEN:
|
||||
return 'открыто';
|
||||
|
||||
case this.STATE_CLOSED:
|
||||
return 'закрыто';
|
||||
}
|
||||
};
|
||||
|
||||
Menu.prototype.showState = function() {
|
||||
alert(this._stateAsString());
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="menu.js"></script>
|
||||
<script>
|
||||
var AnimatingMenu = Menu; // замените на ваш код для AnimatingMenu
|
||||
|
||||
// использование..
|
||||
|
||||
var menu = new AnimatingMenu();
|
||||
|
||||
menu.showState(); // закрыто
|
||||
|
||||
menu.open();
|
||||
menu.showState(); // анимация
|
||||
|
||||
setTimeout(function() {
|
||||
menu.showState(); // открыто
|
||||
|
||||
menu.close();
|
||||
menu.showState(); // закрыто (закрытие без анимации)
|
||||
}, 1000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,28 @@
|
|||
function Menu(state) {
|
||||
this._state = state || Menu.STATE_CLOSED;
|
||||
};
|
||||
|
||||
Menu.STATE_OPEN = 1;
|
||||
Menu.STATE_CLOSED = 0;
|
||||
|
||||
Menu.prototype.open = function() {
|
||||
this._state = Menu.STATE_OPEN;
|
||||
};
|
||||
|
||||
Menu.prototype.close = function() {
|
||||
this._state = Menu.STATE_CLOSED;
|
||||
};
|
||||
|
||||
Menu.prototype._stateAsString = function() {
|
||||
switch (this._state) {
|
||||
case Menu.STATE_OPEN:
|
||||
return 'открыто';
|
||||
|
||||
case Menu.STATE_CLOSED:
|
||||
return 'закрыто';
|
||||
}
|
||||
};
|
||||
|
||||
Menu.prototype.showState = function() {
|
||||
alert(this._stateAsString());
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Меню с таймером для анимации
|
||||
|
||||
Есть класс `Menu`. У него может быть два состояния: открыто `STATE_OPEN` и закрыто `STATE_CLOSED`.
|
||||
|
||||
Создайте наследника `AnimatingMenu`, который добавляет третье состояние `STATE_ANIMATING`.
|
||||
|
||||
- При вызове `open()` состояние меняется на `STATE_ANIMATING`, а через 1 секунду, по таймеру, открытие завершается вызовом `open()` родителя.
|
||||
- Вызов `close()` при необходимости отменяет таймер анимации (назначаемый в `open`) и передаёт вызов родительскому `close`.
|
||||
- Метод `showState` для нового состояния выводит `"анимация"`, для остальных -- полагается на родителя.
|
||||
|
||||
[edit src="source" title="Исходный документ, вместе с тестом"]
|
|
@ -0,0 +1,24 @@
|
|||
**Нет, не распознает, выведет `false`.**
|
||||
|
||||
Свойство `constructor` содержится в `prototype` функции по умолчанию, интерпретатор не поддерживает его корректность. Посмотрим, чему оно равно и откуда оно будет взято в данном случае.
|
||||
|
||||
Порядок поиска свойства `rabbit.constructor`, по цепочке прототипов:
|
||||
|
||||
1. `rabbit` -- это пустой объект, в нём нет.
|
||||
2. `Rabbit.prototype` -- в него при помощи `Object.create` записан пустой объект, наследующий от `Animal.prototype`. Поэтому `constructor'а` в нём также нет.
|
||||
3. `Animal.prototype` -- у функции `Animal` свойство `prototype` никто не менял. Поэтому оно содержит `Animal.prototype.constructor == Animal`.
|
||||
|
||||
```js run
|
||||
function Animal() {}
|
||||
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
alert( rabbit.constructor == Rabbit ); // false
|
||||
alert( rabbit.constructor == Animal ); // true
|
||||
*/!*
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Что содержит constructor?
|
||||
|
||||
В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`.
|
||||
|
||||
Что содержит свойство `rabbit.constructor`? Распознает ли проверка в `alert` объект как `Rabbit`?
|
||||
|
||||
```js
|
||||
function Animal() {}
|
||||
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
alert( rabbit.constructor == Rabbit ); // что выведет?
|
||||
```
|
||||
|
340
1-js/9-object-inheritance/15-class-inheritance/article.md
Normal file
|
@ -0,0 +1,340 @@
|
|||
# Наследование классов в JavaScript
|
||||
|
||||
Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку `__proto__`.
|
||||
|
||||
Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через `new Admin`, должны иметь все методы, которые есть у объектов, создаваемых через `new User`, и ещё какие-то свои.
|
||||
|
||||
[cut]
|
||||
|
||||
## Наследование Array от Object
|
||||
|
||||
Для реализации наследования в наших классах мы будем использовать тот же подход, который принят внутри JavaScript.
|
||||
|
||||
Взглянем на него ещё раз на примере `Array`, который наследует от `Object`:
|
||||
|
||||

|
||||
|
||||
- Методы массивов `Array` хранятся в `Array.prototype`.
|
||||
- `Array.prototype` имеет прототипом `Object.prototype`.
|
||||
|
||||
Поэтому когда экземпляры класса `Array` хотят получить метод массива -- они берут его из своего прототипа, например `Array.prototype.slice`.
|
||||
|
||||
Если же нужен метод объекта, например, `hasOwnProperty`, то его в `Array.prototype` нет, и он берётся из `Object.prototype`.
|
||||
|
||||
Отличный способ "потрогать это руками" -- запустить в консоли команду `console.dir([1,2,3])`.
|
||||
|
||||
Вывод в Chrome будет примерно таким:
|
||||
|
||||

|
||||
|
||||
Здесь отчётливо видно, что сами данные и `length` находятся в массиве, дальше в `__proto__` идут методы для массивов `concat`, то есть `Array.prototype`, а далее -- `Object.prototype`.
|
||||
|
||||
```smart header="`console.dir` для доступа к свойствам"
|
||||
Обратите внимание, я использовал именно `console.dir`, а не `console.log`, поскольку `log` зачастую выводит объект в виде строки, без доступа к свойствам.
|
||||
```
|
||||
|
||||
## Наследование в наших классах
|
||||
|
||||
Применим тот же подход для наших классов: объявим класс `Rabbit`, который будет наследовать от `Animal`.
|
||||
|
||||
Вначале создадим два этих класса по отдельности, они пока что будут совершенно независимы.
|
||||
|
||||
`Animal`:
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
Animal.prototype.run = function(speed) {
|
||||
this.speed += speed;
|
||||
alert( this.name + ' бежит, скорость ' + this.speed );
|
||||
};
|
||||
|
||||
Animal.prototype.stop = function() {
|
||||
this.speed = 0;
|
||||
alert( this.name + ' стоит' );
|
||||
};
|
||||
```
|
||||
|
||||
`Rabbit`:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
Rabbit.prototype.jump = function() {
|
||||
this.speed++;
|
||||
alert( this.name + ' прыгает' );
|
||||
};
|
||||
|
||||
var rabbit = new Rabbit('Кроль');
|
||||
```
|
||||
|
||||
Для того, чтобы наследование работало, объект `rabbit = new Rabbit` должен использовать свойства и методы из своего прототипа `Rabbit.prototype`, а если их там нет, то -- свойства и метода родителя, которые хранятся в `Animal.prototype`.
|
||||
|
||||
Если ещё короче -- порядок поиска свойств и методов должен быть таким: `rabbit -> Rabbit.prototype -> Animal.prototype`, по аналогии с тем, как это сделано для объектов и массивов.
|
||||
|
||||
Для этого можно поставить ссылку `__proto__` с `Rabbit.prototype` на `Animal.prototype`.
|
||||
|
||||
Можно сделать это так:
|
||||
```js
|
||||
Rabbit.prototype.__proto__ = Animal.prototype;
|
||||
```
|
||||
|
||||
Однако, прямой доступ к `__proto__` не поддерживается в IE10-, поэтому для поддержки этих браузеров мы используем функцию `Object.create`. Она либо встроена либо легко эмулируется во всех браузерах.
|
||||
|
||||
Класс `Animal` остаётся без изменений, а `Rabbit.prototype` мы будем создавать с нужным прототипом, используя `Object.create`:
|
||||
|
||||
```js no-beautify
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
*!*
|
||||
// задаём наследование
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
*/!*
|
||||
|
||||
// и добавим свой метод (или методы...)
|
||||
Rabbit.prototype.jump = function() { ... };
|
||||
```
|
||||
|
||||
Теперь выглядеть иерархия будет так:
|
||||
|
||||

|
||||
|
||||
В `prototype` по умолчанию всегда находится свойство `constructor`, указывающее на функцию-конструктор. В частности, `Rabbit.prototype.constructor == Rabbit`. Если мы рассчитываем использовать это свойство, то при замене `prototype` через `Object.create` нужно его явно сохранить:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
Rabbit.prototype.constructor = Rabbit;
|
||||
```
|
||||
|
||||
## Полный код наследования
|
||||
|
||||
Для наглядности -- вот итоговый код с двумя классами `Animal` и `Rabbit`:
|
||||
|
||||
```js
|
||||
// 1. Конструктор Animal
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
// 1.1. Методы -- в прототип
|
||||
|
||||
Animal.prototype.stop = function() {
|
||||
this.speed = 0;
|
||||
alert( this.name + ' стоит' );
|
||||
}
|
||||
|
||||
Animal.prototype.run = function(speed) {
|
||||
this.speed += speed;
|
||||
alert( this.name + ' бежит, скорость ' + this.speed );
|
||||
};
|
||||
|
||||
// 2. Конструктор Rabbit
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
// 2.1. Наследование
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
Rabbit.prototype.constructor = Rabbit;
|
||||
|
||||
// 2.2. Методы Rabbit
|
||||
Rabbit.prototype.jump = function() {
|
||||
this.speed++;
|
||||
alert( this.name + ' прыгает, скорость ' + this.speed );
|
||||
}
|
||||
```
|
||||
|
||||
Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте.
|
||||
|
||||
Обратим внимание: `Rabbit.prototype = Object.create(Animal.prototype)` присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы.
|
||||
|
||||
````warn header="Неправильный вариант: `Rabbit.prototype = new Animal`"
|
||||
В некоторых устаревших руководствах предлагают вместо `Object.create(Animal.prototype)` записывать в прототип `new Animal`, вот так:
|
||||
|
||||
```js
|
||||
// вместо Rabbit.prototype = Object.create(Animal.prototype)
|
||||
Rabbit.prototype = new Animal();
|
||||
```
|
||||
|
||||
Частично, он рабочий, поскольку иерархия прототипов будет такая же, ведь `new Animal` -- это объект с прототипом `Animal.prototype`, как и `Object.create(Animal.prototype)`. Они в этом плане идентичны.
|
||||
|
||||
Но у этого подхода важный недостаток. Как правило мы не хотим создавать `Animal`, а хотим только унаследовать его методы!
|
||||
|
||||
Более того, на практике создание объекта может требовать обязательных аргументов, влиять на страницу в браузере, делать запросы к серверу и что-то ещё, чего мы хотели бы избежать. Поэтому рекомендуется использовать вариант с `Object.create`.
|
||||
````
|
||||
|
||||
## Вызов конструктора родителя
|
||||
|
||||
Посмотрим внимательно на конструкторы `Animal` и `Rabbit` из примеров выше:
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
```
|
||||
|
||||
Как видно, объект `Rabbit` не добавляет никакой особенной логики при создании, которой не было в `Animal`.
|
||||
|
||||
Чтобы упростить поддержку кода, имеет смысл не дублировать код конструктора `Animal`, а напрямую вызвать его:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
Animal.apply(this, arguments);
|
||||
}
|
||||
```
|
||||
|
||||
Такой вызов запустит функцию `Animal` в контексте текущего объекта, со всеми аргументами, она выполнится и запишет в `this` всё, что нужно.
|
||||
|
||||
Здесь можно было бы использовать и `Animal.call(this, name)`, но `apply` надёжнее, так как работает с любым количеством аргументов.
|
||||
|
||||
## Переопределение метода
|
||||
|
||||
Итак, `Rabbit` наследует `Animal`. Теперь если какого-то метода нет в `Rabbit.prototype` -- он будет взят из `Animal.prototype`.
|
||||
|
||||
В `Rabbit` может понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод `run()`:
|
||||
|
||||
```js
|
||||
Rabbit.prototype.run = function(speed) {
|
||||
this.speed++;
|
||||
this.jump();
|
||||
};
|
||||
```
|
||||
|
||||
Вызов `rabbit.run()` теперь будет брать `run` из своего прототипа:
|
||||
|
||||

|
||||
|
||||
### Вызов метода родителя внутри своего
|
||||
|
||||
Более частая ситуация -- когда мы хотим не просто заменить метод на свой, а взять метод родителя и расширить его. Скажем, кролик бежит так же, как и другие звери, но время от времени подпрыгивает.
|
||||
|
||||
Для вызова метода родителя можно обратиться к нему напрямую, взяв из прототипа:
|
||||
|
||||
```js
|
||||
Rabbit.prototype.run = function() {
|
||||
*!*
|
||||
// вызвать метод родителя, передав ему текущие аргументы
|
||||
Animal.prototype.run.apply(this, arguments);
|
||||
*/!*
|
||||
this.jump();
|
||||
}
|
||||
```
|
||||
|
||||
Обратите внимание на вызов через `apply` и явное указание контекста.
|
||||
|
||||
Если вызвать просто `Animal.prototype.run()`, то в качестве `this` функция `run` получит `Animal.prototype`, а это неверно, нужен текущий объект.
|
||||
|
||||
## Итого
|
||||
|
||||
- Для наследования нужно, чтобы "склад методов потомка" (`Child.prototype`) наследовал от "склада метода родителей" (`Parent.prototype`).
|
||||
|
||||
Это можно сделать при помощи `Object.create`:
|
||||
|
||||
Код:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
```
|
||||
- Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя `apply(this, arguments)`, вот так:
|
||||
|
||||
```js
|
||||
function Rabbit(...) {
|
||||
Animal.apply(this, arguments);
|
||||
}
|
||||
```
|
||||
- При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа:
|
||||
|
||||
```js
|
||||
Rabbit.prototype.run = function() {
|
||||
var result = Animal.prototype.run.apply(this, ...);
|
||||
// result -- результат вызова метода родителя
|
||||
}
|
||||
```
|
||||
|
||||
Структура наследования полностью:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
// --------- Класс-Родитель ------------
|
||||
*/!*
|
||||
// Конструктор родителя пишет свойства конкретного объекта
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
this.speed = 0;
|
||||
}
|
||||
|
||||
// Методы хранятся в прототипе
|
||||
Animal.prototype.run = function() {
|
||||
alert(this.name + " бежит!")
|
||||
}
|
||||
|
||||
*!*
|
||||
// --------- Класс-потомок -----------
|
||||
*/!*
|
||||
// Конструктор потомка
|
||||
function Rabbit(name) {
|
||||
Animal.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Унаследовать
|
||||
*!*
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
*/!*
|
||||
|
||||
// Желательно и constructor сохранить
|
||||
Rabbit.prototype.constructor = Rabbit;
|
||||
|
||||
// Методы потомка
|
||||
Rabbit.prototype.run = function() {
|
||||
// Вызов метода родителя внутри своего
|
||||
Animal.prototype.run.apply(this);
|
||||
alert( this.name + " подпрыгивает!" );
|
||||
};
|
||||
|
||||
// Готово, можно создавать объекты
|
||||
var rabbit = new Rabbit('Кроль');
|
||||
rabbit.run();
|
||||
```
|
||||
|
||||
Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте.
|
||||
|
||||
Кроме того, есть ещё неявное, но очень важное архитектурное отличие.
|
||||
|
||||
Зачастую вызов конструктора имеет какие-то побочные эффекты, например влияет на документ. Если конструктор родителя имеет какое-то поведение, которое нужно переопределить в потомке, то в функциональном стиле это невозможно.
|
||||
|
||||
Иначе говоря, в функциональном стиле в процессе создания `Rabbit` нужно обязательно вызывать `Animal.apply(this, arguments)`, чтобы получить методы родителя -- и если этот `Animal.apply` кроме добавления методов говорит: "Му-у-у!", то это проблема:
|
||||
|
||||
```js
|
||||
function Animal() {
|
||||
this.walk = function() {
|
||||
alert('walk')
|
||||
};
|
||||
alert( 'Му-у-у!' );
|
||||
}
|
||||
|
||||
function Rabbit() {
|
||||
Animal.apply(this, arguments); // как избавиться от мычания, но получить walk?
|
||||
}
|
||||
```
|
||||
|
||||
...Которой нет в прототипном подходе, потому что в процессе создания `new Rabbit` мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.
|
||||
|
||||
Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса -- она сильно лучше в новом стандарте ES6, которым можно пользоваться уже сейчас, если взять транслятор [babeljs](https://babeljs.io/).
|
||||
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,7 @@
|
|||
Да, это выглядит достаточно странно, поскольку объект `a` не создавался функцией `B`.
|
||||
|
||||
Но методу `instanceof` на самом деле вообще не важна функция. Он смотрит на её `prototype` и сверяет его с цепочкой `__proto__` объекта.
|
||||
|
||||
В данном случае `a.__proto__ == B.prototype`, поэтому `instanceof` возвращает `true`.
|
||||
|
||||
По логике `instanceof` именно прототип задаёт "тип объекта", поэтому `instanceof` работает именно так.
|
|
@ -0,0 +1,22 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Странное поведение instanceof
|
||||
|
||||
Почему `instanceof` в коде ниже возвращает `true`, ведь объект `a` явно создан не `B()`?
|
||||
|
||||
```js run
|
||||
function A() {}
|
||||
|
||||
function B() {}
|
||||
|
||||
A.prototype = B.prototype = {};
|
||||
|
||||
var a = new A();
|
||||
|
||||
*!*
|
||||
alert( a instanceof B ); // true
|
||||
*/!*
|
||||
```
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
Да, распознает.
|
||||
|
||||
Он проверяет наследование с учётом цепочки прототипов.
|
||||
|
||||
```js run
|
||||
function Animal() {}
|
||||
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
alert( rabbit instanceof Rabbit ); // true
|
||||
alert( rabbit instanceof Animal ); // true
|
||||
alert( rabbit instanceof Object ); // true
|
||||
```
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Что выведет instanceof?
|
||||
|
||||
В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`.
|
||||
|
||||
Что выведет [instanceof](/instanceof)?
|
||||
|
||||
Распознает ли он `rabbit` как `Animal`, `Rabbit` и к тому же `Object`?
|
||||
|
||||
```js
|
||||
function Animal() {}
|
||||
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
alert( rabbit instanceof Rabbit );
|
||||
alert( rabbit instanceof Animal );
|
||||
alert( rabbit instanceof Object );
|
||||
```
|
||||
|
82
1-js/9-object-inheritance/16-instanceof/article.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
# Проверка класса: "instanceof"
|
||||
|
||||
Оператор `instanceof` позволяет проверить, какому классу принадлежит объект, с учетом прототипного наследования.
|
||||
|
||||
[cut]
|
||||
|
||||
## Алгоритм работы instanceof [#ref-instanceof]
|
||||
|
||||
Вызов `obj instanceof Constructor` возвращает `true`, если объект принадлежит классу `Constructor` или классу, наследующему от него.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
|
||||
*!*
|
||||
// создаём объект
|
||||
*/!*
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
// проверяем -- этот объект создан Rabbit?
|
||||
*!*
|
||||
alert( rabbit instanceof Rabbit ); // true, верно
|
||||
*/!*
|
||||
```
|
||||
|
||||
Массив `arr` принадлежит классу `Array`, но также и является объектом `Object`. Это верно, так как массивы наследуют от объектов:
|
||||
|
||||
```js run
|
||||
var arr = [];
|
||||
alert( arr instanceof Array ); // true
|
||||
alert( arr instanceof Object ); // true
|
||||
```
|
||||
|
||||
Как это часто бывает в JavaScript, здесь есть ряд тонкостей. Проверка происходит через сравнение прототипов, поэтому в некоторых ситуациях может даже ошибаться!
|
||||
|
||||
**Алгоритм проверки `obj instanceof Constructor`:**
|
||||
|
||||
1. Получить `obj.__proto__`
|
||||
2. Сравнить `obj.__proto__` с `Constructor.prototype`
|
||||
3. Если не совпадает, тогда заменить `obj` на `obj.__proto__` и повторить проверку на шаге 2 до тех пор, пока либо не найдется совпадение (результат `true`), либо цепочка прототипов не закончится (результат `false`).
|
||||
|
||||
В проверке `rabbit instanceof Rabbit` совпадение происходит на первом же шаге этого алгоритма, так как: `rabbit.__proto__ == Rabbit.prototype`.
|
||||
|
||||
А если рассмотреть `arr instanceof Object`, то совпадение будет найдено на следующем шаге, так как `arr.__proto__.__proto__ == Object.prototype`.
|
||||
|
||||
Забавно, что сама функция-конструктор не участвует в процессе проверки! Важна только цепочка прототипов для проверяемого объекта.
|
||||
|
||||
Это может приводить к забавному результату и даже ошибкам в проверке при изменении `prototype`, например:
|
||||
|
||||
```js run
|
||||
// Создаём объект rabbit, как обычно
|
||||
function Rabbit() {}
|
||||
var rabbit = new Rabbit();
|
||||
|
||||
// изменили prototype...
|
||||
Rabbit.prototype = {};
|
||||
|
||||
// ...instanceof перестал работать!
|
||||
*!*
|
||||
alert( rabbit instanceof Rabbit ); // false
|
||||
*/!*
|
||||
```
|
||||
|
||||
Стоит ли говорить, что это один из доводов для того, чтобы никогда не менять `prototype`? Так сказать, во избежание.
|
||||
|
||||
```warn header="Не друзья: `instanceof` и фреймы"
|
||||
Оператор `instanceof` не срабатывает, когда значение приходит из другого окна или фрейма.
|
||||
|
||||
Например, массив, который создан в ифрейме и передан родительскому окну -- будет массивом *в том ифрейме*, но не в родительском окне. Проверка `instanceof Array` в родительском окне вернёт `false`.
|
||||
|
||||
Вообще, у каждого окна и фрейма -- своя иерархия объектов и свой `window` .
|
||||
|
||||
Как правило, эта проблема возникает со встроенными объектами, в этом случае используется проверка внутреннего свойства `[[Class]]`, которое подробнее описано в главе <info:class-instanceof>.
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
- Оператор `obj instanceof Func` проверяет тот факт, что `obj` является результатом вызова `new Func`. Он учитывает цепочку `__proto__`, поэтому наследование поддерживается.
|
||||
- Оператор `instanceof` не сможет проверить тип значения, если объект создан в одном окне/фрейме, а проверяется в другом. Это потому, что в каждом окне -- своя иерархия объектов. Для точной проверки типов встроенных объектов можно использовать свойство `[[Class]]`.
|
||||
|
||||
Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования.
|