up
|
@ -0,0 +1,311 @@
|
|||
|
||||
# Property flags and descriptors
|
||||
|
||||
As we know, objects can store properties.
|
||||
|
||||
Till now, a property was a simple "key-value" pair to us. But an object property is actually more complex and tunable thing.
|
||||
|
||||
[cut]
|
||||
|
||||
## Property flags
|
||||
|
||||
Object properties, besides a **`value`**, have three special attributes (so-called "flags"):
|
||||
|
||||
- **`writable`** -- if `true`, can be changed, otherwise it's read-only.
|
||||
- **`enumerable`** -- if `true`, then listed in loops, otherwise not listed.
|
||||
- **`configurable`** -- if `true`, the property can be deleted and these attributes can be modified, otherwise not.
|
||||
|
||||
We didn't see them yet, because generally they do not show up. When we create a property "the usual way", all of them are `true`. But we also can change them any time.
|
||||
|
||||
First, let's see how to get those flags.
|
||||
|
||||
The method [Object.getOwnPropertyDescriptor](mdn:js/Object/getOwnPropertyDescriptor) allows to query the *full* information about a property.
|
||||
|
||||
The syntax is:
|
||||
```js
|
||||
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
|
||||
```
|
||||
|
||||
`obj`
|
||||
: The object to get information from.
|
||||
|
||||
`propertyName`
|
||||
: The name of the property.
|
||||
|
||||
The returned value is a so-called "property descriptor" object: it contains the value and all the flags.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
|
||||
|
||||
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||
/* property descriptor:
|
||||
{
|
||||
"value": "John",
|
||||
"writable": true,
|
||||
"enumerable": true,
|
||||
"configurable": true
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
To change the flags, we can use [Object.defineProperty](mdn:js/Object/defineProperty).
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
Object.defineProperty(obj, propertyName, descriptor)
|
||||
```
|
||||
|
||||
`obj`, `propertyName`
|
||||
: The object and property to work on.
|
||||
|
||||
`descriptor`
|
||||
: Property descriptor to apply.
|
||||
|
||||
If the property exist, it updates its flags, otherwise, it creates the property with the given value and flags. Please note, that if a flag is not supplied, it is assumed `false`.
|
||||
|
||||
For instance, here a property `name` is created with all falsy flags:
|
||||
|
||||
```js run
|
||||
let user = {};
|
||||
|
||||
*!*
|
||||
Object.defineProperty(user, "name", {
|
||||
value: "John"
|
||||
});
|
||||
*/!*
|
||||
|
||||
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
|
||||
|
||||
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||
/*
|
||||
{
|
||||
"value": "John",
|
||||
*!*
|
||||
"writable": false,
|
||||
"enumerable": false,
|
||||
"configurable": false
|
||||
*/!*
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
Compare it with "normally created" user.name above: now all flags are falsy. If that's not what we want then we'd better to set them to `true` in the `descriptor`.
|
||||
|
||||
Now let's see effects of the flags by example.
|
||||
|
||||
## Read-only
|
||||
|
||||
Let's make `user.name` read-only by changing `writable` flag:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John"
|
||||
};
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
*!*
|
||||
writable: false
|
||||
*/!*
|
||||
});
|
||||
|
||||
*!*
|
||||
user.name = "Pete"; // Error: Cannot assign to read only property 'name'...
|
||||
*/!*
|
||||
```
|
||||
|
||||
Now no one can change the name of our user, unless he applies his own `defineProperty` to override ours.
|
||||
|
||||
Here's the same operation, but for the case when a property doesn't exist:
|
||||
|
||||
```js run
|
||||
let user = { };
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
*!*
|
||||
value: "Pete",
|
||||
// for new properties need to explicitly list what's true
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
*/!*
|
||||
});
|
||||
|
||||
alert(user.name); // Pete
|
||||
user.name = "Alice"; // Error
|
||||
```
|
||||
|
||||
|
||||
## Non-enumerable
|
||||
|
||||
Now let's a custom `toString` to `user`.
|
||||
|
||||
Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. But if we add `toString` of our own, then by default it shows up in `for..in`.
|
||||
|
||||
...But if we don't like it, then we can set `enumerable:false`. Then it won't appear in `for..in` loop, just like the built-in one:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
toString() {
|
||||
return this.name;
|
||||
}
|
||||
};
|
||||
|
||||
// By default, both our properties are listed:
|
||||
for(let key in user) alert(key); // name, toString
|
||||
|
||||
Object.defineProperty(user, "toString", {
|
||||
*!*
|
||||
enumerable: false
|
||||
*/!*
|
||||
});
|
||||
|
||||
*!*
|
||||
// Now toString disappears:
|
||||
*/!*
|
||||
for(let key in user) alert(key); // name
|
||||
```
|
||||
|
||||
Non-enumerable properties are also excluded from `Object.keys`:
|
||||
|
||||
```js
|
||||
alert(Object.keys(user)); // name
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Non-configurable
|
||||
|
||||
The non-configurable flag (`configurable:false`) is usually set for built-in objects and properties.
|
||||
|
||||
A non-configurable property can not be deleted or altered with `defineProperty`.
|
||||
|
||||
For instance, `Math.PI` is both read-only, non-enumerable and non-configurable:
|
||||
|
||||
```js run
|
||||
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
|
||||
|
||||
alert( JSON.stringify(descriptor, null, 2 ) );
|
||||
/*
|
||||
{
|
||||
"value": 3.141592653589793,
|
||||
"writable": false,
|
||||
"enumerable": false,
|
||||
"configurable": false
|
||||
}
|
||||
*/
|
||||
```
|
||||
So, a programmer is unable to change the value of `Math.PI` or overwrite it.
|
||||
|
||||
```js run
|
||||
Math.PI = 3; // Error
|
||||
|
||||
// delete Math.PI won't work either
|
||||
```
|
||||
|
||||
Making a property non-configurable is one-way road. We cannot change it back, because `defineProperty` doesn't work on non-configurable properties.
|
||||
|
||||
Here we are making `user.name` a "forever sealed" constant:
|
||||
|
||||
```js run
|
||||
let user = { };
|
||||
|
||||
Object.defineProperty(user, "name", {
|
||||
value: "John",
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
*!*
|
||||
// won't be able to change user.name or its flags
|
||||
// all this won't work:
|
||||
// user.name = "Pete"
|
||||
// delete user.name
|
||||
// defineProperty(user, "name", ...)
|
||||
Object.defineProperty(user, "name", {writable: true}); // Error
|
||||
*/!*
|
||||
```
|
||||
|
||||
```smart header="Errors appear only in use strict"
|
||||
In the non-strict mode, no errors occur when writing to read-only properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict.
|
||||
```
|
||||
|
||||
## Object.defineProperties
|
||||
|
||||
There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
Object.defineProperties(obj, {
|
||||
prop1: descriptor1,
|
||||
prop2: descriptor2
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
Object.defineProperties(user, {
|
||||
name: { value: "John", writable: false },
|
||||
surname: { value: "Smith", writable: false },
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
So, we can set many properties at once.
|
||||
|
||||
## Object.getOwnPropertyDescriptors
|
||||
|
||||
To get many descriptors at once, we can use the method [Object.getOwnPropertyDescriptors(obj)](mdn:js/Object/getOwnPropertyDescriptors).
|
||||
|
||||
Together with `Object.defineProperties` it can be used as an "flags-aware" way of cloning an object:
|
||||
|
||||
```js
|
||||
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
|
||||
```
|
||||
|
||||
Normally when we clone an object, we use an assignment to copy properties, like this:
|
||||
|
||||
```js
|
||||
for(let key in user) {
|
||||
clone[key] = user[key]
|
||||
}
|
||||
```
|
||||
|
||||
...But that does not copy flags. So if we want a "better" clone then `Object.defineProperties` is preferred.
|
||||
|
||||
## Sealing an object globally
|
||||
|
||||
Property descriptors work at the level of individual properties.
|
||||
|
||||
There are also methods that limit access to the *whole* object:
|
||||
|
||||
[Object.preventExtensions(obj)](mdn:js/Object/preventExtensions)
|
||||
: Forbids to add properties to the object.
|
||||
|
||||
[Object.seal(obj)](mdn:js/Object/seal)
|
||||
: Forbids to add/remove properties, sets for all existing properties `configurable: false`.
|
||||
|
||||
[Object.freeze(obj)](mdn:js/Object/freeze)
|
||||
: Forbids to add/remove/change properties, sets for all existing properties `configurable: false, writable: false`.
|
||||
|
||||
And also there are tests for them:
|
||||
|
||||
[Object.isExtensible(obj)](mdn:js/Object/isExtensible)
|
||||
: Returns `false` if adding properties is forbidden, otherwise `true`.
|
||||
|
||||
[Object.isSealed(obj)](mdn:js/Object/isSealed)
|
||||
: Returns `true` if adding/removing properties is forbidden, and all existing properties have `configurable: false`.
|
||||
|
||||
[Object.isFrozen(obj)](mdn:js/Object/isFrozen)
|
||||
: Returns `true` if adding/removing/changing properties is forbidden, and all current properties are `configurable: false, writable: false`.
|
||||
|
||||
These methods are rarely used in practice.
|
|
@ -0,0 +1,239 @@
|
|||
|
||||
# Property getters and setters
|
||||
|
||||
There are two kinds of properties.
|
||||
|
||||
The first kind is *data properties*. We already know how to work with them. Actually, all properties that we've been using till now were data properties.
|
||||
|
||||
The second type of properties is something new. It's *accessor properties*. They are essentially functions that work on getting and setting a value, but look like regular properties to an external code.
|
||||
|
||||
[cut]
|
||||
|
||||
## Getters and setters
|
||||
|
||||
Accessor properties are represented by "getter" and "setter" methods. In an object literal they are denoted by `get` and `set`:
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
*!*get propName()*/!* {
|
||||
// getter, the code executed on getting obj.propName
|
||||
},
|
||||
|
||||
*!*set propName(value)*/!* {
|
||||
// setter, the code executed on setting obj.propName = value
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The getter works when `obj.propName` is read, the setter -- when it is assigned.
|
||||
|
||||
For instance, we have a `user` object with `name` and `surname`:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith"
|
||||
};
|
||||
```
|
||||
|
||||
Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith",
|
||||
|
||||
*!*
|
||||
get fullName() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
}
|
||||
*/!*
|
||||
};
|
||||
|
||||
*!*
|
||||
alert(user.fullName); // John Smith
|
||||
*/!*
|
||||
```
|
||||
|
||||
From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't call `user.fullName` as a function, we read it normally: the getter runs behind the scenes.
|
||||
|
||||
As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error.
|
||||
|
||||
Let's fix it by adding a setter for `user.fullName`:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith",
|
||||
|
||||
get fullName() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
},
|
||||
|
||||
*!*
|
||||
set fullName(value) {
|
||||
[this.name, this.surname] = value.split(" ");
|
||||
}
|
||||
*/!*
|
||||
};
|
||||
|
||||
// set fullName is executed with the given value.
|
||||
user.fullName = "Alice Cooper";
|
||||
|
||||
alert(user.name); // Alice
|
||||
alert(user.surname); // Cooper
|
||||
```
|
||||
|
||||
Now we have a "virtual" property. It is readable and writable, but in fact does not exist.
|
||||
|
||||
```smart header="Accessor properties are only accessible with get/set"
|
||||
A property can either be a "data property" or an "accessor property", but not both.
|
||||
|
||||
Once a property is defined with `get prop()` or `set prop()`, it's an accessor property. So there must be a getter to read it, and must be a setter if we want to assign it.
|
||||
|
||||
Sometimes it's normal that there's only a setter or only a getter. But the property won't be readable or writable in that case.
|
||||
```
|
||||
|
||||
|
||||
## Accessor descriptors
|
||||
|
||||
Descriptors for accessor properties are different.
|
||||
|
||||
For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions.
|
||||
|
||||
So an accessor descriptor may have:
|
||||
|
||||
- **`get`** -- a function without arguments, that works when a property is read,
|
||||
- **`set`** -- a function with one argument, that is called when the property is set,
|
||||
- **`enumerable`** -- same as for data properties,
|
||||
- **`configurable`** -- same as for data properties.
|
||||
|
||||
For instance, to create an accessor `fullName` with `defineProperty`, we can pass a descriptor with `get` and `set`:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
name: "John",
|
||||
surname: "Smith"
|
||||
};
|
||||
|
||||
*!*
|
||||
Object.defineProperty(user, 'fullName', {
|
||||
get() {
|
||||
return `${this.name} ${this.surname}`;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
[this.name, this.surname] = value.split(" ");
|
||||
}
|
||||
*/!*
|
||||
});
|
||||
|
||||
alert(user.fullName); // John Smith
|
||||
|
||||
for(let key in user) alert(key);
|
||||
```
|
||||
|
||||
Please note once again that a property can be either an accessor or a data property, not both.
|
||||
|
||||
If we try to supply both `get` and `value` in the same descriptor, there will be an error:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
// Error: Invalid property descriptor.
|
||||
*/!*
|
||||
Object.defineProperty({}, 'prop', {
|
||||
get() {
|
||||
return 1
|
||||
},
|
||||
|
||||
value: 2
|
||||
});
|
||||
```
|
||||
|
||||
## Smarter getters/setters
|
||||
|
||||
Getters/setters can be used as wrappers over "real" property values to gain more control over them.
|
||||
|
||||
For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`. And filter assignments in the setter:
|
||||
|
||||
```js run
|
||||
let user = {
|
||||
get name() {
|
||||
return this._name;
|
||||
},
|
||||
|
||||
set name(value) {
|
||||
if (value.length < 4) {
|
||||
alert("Name is too short, need at least 4 characters");
|
||||
return;
|
||||
}
|
||||
this._name = value;
|
||||
}
|
||||
};
|
||||
|
||||
user.name = "Pete";
|
||||
alert(user.name); // Pete
|
||||
|
||||
user.name = ""; // Name is too short...
|
||||
```
|
||||
|
||||
Technically, the external code may still access the name directly by using `user._name`. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside the object.
|
||||
|
||||
|
||||
## Using for compatibility
|
||||
|
||||
One of great ideas behind getters and setters -- they allow to take control over a "normal" data property and tweak it at any moment.
|
||||
|
||||
For instance, we started implementing user objects using data properties `name` and `age`:
|
||||
|
||||
```js
|
||||
function User(name, age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
let john = new User("John", 25);
|
||||
|
||||
alert( john.age ); // 25
|
||||
```
|
||||
|
||||
...But sooner or later, things may change. Instead of `age` we may decide to store `birthday`, because it's more precise and convenient:
|
||||
|
||||
```js
|
||||
function User(name, birthday) {
|
||||
this.name = name;
|
||||
this.birthday = birthday;
|
||||
}
|
||||
|
||||
let john = new User("John", new Date(1992, 6, 1));
|
||||
```
|
||||
|
||||
Now what to do with the old code that still uses `age` property?
|
||||
|
||||
We can try to find all such places and fix them, but that takes time and can be hard to do if that code is written by other people. And besides, `age` is a nice thing to have in `user`, right? In some places it's just what we want.
|
||||
|
||||
Adding a getter for `age` mitigates the problem:
|
||||
|
||||
```js run no-beautify
|
||||
function User(name, birthday) {
|
||||
this.name = name;
|
||||
this.birthday = birthday;
|
||||
|
||||
*!*
|
||||
// age is calculated from the current date and birthday
|
||||
Object.defineProperty(this, "age", {
|
||||
get() {
|
||||
let todayYear = new Date().getFullYear();
|
||||
return todayYear - this.birthday.getFullYear();
|
||||
}
|
||||
});
|
||||
*/!*
|
||||
}
|
||||
|
||||
let john = new User("John", new Date(1992, 6, 1));
|
||||
|
||||
alert( john.birthday ); // birthday is available
|
||||
alert( john.age ); // ...as well as the age
|
||||
```
|
||||
|
||||
Now the old code works too and we've got a nice additional property.
|
|
@ -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
|
||||
```
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# Prototypal inheritance
|
||||
|
||||
In programming, we often want to take something and extend it.
|
||||
|
||||
For instance, we have a `user` object with its properties and methods, and want to make `admin` and `guest` as slightly modified variants 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.
|
||||
|
||||
*Prototypal inheritance* is a language feature that helps in that.
|
||||
|
||||
|
||||
[cut]
|
||||
|
||||
## [[Prototype]]
|
||||
|
||||
In JavaScript, objects have a special hidden property `[[Prototype]]` (as named in the specification), that is either `null` or references another object. That object is called "a prototype":
|
||||
|
||||

|
||||
|
||||
That `[[Prototype]]` has a "magical" meaning. When we want to read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, such thing is called a "prototypal inheritance". Many cool language features and approaches are based on it.
|
||||
|
||||
The property `[[Prototype]]` is internal and hidden, but there are many ways to set it.
|
||||
|
||||
One of them is to use `__proto__`, like this:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true
|
||||
};
|
||||
let rabbit = {
|
||||
jumps: true
|
||||
};
|
||||
|
||||
*!*
|
||||
rabbit.__proto__ = animal;
|
||||
*/!*
|
||||
```
|
||||
|
||||
Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. We'll talk about other ways of setting it later, as for now `__proto__` will do just fine.
|
||||
|
||||
So now if we look for something in `rabbit` and it's missing, JavaScript automatically takes it from `animal`.
|
||||
|
||||
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 the line `(*)` sets `animal` to be a prototype of `rabbit`.
|
||||
|
||||
Then, 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".
|
||||
|
||||
If we have a method in `animal`, it can be called on `rabbit`:
|
||||
|
||||
```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 we try to assign `__proto__` in circle.
|
||||
2. The value of `__proto__` can be either an object or `null`. All other values (like primitives) 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 for data properties works directly with the object.
|
||||
|
||||
In the example below, we assign its 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!
|
||||
```
|
||||
|
||||
Since now, `rabbit.walk()` call finds the method immediately in the object and executes it, without using the prototype:
|
||||
|
||||

|
||||
|
||||
The assignment handling is different for accessor properties, because these properties behave more like functions. For instance, check out `admin.fullName` property in the code below:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Here in the line `(*)` the property `admin.fullName` has a setter in the prototype `user`. So it is not written into `admin`. Instead, the inherited setter is called.
|
||||
|
||||
So, the general rule would be:
|
||||
|
||||
1. For accessor properties use a setter (from the prototype chain).
|
||||
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(value)`? 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()` sets `this.isSleeping` on the `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();
|
||||
|
||||
alert(rabbit.isSleeping); // true
|
||||
alert(animal.isSleeping); // undefined (no such property in the prototype)
|
||||
```
|
||||
|
||||
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 are shared, but the state will be not.
|
||||
|
||||
## Summary
|
||||
|
||||
- In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`.
|
||||
- We can use `obj.__proto__` to access it (there are other ways too, to be covered soon).
|
||||
- The object references by `[[Prototype]]` is called a "prototype".
|
||||
- If we want to read a property of `obj` or call a method, and it doesn't exist, then JavaScript tries to find it in the prototype. Write/delete operations work directly on the object, they don't use the prototype (unless the property is actually a setter).
|
||||
- If we call `obj.method()`, and the `method` is taken from the prototype, `this` still references `obj`. So methods always work with the current objects even if they are inherited.
|
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 15 KiB |
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 |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1,20 @@
|
|||
|
||||
Answers:
|
||||
|
||||
1. `true`.
|
||||
|
||||
The assignment to `Rabbit.prototype` sets up `[[Prototype]]` for new objects, but it does not affect the existing ones.
|
||||
|
||||
2. `false`.
|
||||
|
||||
Objects are assigned by reference. The object from `Rabbit.prototype` is not duplicated, it's still a single object is referenced both by `Rabbit.prototype` and by the `[[Prototype]]` of `rabbit`.
|
||||
|
||||
So when we change its content through one reference, it is visible through the other one.
|
||||
|
||||
3. `true`.
|
||||
|
||||
All `delete` operations are applied directly to the object. Here `delete rabbit.eats` tries to remove `eats` property from `rabbit`, but it doesn't have it. So the operation won't have any effect.
|
||||
|
||||
4. `undefined`.
|
||||
|
||||
The property `eats` is deleted from the prototype, it doesn't exist any more.
|
|
@ -0,0 +1,89 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Changing "prototype"
|
||||
|
||||
In the code below we create `new Rabbit`, and then try to modify its prototype.
|
||||
|
||||
In the start, we have this code:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
alert( rabbit.eats ); // true
|
||||
```
|
||||
|
||||
|
||||
1. We added one more string (emphasized), what `alert` shows now?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = {};
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats ); // ?
|
||||
```
|
||||
|
||||
2. ...And if the code is like this (replaced one line)?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
Rabbit.prototype.eats = false;
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats ); // ?
|
||||
```
|
||||
|
||||
3. Like this (replaced one line)?
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
delete rabbit.eats;
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats ); // ?
|
||||
```
|
||||
|
||||
4. The last variant:
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
Rabbit.prototype = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit();
|
||||
|
||||
*!*
|
||||
delete Rabbit.prototype.eats;
|
||||
*/!*
|
||||
|
||||
alert( rabbit.eats ); // ?
|
||||
```
|
|
@ -0,0 +1,44 @@
|
|||
We can use such approach if we are sure that `"constructor"` property has the correct value.
|
||||
|
||||
For instance, if we don't touch the default `"prototype"`, then this code works for sure:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
let user = new User('John');
|
||||
let user2 = new user.constructor('Pete');
|
||||
|
||||
alert( user2.name ); // Pete (worked!)
|
||||
```
|
||||
|
||||
It worked, because `User.prototype.constructor == User`.
|
||||
|
||||
..But if someone, so to say, overwrites `User.prototype` and forgets to recreate `"constructor"`, then it would fail.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.name = name;
|
||||
}
|
||||
*!*
|
||||
User.prototype = {}; // (*)
|
||||
*/!*
|
||||
|
||||
let user = new User('John');
|
||||
let user2 = new user.constructor('Pete');
|
||||
|
||||
alert( user2.name ); // undefined
|
||||
```
|
||||
|
||||
Why `user2.name` is `undefined`?
|
||||
|
||||
Here's how `new user.constructor('Pete')` works:
|
||||
|
||||
1. First, it looks for `constructor` in `user`. Nothing.
|
||||
2. Then it follows the prototype chain. The prototype of `user` is `User.prototype`, and it also has nothing.
|
||||
3. The value of `User.prototype` is a plain object `{}`, its prototype is `Object.prototype`. And there is `Object.prototype.constructor == Object`. So it is used.
|
||||
|
||||
At the end, we have `let user2 = new Object('Pete')`. The built-in `Object` constructor ignores arguments, it always creates an empty object -- that's what we have in `user2` after all.
|
|
@ -0,0 +1,15 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Create an object with the same constructor
|
||||
|
||||
Imagine, we have an arbitrary object `obj`, created by a constructor function -- we don't know which one, but we'd like to create a new object using it.
|
||||
|
||||
Can we do it like that?
|
||||
|
||||
```js
|
||||
let obj2 = new obj.constructor();
|
||||
```
|
||||
|
||||
Give an example of a constructor function for `obj` which lets such code work right. And an example that makes it work wrong.
|
|
@ -0,0 +1,159 @@
|
|||
# F.prototype
|
||||
|
||||
In modern JavaScript we can set a prototype using `__proto__`. But it wasn't like that all the time.
|
||||
|
||||
[cut]
|
||||
|
||||
JavaScript has had prototypal inheritance from the beginning. It was one of the core features of the language.
|
||||
|
||||
But in the old times, there was another (and the only) way to set it: to use a `"prototype"` property of the constructor function. And there are still many scripts that use it.
|
||||
|
||||
## The "prototype" property
|
||||
|
||||
As we know already, `new F()` creates a new object. But what we didn't use yet `F.prototype` property.
|
||||
|
||||
That property is used by the JavaScript itself to set `[[Prototype]]` for new objects.
|
||||
|
||||
**When a new object is created with `new F()`, the `[[Prototype]]` of it is set to `F.prototype`.**
|
||||
|
||||
Please note that `F.prototype` here means a regular property named `"prototype"` on `F`. It sounds something similar to the term "prototype", but here we really mean a regular property with this name.
|
||||
|
||||
Here's the example:
|
||||
|
||||
```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 states the following: "When a `new Rabbit` is created, assign its `[[Prototype]]` to `animal`".
|
||||
|
||||
That's the resulting picture:
|
||||
|
||||

|
||||
|
||||
On the picture, `"prototype"` is a horizontal arrow, it's a regular property, and `[[Prototype]]` is vertical, meaning the inheritance of `rabbit` from `animal`.
|
||||
|
||||
|
||||
## Default F.prototype, constructor property
|
||||
|
||||
Every function has the `"prototype"` property even if we don't supply it.
|
||||
|
||||
The default `"prototype"` is an object with the only property `constructor` that points back to the function itself.
|
||||
|
||||
Like this:
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
|
||||
/* default prototype
|
||||
Rabbit.prototype = { constructor: Rabbit };
|
||||
*/
|
||||
```
|
||||
|
||||

|
||||
|
||||
We can check it:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
// by default:
|
||||
// Rabbit.prototype = { constructor: Rabbit }
|
||||
|
||||
alert( Rabbit.prototype.constructor == Rabbit ); // true
|
||||
```
|
||||
|
||||
Naturally, it we do nothing, the `constructor` property is available to all rabbits through `[[Prototype]]`:
|
||||
|
||||
```js run
|
||||
function Rabbit() {}
|
||||
// by default:
|
||||
// Rabbit.prototype = { constructor: Rabbit }
|
||||
|
||||
let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
|
||||
|
||||
alert(rabbit.constructor == Rabbit); // true (from prototype)
|
||||
```
|
||||
|
||||

|
||||
|
||||
We can use `constructor` to create a new object using the same constructor as the existing one.
|
||||
|
||||
Like here:
|
||||
|
||||
```js run
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
alert(name);
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("White Rabbit");
|
||||
|
||||
let rabbit2 = new rabbit.constructor("Black Rabbit");
|
||||
```
|
||||
|
||||
That's handy when we have an object, don't know which constructor was used for it (e.g. it comes from a 3rd party library), and we need to create the same.
|
||||
|
||||
...But probably the most important thing about `"constructor"` is that...
|
||||
|
||||
**JavaScript itself does not ensure the right `"constructor"` at all.**
|
||||
|
||||
Yes, it exists in the default `"prototype"` for functions, but that's all. It is created automatically, but what happens with it later -- is totally on us.
|
||||
|
||||
In particular, if we replace the default prototype by assigning our own `Rabbit.prototype = { jumps: true }`, then there will be no `"constructor"` in it.
|
||||
|
||||
But we may want to keep `"constructor"` for convenience by adding properties to the default `"prototype"` instead of overwriting it as a whole:
|
||||
|
||||
```js
|
||||
function Rabbit() {}
|
||||
|
||||
// Not overwrite Rabbit.prototype totally
|
||||
// just add to it
|
||||
Rabbit.prototype.jumps = true
|
||||
// the default Rabbit.prototype.constructor is preserved
|
||||
```
|
||||
|
||||
Or, alternatively, recreate it manually:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = {
|
||||
jumps: true,
|
||||
*!*
|
||||
constructor: Rabbit
|
||||
*/!*
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
In this chapter we briefly described the way of setting a `[[Prototype]]` for objects created via a constructor function. Later we'll see more advanced programming patterns that rely on it.
|
||||
|
||||
Everything is quite simple, just few notes to make things clear:
|
||||
|
||||
- The `F.prototype` property is not the same as `[[Prototype]]`.
|
||||
- The only thing `F.prototype` does: it sets `[[Prototype]]` of new objects when `new F()` is called.
|
||||
- The value of `F.prototype` should be either an object or null: other values won't work.
|
||||
- The `"prototype"` property only has such a special effect when is set to a constructor function, and it is invoked with `new`.
|
||||
|
||||
On regular objects this property does nothing. That's an ordinary property:
|
||||
```js
|
||||
let user = {
|
||||
name: "John",
|
||||
prototype: "Bla-bla" // no magic at all
|
||||
};
|
||||
```
|
||||
|
||||
By default all functions have `F.prototype = { constructor: F }`. So by default we can get the constructor of an object by accessing its `"constructor"` property.
|
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 105 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1,13 @@
|
|||
|
||||
|
||||
```js run
|
||||
Function.prototype.defer = function(ms) {
|
||||
setTimeout(this, ms);
|
||||
};
|
||||
|
||||
function f() {
|
||||
alert("Hello!");
|
||||
}
|
||||
|
||||
f.defer(1000); // shows "Hello!" after 1 sec
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Add method "f.defer(ms)" to functions
|
||||
|
||||
Add to the prototype of all functions the method `defer(ms)`, that runs the function after `ms` milliseconds.
|
||||
|
||||
After you do it, such code should work:
|
||||
|
||||
```js
|
||||
function f() {
|
||||
alert("Hello!");
|
||||
}
|
||||
|
||||
f.defer(1000); // shows "Hello!" after 1 second
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
|
||||
```js run
|
||||
Function.prototype.defer = function(ms) {
|
||||
let f = this;
|
||||
return function(...args) {
|
||||
setTimeout(() => f.apply(this, args), ms);
|
||||
}
|
||||
};
|
||||
|
||||
// check it
|
||||
function f(a, b) {
|
||||
alert( a + b );
|
||||
}
|
||||
|
||||
f.defer(1000)(1, 2); // shows 3 after 1 sec
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Add the decorating "defer()" to functions
|
||||
|
||||
Add to the prototype of all functions the method `defer(ms)`, that returns a wrapper, delaying the call by `ms` milliseconds.
|
||||
|
||||
Here's an example of how it should work:
|
||||
|
||||
```js
|
||||
function f(a, b) {
|
||||
alert( a + b );
|
||||
}
|
||||
|
||||
f.defer(1000)(1, 2); // shows 3 after 1 second
|
||||
```
|
||||
|
||||
Please note that the arguments should be passed to the original function.
|
|
@ -0,0 +1,180 @@
|
|||
# Native prototypes
|
||||
|
||||
The `"prototype"` property is widely used by the core of JavaScript itself. All built-in constructor functions use it.
|
||||
|
||||
We'll see how it is for plain objects first, and then for more complex ones.
|
||||
|
||||
## Object.prototype
|
||||
|
||||
Let's say we output an empty object:
|
||||
|
||||
```js run
|
||||
let obj = {};
|
||||
alert( obj ); // "[object Object]" ?
|
||||
```
|
||||
|
||||
Where's the code that generates the string `"[object Object]"`? That's a built-in `toString` method, but where is it? The `obj` is empty!
|
||||
|
||||
...But the short notation `obj = {}` is the same as `obj = new Object()`, where `Object` -- is a built-in object constructor function. And that function has `Object.prototype` that references a huge object with `toString` and other functions.
|
||||
|
||||
Like this (all that is built-in):
|
||||
|
||||

|
||||
|
||||
When `new Object()` is called (or a literal object `{...}` is created), the `[[Prototype]]` of it is set to `Object.prototype` by the rule that we've discussed in the previous chapter:
|
||||
|
||||

|
||||
|
||||
Afterwards when `obj.toString()` is called -- the method is taken from `Object.prototype`.
|
||||
|
||||
We can check it like this:
|
||||
|
||||
```js run
|
||||
let obj = {};
|
||||
|
||||
alert(obj.__proto__ === Object.prototype); // true
|
||||
// obj.toString === obj.__proto__toString == Object.prototype.toString
|
||||
```
|
||||
|
||||
Please note that there is no additional `[[Prototype]]` in the chain above `Object.prototype`:
|
||||
|
||||
```js run
|
||||
alert(Object.prototype.__proto__); // null
|
||||
```
|
||||
|
||||
## Other built-in prototypes
|
||||
|
||||
Other built-in objects such as `Array`, `Date`, `Function` and others also keep methods in prototypes.
|
||||
|
||||
For instance, when we create an array `[1, 2, 3]`, the default `new Array()` constructor is used internally. So the array data is written into the new object, and `Array.prototype` becomes its prototype and provides methods. That's very memory-efficient.
|
||||
|
||||
By specification, all built-in prototypes have `Object.prototype` on the top. Sometimes people say that "everything inherits from objects".
|
||||
|
||||
Here's the overall picture (for 3 built-ins to fit):
|
||||
|
||||

|
||||
|
||||
Let's check the prototypes manually:
|
||||
|
||||
```js run
|
||||
let arr = [1, 2, 3];
|
||||
|
||||
// it inherits from Array.prototype?
|
||||
alert( arr.__proto__ === Array.prototype ); // true
|
||||
|
||||
// then from Object.prototype?
|
||||
alert( arr.__proto__.__proto__ === Object.prototype ); // true
|
||||
|
||||
// and null on the top.
|
||||
alert( arr.__proto__.__proto__.__proto__ ); // null
|
||||
```
|
||||
|
||||
Some methods in prototypes may overlap, for instance, `Array.prototype` has its own `toString` that lists comma-delimited elements:
|
||||
|
||||
```js run
|
||||
let arr = [1, 2, 3]
|
||||
alert(arr); // 1,2,3 <-- the result of Array.prototype.toString
|
||||
```
|
||||
|
||||
As we've seen before, `Object.prototype` has `toString` as well, but `Array.prototype` is closer in the chain, so the array variant is used.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
In-browser tools like Chrome developer console also show inheritance (may need to use `console.dir` for built-in objects):
|
||||
|
||||

|
||||
|
||||
Other built-in objects also work the same way. Even functions. They are objects of a built-in `Function` constructor, and their methods: `call/apply` and others are taken from `Function.prototype`. Functions have their own `toString` too.
|
||||
|
||||
```js run
|
||||
function f() {}
|
||||
|
||||
alert(f.__proto__ == Function.prototype); // true
|
||||
alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects
|
||||
```
|
||||
|
||||
## Primitives
|
||||
|
||||
The most intricate thing happens with strings, numbers and booleans.
|
||||
|
||||
As we remember, they are not objects. But if we try to access their properties, then temporary wrapper objects are created using built-in constructors `String`, `Number`, `Boolean`, they provide the methods and disappear.
|
||||
|
||||
These objects are created invisibly to us and most engines optimize them out, but the specification describes it exactly this way. Methods of these objects also reside in prototypes, available as `String.prototype`, `Number.prototype` and `Boolean.prototype`.
|
||||
|
||||
```warn header="Values `null` and `undefined` have no object wrappers"
|
||||
Special values `null` and `undefined` stand apart. They have no object wrappers, so methods and properties are not available for them. And there are no corresponding prototypes too.
|
||||
```
|
||||
|
||||
## Changing native prototypes [#native-prototype-change]
|
||||
|
||||
Native prototypes can be modified. For instance, if we add a method to `String.prototype`, it becomes available to all strings:
|
||||
|
||||
```js run
|
||||
String.prototype.show = function() {
|
||||
alert(this);
|
||||
};
|
||||
|
||||
"BOOM!".show(); // BOOM!
|
||||
```
|
||||
|
||||
During the process of development we may have ideas which new built-in methods we'd like to have. And there may be a slight temptation to add them to native prototypes. But that is generally a bad idea.
|
||||
|
||||
Prototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them overwrites the other one.
|
||||
|
||||
In modern programming, there is only one case when modifying native prototypes is approved. That's polyfills. In other words, if there's a method in JavaScript specification that is not yet supported by our JavaScript engine (or any of those that we want to support), then may implement it manually and populate the built-in prototype with it.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
if (!String.prototype.repeat) { // if there's no such method
|
||||
// add it to the prototype
|
||||
|
||||
String.prototype.repeat = function(n) {
|
||||
// repeat the string n times
|
||||
|
||||
// actually, the code should be more complex than that,
|
||||
// throw errors for negative values of "n"
|
||||
// the full algorithm is in the specification
|
||||
return new Array(n + 1).join(this);
|
||||
};
|
||||
}
|
||||
|
||||
alert( "La".repeat(3) ); // LaLaLa
|
||||
```
|
||||
|
||||
## Borrowing from prototypes
|
||||
|
||||
In the chapter <info:call-apply-decorators#method-borrowing> we talked about method borrowing:
|
||||
|
||||
```js run
|
||||
function showArgs() {
|
||||
*!*
|
||||
// borrow join from array and call in the context of arguments
|
||||
alert( [].join.call(arguments, " - ") );
|
||||
*/!*
|
||||
}
|
||||
|
||||
showList("John", "Pete", "Alice"); // John - Pete - Alice
|
||||
```
|
||||
|
||||
Because `join` resides in `Array.prototype`, we can call it from there directly and rewrite it as:
|
||||
|
||||
```js
|
||||
function showArgs() {
|
||||
*!*
|
||||
alert( Array.prototype.join.call(arguments, " - ") );
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
||||
That's more efficient, because evades creation of an extra array object `[]`. From the other side -- more letters to write it.
|
||||
|
||||
## Summary
|
||||
|
||||
- All built-in objects follow the same pattern:
|
||||
- The methods are stored in the prototype (`Array.prototype`, `Object.prototype`, `Date.prototype` etc).
|
||||
- The object itself stores only the data (array items, object properties, the date).
|
||||
- Primitives also store methods in prototypes of wrapper objects: `Number.prototype`, `String.prototype`, `Boolean.prototype`. There are no wrapper objects only for `undefined` and `null`.
|
||||
- Built-in prototypes can be modified or populated with new methods. But it's not recommended to change them. Probably the only allowable cause is when we add-in a new standard, but not yet supported by the engine JavaScript method.
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 105 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 4 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1,28 @@
|
|||
|
||||
The method can take all enumerable keys using `Object.keys` and output their list.
|
||||
|
||||
To make `toString` non-enumerable, let's define it using a property descriptor. The syntax of `Object.create` allows to provide an object with property descriptors as the second argument.
|
||||
|
||||
```js run
|
||||
*!*
|
||||
let dictionary = Object.create(null, {
|
||||
toString: { // define toString property
|
||||
value() { // with function value
|
||||
return Object.keys(this).join();
|
||||
}
|
||||
}
|
||||
});
|
||||
*/!*
|
||||
|
||||
dictionary.apple = "Apple";
|
||||
dictionary.__proto__ = "test";
|
||||
|
||||
// apple and __proto__ is in the loop
|
||||
for(let key in dictionary) {
|
||||
alert(key); // "apple", then "__proto__"
|
||||
}
|
||||
|
||||
// comma-separated list of properties by toString
|
||||
alert(dictionary); // "apple,__proto__"
|
||||
```
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Add toString to the dictionary
|
||||
|
||||
There's an object `dictionary`, suited to store any `key/value` pairs.
|
||||
|
||||
Add method `dictionary.toString()` into it, that should return a comma-delimited list of keys. Your `toString` should not show up in `for..in` over the object.
|
||||
|
||||
Here's how it should work:
|
||||
|
||||
```js
|
||||
let dictionary = Object.create(null);
|
||||
|
||||
*!*
|
||||
// your code to add toString
|
||||
*/!*
|
||||
|
||||
// add some data
|
||||
dictionary.apple = "Apple";
|
||||
dictionary.__proto__ = "test"; // __proto__ is a regular property key here
|
||||
|
||||
// only apple and __proto__ are in the loop
|
||||
for(let key in dictionary) {
|
||||
alert(key); // "apple", then "__proto__"
|
||||
}
|
||||
|
||||
// your toString in action
|
||||
alert(dictionary); // "apple,__proto__"
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
The first call has `this == rabbit`, the other ones have `this` equal to `Rabbit.prototype`, because it's actually the object before the dot.
|
||||
|
||||
So only the first call shows `Rabbit`, other ones show `undefined`:
|
||||
|
||||
```js run
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype.sayHi = function() {
|
||||
alert( this.name );
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("Rabbit");
|
||||
|
||||
rabbit.sayHi(); // Rabbit
|
||||
Rabbit.prototype.sayHi(); // undefined
|
||||
Object.getPrototypeOf(rabbit).sayHi(); // undefined
|
||||
rabbit.__proto__.sayHi(); // undefined
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# The difference beteeen calls
|
||||
|
||||
Let's create a new `rabbit` object:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
Rabbit.prototype.sayHi = function() {
|
||||
alert(this.name);
|
||||
}
|
||||
|
||||
let rabbit = new Rabbit("Rabbit");
|
||||
```
|
||||
|
||||
These calls do the same thing or not?
|
||||
|
||||
```js
|
||||
rabbit.sayHi();
|
||||
Rabbit.prototype.sayHi();
|
||||
Object.getPrototypeOf(rabbit).sayHi();
|
||||
rabbit.__proto__.sayHi();
|
||||
```
|
|
@ -0,0 +1,230 @@
|
|||
|
||||
# Methods for prototypes
|
||||
|
||||
There are also other ways to get/set a prototype, besides those that we already know:
|
||||
|
||||
- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` and optional property descriptors.
|
||||
- [Object.getPrototypeOf(obj)](mdn:js/Object.getPrototypeOf) -- returns the `[[Prototype]]` of `obj`.
|
||||
- [Object.setPrototypeOf(obj, proto)](mdn:js/Object.setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto`.
|
||||
|
||||
[cut]
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
*!*
|
||||
let rabbit = Object.create(animal);
|
||||
*/!*
|
||||
|
||||
alert(rabbit.eats); // true
|
||||
alert(Object.getPrototypeOf(rabbit) === animal); // true
|
||||
|
||||
Object.setPrototypeOf(rabbit, {}); // reset the prototype of rabbit to {}
|
||||
```
|
||||
|
||||
|
||||
````smart header="`Object.create` to make a clone"
|
||||
`Object.create` can be used to perform a full object cloning:
|
||||
|
||||
```js
|
||||
// fully identical shallow clone of obj
|
||||
let clone = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
|
||||
```
|
||||
|
||||
This call makes a truly exact copy of `obj`, including all properties: enumerable and non-enumerable, data properties and setters/getters -- everything, and with the right `[[Prototype]]`. But not an in-depth copy of course.
|
||||
````
|
||||
|
||||
|
||||
If we count all the ways to manage `[[Prototype]]`, there's a lot! Many ways to do the same!
|
||||
|
||||
Why so?
|
||||
|
||||
That's for historical reasons.
|
||||
|
||||
- The `"prototype"` property of a constructor function works since very ancient times.
|
||||
- Later in the year 2012: `Object.create` appeared in the standard. It allowed to create objects with the given prototype, but that was all. So browsers implemented a more powerful, but non-standard `__proto__` accessor that allowed to get/set a prototype at any time.
|
||||
- Later in the year 2015: `Object.setPrototypeOf` and `Object.getPrototypeOf` were added to the standard, and also `__proto__` became a part of the Annex B of the standard (optional for non-browser environments), because almost all browsers implemented it.
|
||||
|
||||
And now we have all these ways at our disposal.
|
||||
|
||||
But please note: for most practical tasks, prototype chains are fixed: `rabbit` inherits from `animal`, and that is not going to change. And JavaScript engines are highly optimized to that. Changing a prototype "on-the-fly" with `Object.setPrototypeOf` or `obj.__proto__=` is a very slow operation. But it is possible.
|
||||
|
||||
## "Very plain" objects
|
||||
|
||||
As we know, objects can be used as associative arrays to store key/value pairs.
|
||||
|
||||
...But if we try to use it for *any* keys (user-provided for instance), we can see an interesting glitch.
|
||||
|
||||
Check out the example:
|
||||
|
||||
```js run
|
||||
let obj = {};
|
||||
|
||||
let key = prompt("What's the key?", "__proto__");
|
||||
obj[key] = "some value";
|
||||
|
||||
alert(obj[key]); // [object Object], not "some value"!
|
||||
```
|
||||
|
||||
Here if the user types in `__proto__`, the assignment is ignored! That's because `__proto__` must be either an object or `null`, a string can not become a prototype.
|
||||
|
||||
We did not intend to implement such behavior, right? So that's a bug. Here the consequences are not terrible. But in more complex cases the prototype may indeed be changed, so the execution may go wrong in totally unexpected ways.
|
||||
|
||||
What's worst -- usually developers do not think about such possibility at all. That makes such bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on server-side.
|
||||
|
||||
Such thing happens only with `__proto__`. All other properties are "assignable" normally.
|
||||
|
||||
So, what's going and and how to evade the problem?
|
||||
|
||||
First, we can just switch to using `Map`, then everything's fine.
|
||||
|
||||
But `Object` also can serve us well here, because language creators gave a thought to that problem long ago.
|
||||
|
||||
The `__proto__` is not a property of an object, but an accessor property of `Object.prototype`:
|
||||
|
||||

|
||||
|
||||
So, if `obj.__proto__` is read or assigned, the corresponding getter/setter is called from its prototype, and it gets/sets `[[Prototype]]`.
|
||||
|
||||
As it was said in the beginning: `__proto__` is a way to access `[[Prototype]]`, it is not `[[Prototype]]` itself.
|
||||
|
||||
Now, if we want to use an object as an assotiative array, we can do it with a little trick:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
let obj = Object.create(null);
|
||||
*/!*
|
||||
|
||||
let key = prompt("What's the key?", "__proto__");
|
||||
obj[key] = "some value";
|
||||
|
||||
alert(obj[key]); // "some value"
|
||||
```
|
||||
|
||||
`Object.create(null)` creates an empty object without a prototype (`[[Prototype]]` is `null`):
|
||||
|
||||

|
||||
|
||||
So, there is no inherited getter/setter for `__proto__`. Now it is processed as a regular data property, so the example above works right.
|
||||
|
||||
We can call such object "very plain" or "pure dictionary objects", because they are even simpler than regular plain object `{...}`.
|
||||
|
||||
A downside is that such objects lack any built-in object methods, e.g. `toString`:
|
||||
|
||||
```js run
|
||||
*!*
|
||||
let obj = Object.create(null);
|
||||
*/!*
|
||||
|
||||
alert(obj); // Error (no toString)
|
||||
```
|
||||
|
||||
...But that's usually fine for associative arrays. If needed, we can add a `toString` of our own.
|
||||
|
||||
Please note that most object-related methods are `Object.something(...)`, like `Object.keys(obj)` -- they are not in the prototype, so they will keep working on such objects:
|
||||
|
||||
|
||||
```js run
|
||||
let chineseDictionary = Object.create(null);
|
||||
chineseDictionary.hello = "ni hao";
|
||||
chineseDictionary.bye = "zai jian";
|
||||
|
||||
alert(Object.keys(chineseDictionary)); // hello,bye
|
||||
```
|
||||
|
||||
## Getting all properties
|
||||
|
||||
There are many ways to get keys/values from an object.
|
||||
|
||||
We already know these ones:
|
||||
|
||||
- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs.
|
||||
|
||||
These methods only list *enumerable* properties, and those that have *strings as keys*.
|
||||
|
||||
If we want symbolic properties:
|
||||
|
||||
- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names.
|
||||
|
||||
If we want non-enumerable properties:
|
||||
|
||||
- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names.
|
||||
|
||||
If we want *everything*:
|
||||
|
||||
- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names.
|
||||
|
||||
All of them operate on the object itself. Properties from the prototype are not listed.
|
||||
|
||||
But `for..in` loop is different: it also gives inherited properties.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
jumps: true,
|
||||
__proto__: animal
|
||||
};
|
||||
|
||||
*!*
|
||||
// only own keys
|
||||
alert(Object.keys(rabbit)); // jumps
|
||||
*/!*
|
||||
|
||||
*!*
|
||||
// inherited keys too
|
||||
for(let prop in rabbit) alert(prop); // jumps, then eats
|
||||
*/!*
|
||||
```
|
||||
|
||||
If we want to distinguish inherited properties, there's a built-in method [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`.
|
||||
|
||||
So we can filter out inherited properties (or do something else with them):
|
||||
|
||||
```js run
|
||||
let animal = {
|
||||
eats: true
|
||||
};
|
||||
|
||||
let rabbit = {
|
||||
jumps: true,
|
||||
__proto__: animal
|
||||
};
|
||||
|
||||
for(let prop in rabbit) {
|
||||
let isOwn = rabbit.hasOwnProperty(prop);
|
||||
alert(`${prop}: ${isOwn}`); // jumps:true, then eats:false
|
||||
}
|
||||
```
|
||||
Here we have the following inheritance chain: `rabbit`, then `animal`, then `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it:
|
||||
|
||||

|
||||
|
||||
So when `rabbit.hasOwnProperty` is called, the method `hasOwnProperty` is taken from `Object.prototype`. Why `hasOwnProperty` itself does not appear in `for..in` loop? The answer is simple: it's not enumerable just like all other properties of `Object.prototype`.
|
||||
|
||||
As a take-away, let's remember that if we want inherited properties -- we should use `for..in`, and otherwise we can use other iteration methods or add the `hasOwnProperty` check.
|
||||
|
||||
## Summary
|
||||
|
||||
Here's a brief list of methods we discussed in this chapter -- as a recap:
|
||||
|
||||
- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` (can be `null`) and optional property descriptors.
|
||||
- [Object.getPrototypeOf(obj)](mdn:js/Object.getPrototypeOf) -- returns the `[[Prototype]]` of `obj` (same as `__proto__` getter).
|
||||
- [Object.setPrototypeOf(obj, proto)](mdn:js/Object.setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto` (same as `__proto__` setter).
|
||||
- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs.
|
||||
- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names.
|
||||
- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names.
|
||||
- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names.
|
||||
- [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`.
|
||||
|
||||
We also made it clear that `__proto__` is a getter/setter for `[[Prototype]]` and resides in `Object.prototype`, just as other methods.
|
||||
|
||||
`Object.create(null)` doesn't have any object properties and methods, and `__proto__` also doesn't exist for it.
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 4 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,46 @@
|
|||
Here's the line with the error:
|
||||
|
||||
```js
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
```
|
||||
|
||||
Here `Rabbit.prototype` and `Animal.prototype` become the same object. So methods of both classes become mixed in that object.
|
||||
|
||||
As a result, `Rabbit.prototype.walk` overwrites `Animal.prototype.walk`, so all animals start to bounce:
|
||||
|
||||
```js run
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.walk = function() {
|
||||
alert(this.name + ' walks');
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
*!*
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
*/!*
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert(this.name + " bounces!");
|
||||
};
|
||||
|
||||
*!*
|
||||
let animal = new Animal("pig");
|
||||
animal.walk(); // pig bounces!
|
||||
*/!*
|
||||
```
|
||||
|
||||
The correct variant would be:
|
||||
|
||||
```js
|
||||
Rabbit.prototype.__proto__ = Animal.prototype;
|
||||
// or like this:
|
||||
Rabbit.prototype = Object.create(Animal.prototype);
|
||||
```
|
||||
|
||||
That makes prototypes separate, each of them stores methods of the corresponding class, but `Rabbit.prototype` inherits from `Animal.prototype`.
|
|
@ -0,0 +1,29 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# An error in the inheritance
|
||||
|
||||
Find an error in the prototypal inheritance below.
|
||||
|
||||
What's wrong? What are consequences going to be?
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.walk = function() {
|
||||
alert(this.name + ' walks');
|
||||
};
|
||||
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Rabbit.prototype = Animal.prototype;
|
||||
|
||||
Rabbit.prototype.walk = function() {
|
||||
alert(this.name + " bounces!");
|
||||
};
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
Please note that properties that were internal in functional style (`template`, `timer`) and the internal method `render` are marked private with the underscore `_`.
|
|
@ -0,0 +1,32 @@
|
|||
function Clock({ template }) {
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
Clock.prototype._render = function() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) min = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this._template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this._timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), 1000);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
function Clock({ template }) {
|
||||
|
||||
let timer;
|
||||
|
||||
function render() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) min = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
this.stop = function() {
|
||||
clearInterval(timer);
|
||||
};
|
||||
|
||||
this.start = function() {
|
||||
render();
|
||||
timer = setInterval(render, 1000);
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Rewrite to prototypes
|
||||
|
||||
The `Clock` class is written in functional style. Rewrite it using prototypes.
|
||||
|
||||
P.S. The clock ticks in the console, open it to see.
|
209
1-js/07-object-oriented-programming/08-class-patterns/article.md
Normal file
|
@ -0,0 +1,209 @@
|
|||
|
||||
# Class patterns
|
||||
|
||||
```quote author="Wikipedia"
|
||||
In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods).
|
||||
```
|
||||
|
||||
There's a special syntax construct and a keyword `class` in JavaScript. But before studying it, we should consider that the term "class" comes the theory of object-oriented programming. The definition is cited above, and it's language-independant.
|
||||
|
||||
In JavaScript there are several well-known programming patterns to make classes even without using the `class` keyword. And here we'll talk about them first.
|
||||
|
||||
The `class` construct will be described in the next chapter, but in JavaScript it's a "syntax sugar" and an extension of one of the patterns that we'll study here.
|
||||
|
||||
[cut]
|
||||
|
||||
|
||||
## Functional class pattern
|
||||
|
||||
The constructor function below can be considered a class according to the definition:
|
||||
|
||||
```js run
|
||||
function User(name) {
|
||||
this.sayHi = function() {
|
||||
alert(name;
|
||||
};
|
||||
}
|
||||
|
||||
let user = new User("John");
|
||||
user.sayHi(); // John
|
||||
```
|
||||
|
||||
It follows all parts of the definition:
|
||||
|
||||
1. It is a "program-code-template" for creating objects (callable with `new`).
|
||||
2. It provides initial values for the state (`name` from parameters).
|
||||
3. It provides methods (`sayHi`).
|
||||
|
||||
This is called *functional class pattern*.
|
||||
|
||||
In the functional class pattern, local variables and nested functions inside `User`, that are not assigned to `this`, are visible from inside, but not accessible by the outer code.
|
||||
|
||||
So we can easily add internal functions and variables, like `calcAge()` here:
|
||||
|
||||
```js run
|
||||
function User(name, birthday) {
|
||||
|
||||
*!*
|
||||
// only visible from other methods inside User
|
||||
function calcAge() {
|
||||
new Date().getFullYear() - birthday.getFullYear();
|
||||
}
|
||||
*/!*
|
||||
|
||||
this.sayHi = function() {
|
||||
alert(name + ', age:' + calcAge());
|
||||
};
|
||||
}
|
||||
|
||||
let user = new User("John", new Date(2000,0,1));
|
||||
user.sayHi(); // John
|
||||
```
|
||||
|
||||
In this code variables `name`, `birthday` and the function `calcAge()` are internal, *private* to the object. They are only visible from inside of it. The external code that creates the `user` only can see a *public* method `sayHi`.
|
||||
|
||||
In works, because functional classes provide a shared lexical environment (of `User`) for private variables and methods.
|
||||
|
||||
## Prototype-based classes
|
||||
|
||||
Functional class pattern is rarely used, because prototypes are generally better.
|
||||
|
||||
Soon you'll see why.
|
||||
|
||||
Here's the same class rewritten using prototypes:
|
||||
|
||||
```js run
|
||||
function User(name, birthday) {
|
||||
*!*
|
||||
this._name = name;
|
||||
this._birthday = birthday;
|
||||
*/!*
|
||||
}
|
||||
|
||||
*!*
|
||||
User.prototype._calcAge = function() {
|
||||
*/!*
|
||||
return new Date().getFullYear() - this._birthday.getFullYear();
|
||||
};
|
||||
|
||||
User.prototype.sayHi = function() {
|
||||
alert(this._name + ', age:' + this._calcAge());
|
||||
};
|
||||
|
||||
let user = new User("John", new Date(2000,0,1));
|
||||
user.sayHi(); // John
|
||||
```
|
||||
|
||||
- The constructor `User` only initializes the current object state.
|
||||
- Methods reside in `User.prototype`.
|
||||
|
||||
Here methods are technically not inside `function User`, so they do not share a common lexical environment.
|
||||
|
||||
So, there is a widely known agreement that internal properties and methods are prepended with an underscore `"_"`. Like `_name` or `_calcAge()`. Technically, that's just an agreement, the outer code still can access them. But most developers recognize the meaning of `"_"` and try not to touch prefixed properties and methods in the external code.
|
||||
|
||||
We already can see benefits over the functional pattern:
|
||||
|
||||
- In the functional pattern, each object has its own copy of methods like `this.sayHi = function() {...}`.
|
||||
- In the prototypal pattern, there's a common `User.prototype` shared between all user objects.
|
||||
|
||||
So the prototypal pattern is more memory-efficient.
|
||||
|
||||
...But not only that. Prototypes allow us to setup the inheritance, precisely the same way as built-in JavaScript constructors do. Functional pattern allows to wrap a function into another function, and kind-of emulate inheritance this way, but that's far less effective, so here we won't go into details to save our time.
|
||||
|
||||
## Prototype-based inheritance for classes
|
||||
|
||||
Let's say we have two prototype-based classes.
|
||||
|
||||
`Rabbit`:
|
||||
|
||||
```js
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Rabbit.prototype.jump = function() {
|
||||
alert(this.name + ' jumps!');
|
||||
};
|
||||
|
||||
let rabbit = new Rabbit("My rabbit");
|
||||
```
|
||||
|
||||

|
||||
|
||||
...And `Animal`:
|
||||
|
||||
```js
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Animal.prototype.eat = function() {
|
||||
alert(this.name + ' eats.');
|
||||
};
|
||||
|
||||
let animal = new Animal("My animal");
|
||||
```
|
||||
|
||||

|
||||
|
||||
Right now they are fully independent.
|
||||
|
||||
But naturally `Rabbit` is a "subtype" of `Animal`. In other words, rabbits should be based on animals, have access to methods of `Animal` and extend them with its own methods.
|
||||
|
||||
What does it mean in the language on prototypes?
|
||||
|
||||
Right now `rabbit` objects have access to `Rabbit.prototype`. We should add `Animal.prototype` to it. So the chain would be `rabbit -> Rabbit.prototype -> Animal.prototype`.
|
||||
|
||||
Like this:
|
||||
|
||||

|
||||
|
||||
The code example:
|
||||
|
||||
```js run
|
||||
// Same Animal as before
|
||||
function Animal(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// All animals can eat, right?
|
||||
Animal.prototype.eat = function() {
|
||||
alert(this.name + ' eats.');
|
||||
};
|
||||
|
||||
// Same Rabbit as before
|
||||
function Rabbit(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Rabbit.prototype.jump = function() {
|
||||
alert(this.name + ' jumps!');
|
||||
};
|
||||
|
||||
*!*
|
||||
// setup the inheritance chain
|
||||
Rabbit.prototype.__proto__ = Animal.prototype; // (*)
|
||||
*/!*
|
||||
|
||||
let rabbit = new Rabbit("White Rabbit");
|
||||
*!*
|
||||
rabbit.eat(); // rabbits can eat too
|
||||
*/!*
|
||||
rabbit.jump();
|
||||
```
|
||||
|
||||
The line `(*)` sets up the prototype chain. So that `rabbit` first searches methods in `Rabbit.prototype`, then `Animal.prototype`. And then, just for completeness, the search may continue in `Object.prototype`, because `Animal.prototype` is a regular plain object, so it inherits from it. But that's not painted for brevity.
|
||||
|
||||
Here's what the code does:
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
|
||||
The term "class" comes from the object-oriented programming. In JavaScript it usually means the functional class pattern or the prototypal pattern. The prototypal pattern is more powerful and memory-efficient, so it's recommended to stick to it.
|
||||
|
||||
According to the prototypal pattern:
|
||||
1. Methods are stored in `Class.prototype`.
|
||||
2. Prototypes inherit from each other.
|
||||
|
||||
In the next chapter we'll study `class` keyword and construct. It allows to write prototypal classes shorter and provides some additional benefits.
|
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 38 KiB |
|
@ -0,0 +1,34 @@
|
|||
class Clock {
|
||||
constructor({ template }) {
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
_render() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) min = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this._template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), 1000);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Console clock</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script src="clock.js"></script>
|
||||
<script>
|
||||
let clock = new Clock({template: 'h:m:s'});
|
||||
clock.start();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
|
||||
function Clock({ template }) {
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
Clock.prototype._render = function() {
|
||||
let date = new Date();
|
||||
|
||||
let hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
|
||||
let mins = date.getMinutes();
|
||||
if (mins < 10) min = '0' + mins;
|
||||
|
||||
let secs = date.getSeconds();
|
||||
if (secs < 10) secs = '0' + secs;
|
||||
|
||||
let output = this._template
|
||||
.replace('h', hours)
|
||||
.replace('m', mins)
|
||||
.replace('s', secs);
|
||||
|
||||
console.log(output);
|
||||
};
|
||||
|
||||
Clock.prototype.stop = function() {
|
||||
clearInterval(this._timer);
|
||||
};
|
||||
|
||||
Clock.prototype.start = function() {
|
||||
this._render();
|
||||
this._timer = setInterval(() => this._render(), 1000);
|
||||
};
|