diff --git a/1-js/07-object-oriented-programming/01-property-flags-descriptors/article.md b/1-js/07-object-oriented-programming/01-property-descriptors/article.md similarity index 100% rename from 1-js/07-object-oriented-programming/01-property-flags-descriptors/article.md rename to 1-js/07-object-oriented-programming/01-property-descriptors/article.md diff --git a/1-js/07-object-oriented-programming/04-function-prototype/article.md b/1-js/07-object-oriented-programming/04-function-prototype/article.md index 12680b89..d66a22a7 100644 --- a/1-js/07-object-oriented-programming/04-function-prototype/article.md +++ b/1-js/07-object-oriented-programming/04-function-prototype/article.md @@ -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. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png index 9c0b2235..d3ef3448 100644 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png and b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png index 76879338..45cbb6ba 100644 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png and b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png index 9c0b2235..d3ef3448 100644 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png and b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png index 76879338..45cbb6ba 100644 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png and b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md index 02e07b8f..debaecd6 100644 --- a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md +++ b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md @@ -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. diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md index 6eab6f9e..0d831f2c 100644 --- a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md +++ b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md @@ -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 diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md index 48f67db1..92653bd8 100644 --- a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md +++ b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md @@ -12,7 +12,7 @@ function Rabbit(name) { } Rabbit.prototype.sayHi = function() { alert(this.name); -} +}; let rabbit = new Rabbit("Rabbit"); ``` diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/article.md b/1-js/07-object-oriented-programming/06-prototype-methods/article.md index f40d3d62..d841de39 100644 --- a/1-js/07-object-oriented-programming/06-prototype-methods/article.md +++ b/1-js/07-object-oriented-programming/06-prototype-methods/article.md @@ -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 . + +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`. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/article.md b/1-js/07-object-oriented-programming/08-class-patterns/article.md index 472ca331..c862d907 100644 --- a/1-js/07-object-oriented-programming/08-class-patterns/article.md +++ b/1-js/07-object-oriented-programming/08-class-patterns/article.md @@ -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) diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png index 83dfd7fc..ad4a4093 100644 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png and b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png index 29d7e493..199ed3ee 100644 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png and b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png differ diff --git a/figures.sketch b/figures.sketch index 83039c19..ed9b0654 100644 Binary files a/figures.sketch and b/figures.sketch differ