This commit is contained in:
Ilya Kantor 2017-03-30 16:59:39 +03:00
parent 79f3775034
commit d2378df45e
14 changed files with 129 additions and 54 deletions

View file

@ -89,7 +89,7 @@ alert(rabbit.constructor == Rabbit); // true (from prototype)
![](rabbit-prototype-constructor.png)
We can use `constructor` to create a new object using the same constructor as the existing one.
We can use `constructor` property to create a new object using the same constructor as the existing one.
Like here:
@ -101,20 +101,36 @@ function Rabbit(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.
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 another one of the same kind.
...But probably the most important thing about `"constructor"` is that...
But probably the most important thing about `"constructor"` is that...
**JavaScript itself does not ensure the right `"constructor"` at all.**
**...JavaScript itself does not ensure the right `"constructor"` value.**
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.
Yes, it exists in the default `"prototype"` for functions, but that's all. 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.
In particular, if we replace the default prototype as a whole, 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:
For instance:
```js run
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
*!*
alert(rabbit.constructor === Rabbit); // false
*/!*
```
So, to keep the right `"constructor"` we can choose to add/remove properties to the default `"prototype"` instead of overwriting it as a whole:
```js
function Rabbit() {}
@ -125,7 +141,7 @@ Rabbit.prototype.jumps = true
// the default Rabbit.prototype.constructor is preserved
```
Or, alternatively, recreate it manually:
Or, alternatively, recreate the `constructor` property it manually:
```js
Rabbit.prototype = {
@ -134,6 +150,8 @@ Rabbit.prototype = {
constructor: Rabbit
*/!*
};
// now constructor is also correct, because we added it
```
@ -143,12 +161,11 @@ In this chapter we briefly described the way of setting a `[[Prototype]]` for ob
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 `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`.
- The `"prototype"` property only has such a special effect when is set to a constructor function, and invoked with `new`.
On regular objects this property does nothing. That's an ordinary property:
On regular objects the `prototype` is nothing special:
```js
let user = {
name: "John",
@ -156,4 +173,4 @@ let user = {
};
```
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.
By default all functions have `F.prototype = { constructor: F }`, so we can get the constructor of an object by accessing its `"constructor"` property.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

@ -7,7 +7,7 @@ To make `toString` non-enumerable, let's define it using a property descriptor.
*!*
let dictionary = Object.create(null, {
toString: { // define toString property
value() { // with function value
value() { // the value is a function
return Object.keys(this).join();
}
}
@ -26,3 +26,4 @@ for(let key in dictionary) {
alert(dictionary); // "apple,__proto__"
```
When we create a property using a descriptor, its flags are `false` by default. So in the code above, `dictionary.toString` is non-enumerable.

View file

@ -4,7 +4,7 @@ importance: 5
# Add toString to the dictionary
There's an object `dictionary`, suited to store any `key/value` pairs.
There's an object `dictionary`, created as `Object.create(null)`, 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.
@ -14,7 +14,7 @@ Here's how it should work:
let dictionary = Object.create(null);
*!*
// your code to add toString
// your code to add dictionary.toString method
*/!*
// add some data

View file

@ -12,7 +12,7 @@ function Rabbit(name) {
}
Rabbit.prototype.sayHi = function() {
alert(this.name);
}
};
let rabbit = new Rabbit("Rabbit");
```

View file

@ -1,6 +1,8 @@
# Methods for prototypes
In this chapter we cover additional methods to work with the prototype.
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.
@ -16,19 +18,40 @@ let animal = {
eats: true
};
// create a new object with animal as a prototype
*!*
let rabbit = Object.create(animal);
*/!*
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
*!*
alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit
*/!*
Object.setPrototypeOf(rabbit, {}); // reset the prototype of rabbit to {}
*!*
Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}
*/!*
```
`Object.create` has an optional second argument: property descriptors. We can provide additional properties to the new object there, like this:
````smart header="`Object.create` to make a clone"
`Object.create` can be used to perform a full object cloning:
```js run
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
alert(rabbit.jumps); // true
```
The descriptors are in the same format as described in the chapter <info:property-descriptors>.
We can use `Object.create` to perform a full object cloning, like this:
```js
// fully identical shallow clone of obj
@ -36,8 +59,8 @@ 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.
````
## Brief history
If we count all the ways to manage `[[Prototype]]`, there's a lot! Many ways to do the same!
@ -46,18 +69,18 @@ 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.
- Later in the year 2012: `Object.create` appeared in the standard. It allowed to create objects with the given prototype, but did not allow to get/set it. So browsers implemented 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. The `__proto__` was de-facto implemented everywhere, so it made its way to the Annex B of the standard, that is optional for non-browser environments.
And now we have all these ways at our disposal.
As of 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.
Technically, we can get/set `[[Prototype]]` at any time. But usually we only set it once at the object creation time, and then do not modify: `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.
...But if we try to store *user-provided* keys in it (for instance, a user-entered dictionary), we can see an interesting glitch: all keys work fine except `"__proto__"`.
Check out the example:
@ -70,15 +93,17 @@ 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.
Here if the user types in `__proto__`, the assignment is ignored!
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.
That shouldn't surprise us. The `__proto__` property is special: it must be either an object or `null`, a string can not become a prototype.
But we did not intend to implement such behavior, right? We want to store key/value pairs, and the key named `"__proto__"` was not properly saved. So that's a bug. Here the consequences are not terrible. But in other 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?
How to evade the problem?
First, we can just switch to using `Map`, then everything's fine.
@ -92,7 +117,7 @@ So, if `obj.__proto__` is read or assigned, the corresponding getter/setter is c
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:
Now, if we want to use an object as an associative array, we can do it with a little trick:
```js run
*!*
@ -123,7 +148,7 @@ 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.
...But that's usually fine for associative arrays.
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:
@ -142,9 +167,7 @@ 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*.
- [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:
@ -154,13 +177,13 @@ 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*:
If we want *all* properties:
- [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.
These methods are a bit different about which properties they return, but 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.
The `for..in` loop is different: it loops over inherited properties too.
For instance:
@ -208,9 +231,9 @@ Here we have the following inheritance chain: `rabbit`, then `animal`, then `Obj
![](rabbit-animal-object.png)
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`.
Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited.
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.
...But why `hasOwnProperty` does not appear in `for..in` loop, if it lists all inherited properties? The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`. That's why they are not listed.
## Summary
@ -227,4 +250,6 @@ Here's a brief list of methods we discussed in this chapter -- as a recap:
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.
We can create an object without a prototype by `Object.create(null)`. Such objects are used as "pure dictionaries", they have no issues with `"__proto__"` as the key.
All methods that return object properties (like `Object.keys` and others) -- return "own" properties. If we want inherited ones, then we can use `for..in`.

View file

@ -66,9 +66,37 @@ From the other hand, `sayHi` is the external, *public* method. The external code
This way we can hide internal implementation details and helper methods from the outer code. Only what's assigned to `this` becomes visible outside.
## Factory class pattern
We can create a class without using `new` at all.
Like this:
```js run
function User(name, birthday) {
// only visible from other methods inside User
function calcAge() {
new Date().getFullYear() - birthday.getFullYear();
}
return {
sayHi() {
alert(name + ', age:' + calcAge());
}
};
}
*!*
let user = User("John", new Date(2000,0,1));
*/!*
user.sayHi(); // John
```
As we can see, the function `User` returns an object with public properties and methods. The only benefit of this method is that we can omit `new`: write `let user = User(...)` instead of `let user = new User(...)`. In other aspects it's almost the same as the functional pattern.
## Prototype-based classes
Functional class pattern is rarely used, because prototypes are generally better.
Prototype-based classes is the most important and generally the best. Functional and factory class patterns are rarely used in practice.
Soon you'll see why.
@ -96,21 +124,23 @@ 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`.
The code structure:
Here methods are technically not inside `function User`, so they do not share a common lexical environment.
- The constructor `User` only initializes the current object state.
- Methods are added to `User.prototype`.
As we can see, methods are lexically not inside `function User`, they do not share a common lexical environment. If we declare variables inside `function User`, then they won't be visible to methods.
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:
Here are the advantages 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.
- In the functional pattern, each object has its own copy of every method. We assign a separate copy of `this.sayHi = function() {...}` and other methods in the constructor.
- In the prototypal pattern, all methods are in `User.prototype` that is shared between all user objects. An object itself only stores the data.
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.
...But not only that. Prototypes allow us to setup the inheritance in a really efficient way. Built-in JavaScript objects all use prototypes. Also there's a special syntax construct: "class" that provides nice-looking syntax for them. And there's more, so let's go on with them.
## Prototype-based inheritance for classes
@ -150,17 +180,19 @@ 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.
But we'd want `Rabbit` to extend `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`.
Right now methods for `rabbit` objects are in `Rabbit.prototype`. We'd like `rabbit` to use `Animal.prototype` as a "fallback", if the method is not found in `Rabbit.prototype`.
So the prototype chain should be `rabbit` -> `Rabbit.prototype` -> `Animal.prototype`.
Like this:
![](class-inheritance-rabbit-animal.png)
The code example:
The code to implement that:
```js run
// Same Animal as before
@ -194,9 +226,9 @@ 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.
The line `(*)` sets up the prototype chain. So that `rabbit` first searches methods in `Rabbit.prototype`, then `Animal.prototype`. And then, just for completeness, let's mention that if the method is not found in `Animal.prototype`, then the search continues in `Object.prototype`, because `Animal.prototype` is a regular plain object, so it inherits from it.
Here's what the code does:
So here's the full picture:
![](class-inheritance-rabbit-animal-2.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Before After
Before After