diff --git a/1-js/4-object-basics/04-object-methods/article.md b/1-js/4-object-basics/04-object-methods/article.md index eb7bc097a3..34187badbd 100644 --- a/1-js/4-object-basics/04-object-methods/article.md +++ b/1-js/4-object-basics/04-object-methods/article.md @@ -219,10 +219,11 @@ Please note that usually a call of a function using `this` without an object is ```smart header="The consequences of unbound `this`" If you come from another programming languages, then you are probably used to an idea of a "bound `this`", where methods defined in an object always have `this` referencing that object. -The idea of unbound, run-time evaluated `this` has both pluses and minuses. From one side, a function can be reused for different objects. From the other side, greater flexibility opens a place for mistakes. +The idea of unbound, run-time evaluated `this` has both pluses and minuses. From one side, a function can be reused for different objects. From the other side, greater flexibility opens a place for mistakes. Here we are not to judge whether this language design decision is good or bad. We will understand how to work with it, how to get benefits and evade problems. ``` + ## Internals: Reference Type An intricate method call can loose `this`, for instance: @@ -295,6 +296,27 @@ Any other operation like assignment `hi = user.hi` discards the reference type a So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj[method]()` syntax (they do the same here). +````warn header="Arrow functions do not have `this`" +Arrow functions are special: they don't have "own" `this`. If we reference `this` from such function, it's taken from the outer "normal" function. + +For instance, here `arrow()` uses `this` from the outer `user.sayHi()` method: + +```js run +let user = { + firstName: "Ilya", + sayHi() { + let arrow = () => alert(this.firstName); + arrow(); + } +}; + +user.sayHi(); // Ilya +``` + +That's a special feature of arrow functions, it's useful when we actually do not want to have a separate `this`, but rather to take it from the outer context. Later in the chapter we'll dig more deeply into what's going on. + +```` + ## Summary [todo] diff --git a/1-js/5-data-types/01-primitives-methods/article.md b/1-js/5-data-types/01-primitives-methods/article.md index 844732ad5f..11c229a931 100644 --- a/1-js/5-data-types/01-primitives-methods/article.md +++ b/1-js/5-data-types/01-primitives-methods/article.md @@ -81,6 +81,39 @@ alert( n.toFixed(2) ); // 1.23 We'll see more specific methods in chapters and . + +````warn header="Constructors `String/Number/Boolean` are for internal use only" +Some languages like Java allow to create "wrapper objects" for primitives explicitly using syntax like `new Number(1)` or `new Boolean(false)`. + +In Javascript that's also possible for historical reasons, but highly not recommended. Things will go crazy in many places. + +For instance: + +```js run +alert( typeof 1 ); // "number" + +alert( typeof new Number(1) ); // "object"! +``` + +And, because `zero` is an object: + +```js run +let zero = new Number(0); + +if (zero) { // zero is true, because it's an object + alert( "zero is truthy?!?" ); +} +``` + +From the other side, using same functions `String/Number/Boolean` without `new` is a totally sane and useful thing. They convert a value to the corresponding type: to a string, a number, or a boolean (primitive). + +This is totally valid: +```js +let num = Number("123"); // convert a string to number +``` +```` + + ````warn header="null/undefined have no methods" Special primitives `null` and `undefined` are exceptions. They have no corresponding "wrapper objects" and provide no methods. In a sense, they are "the most primitive". diff --git a/1-js/5-data-types/05-array-methods/article.md b/1-js/5-data-types/05-array-methods/article.md index d47c9f3d44..ab28f7c106 100644 --- a/1-js/5-data-types/05-array-methods/article.md +++ b/1-js/5-data-types/05-array-methods/article.md @@ -606,7 +606,7 @@ alert(typeof {}); // object alert(typeof []); // same ``` -There's a special method for that [Array.isArray(value)](mdn:js/Array/isArray) that returns `true` if the `value` is an array, and `false` otherwise. +...But arrays are used so often that there's a special method for that: [Array.isArray(value)](mdn:js/Array/isArray). It returns `true` if the `value` is an array, and `false` otherwise. ```js run alert(Array.isArray({})); // false @@ -614,24 +614,23 @@ alert(Array.isArray({})); // false alert(Array.isArray([])); // true ``` -```smart header="`Array.isArray` vs other type-checks" -You remembeare other ways to check for - ## Methods: "thisArg" Almost all array methods that call functions -- like `find`, `filter`, `map`, with a notable exception of `sort`, accept an optional additional parameter `thisArg`. -The full syntax is: +In the sections above that parameter is not explained, because it's rarely used. + +But for completeness here's the full syntax: ```js -let result = arr.find(func, thisArg); -let results = arr.filter(func, thisArg); -// etc, thisArg goes after the function +arr.find(func, thisArg); +arr.filter(func, thisArg); +arr.map(func, thisArg); +// ... +// thisArg is the optional last argument ``` -It is used sparingly, but we have to cover it here for the sake of completeness. - -The value of `thisArg` parameter becomes `this` for the function. +The value of `thisArg` parameter becomes `this` for `func`. For instance, here we use an object method as a filter: @@ -657,7 +656,7 @@ let youngerUsers = users.filter(user.younger, user); alert(youngerUsers.length); // 2 ``` -In the call above, we use `user.younger` as a filter and also provide `user` as the context for it. If we did't provide the context, `users.filter(user.younger)` would call `user.younger` as a standalone function, with `this=undefined`. That would be an instant error. +In the call above, we use `user.younger` as a filter and also provide `user` as the context for it. If we did't provide the context, `users.filter(user.younger)` would call `user.younger` as a standalone function, with `this=undefined`. That would mean an instant error. ## Other methods @@ -676,7 +675,7 @@ These and other methods are also listed in the [manual](mdn:js/Array). ## Summary -Most often methods: +Most used array methods: - `split/join` -- convert a string to array and back. - `splice` -- delete and insert elements at the given position. diff --git a/1-js/9-object-inheritance/12-oop-errors/1-format-error/solution.md b/1-js/7-advanced-syntax/12-oop-errors/1-format-error/solution.md similarity index 100% rename from 1-js/9-object-inheritance/12-oop-errors/1-format-error/solution.md rename to 1-js/7-advanced-syntax/12-oop-errors/1-format-error/solution.md diff --git a/1-js/9-object-inheritance/12-oop-errors/1-format-error/task.md b/1-js/7-advanced-syntax/12-oop-errors/1-format-error/task.md similarity index 100% rename from 1-js/9-object-inheritance/12-oop-errors/1-format-error/task.md rename to 1-js/7-advanced-syntax/12-oop-errors/1-format-error/task.md diff --git a/1-js/9-object-inheritance/12-oop-errors/article.md b/1-js/7-advanced-syntax/12-oop-errors/article.md similarity index 100% rename from 1-js/9-object-inheritance/12-oop-errors/article.md rename to 1-js/7-advanced-syntax/12-oop-errors/article.md diff --git a/1-js/8-more-functions/02-rest-parameters-spread-operator/article.md b/1-js/8-more-functions/02-rest-parameters-spread-operator/article.md index 68e57f4a17..c13d755db6 100644 --- a/1-js/8-more-functions/02-rest-parameters-spread-operator/article.md +++ b/1-js/8-more-functions/02-rest-parameters-spread-operator/article.md @@ -107,6 +107,23 @@ Also, it always has all arguments in it, we can't capture them partially, like w So when we need these features, then rest parameters are preferred. +````smart header="Arrow functions do not have `\"arguments\"`" +If we access the `arguments` object from an arrow function, it takes them from the outer "normal" function. + +Here's an example: + +```js run +function f() { + let showArg = () => alert(arguments[0]); + showArg(); +} + +f(1); // 1 +``` +As we remember, arrow functions don't have own `this`. Now we know they don't have the special `arguments` object too. + +```` + ## Spread operator [#spread-operator] We've just seen how to get an array from the list of parameters. diff --git a/1-js/8-more-functions/03-closure/2-counter-object-independent/solution.md b/1-js/8-more-functions/03-closure/2-counter-object-independent/solution.md index c39acd71a3..cd4e641e42 100644 --- a/1-js/8-more-functions/03-closure/2-counter-object-independent/solution.md +++ b/1-js/8-more-functions/03-closure/2-counter-object-independent/solution.md @@ -1,7 +1,7 @@ -Both nested functions are created within the same Lexical Environment. +Surely it will work just fine. -So they share the same `count`: +Both nested functions are created within the same outer Lexical Environment, so they share access to the same `count` variable: ```js run function Counter() { @@ -10,6 +10,7 @@ function Counter() { this.up = function() { return ++count; }; + this.down = function() { return --count; }; diff --git a/1-js/8-more-functions/08-call-apply-decorators/2-spy-decorator/solution.md b/1-js/8-more-functions/08-call-apply-decorators/2-spy-decorator/solution.md index 412ecb20f8..4a79d3e118 100644 --- a/1-js/8-more-functions/08-call-apply-decorators/2-spy-decorator/solution.md +++ b/1-js/8-more-functions/08-call-apply-decorators/2-spy-decorator/solution.md @@ -1,33 +1 @@ -Решение аналогично задаче , разница в том, что в лог вместо одного аргумента идет весь объект `arguments`. - -Для передачи вызова с произвольным количеством аргументов используем `f.apply(this, arguments)`. - -```js run -function work(a, b) { - alert( a + b ); // work - произвольная функция -} - -function makeLogging(f, log) { - -*!* - function wrapper() { - log.push([].slice.call(arguments)); - return f.apply(this, arguments); - } -*/!* - - return wrapper; -} - -var log = []; -work = makeLogging(work, log); - -work(1, 2); // 3 -work(4, 5); // 9 - -for (var i = 0; i < log.length; i++) { - var args = log[i]; // массив из аргументов i-го вызова - alert( 'Лог:' + args.join() ); // "Лог:1,2", "Лог:4,5" -} -``` - +Here we can use `log.push(args)` to store all arguments in the log and `f.apply(this, args)` to forward the call. diff --git a/1-js/8-more-functions/08-call-apply-decorators/article.md b/1-js/8-more-functions/08-call-apply-decorators/article.md index e5773f162f..db99676d4c 100644 --- a/1-js/8-more-functions/08-call-apply-decorators/article.md +++ b/1-js/8-more-functions/08-call-apply-decorators/article.md @@ -434,7 +434,6 @@ Taken from the specification almost "as-is": So, technically it takes `this` and joins `this[0]`, `this[1]` ...etc together. It's intentionally written in a way that allows any array-like `this` (not a coincidence, many methods follow this practice). That's why it also works with `this=arguments`. - ## Summary *Decorator* is a wrapper around a function that alters its behavior. The main job is still carried out by the function. diff --git a/1-js/8-more-functions/09-bind/article.md b/1-js/8-more-functions/09-bind/article.md index 1f8e33916f..7f27ba9a4b 100644 --- a/1-js/8-more-functions/09-bind/article.md +++ b/1-js/8-more-functions/09-bind/article.md @@ -39,7 +39,7 @@ let f = user.sayHi; setTimeout(f, 1000); // lost user context ``` -The method `setTimeout` is a little special: it sets `this=window` for the function call. So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases as we'll see, usually `this` just becomes `undefined`. +The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.JS, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases as we'll see, usually `this` just becomes `undefined`. The task is quite typical -- we want to pass an object method somewhere else (here -- to the scheduler) where it will be called. How to make sure that it will be called in the right context? @@ -494,4 +494,4 @@ But most implementations of currying in Javascript are advanced, as described: t - *Currying* is a transform that makes `f(a,b,c)` callable as `f(a)(b)(c)`. Javascript implementations usually both keep the function callable normally and return the partial if arguments count is not enough. - Currying is convenient when we want to have easy partials. We saw an example with logging: the universal function `log(date, importance, message)` after currying gives us partials when called with one argument like `log(date)` or two arguments `log(date, importance)`. + Currying is great when we want easy partials. As we've seen in the logging example: the universal function `log(date, importance, message)` after currying gives us partials when called with one argument like `log(date)` or two arguments `log(date, importance)`. diff --git a/1-js/8-more-functions/10-arrow-functions/article.md b/1-js/8-more-functions/10-arrow-functions/article.md new file mode 100644 index 0000000000..e2e71b38e7 --- /dev/null +++ b/1-js/8-more-functions/10-arrow-functions/article.md @@ -0,0 +1,130 @@ +# Arrow functions revisited + +Let's revisit arrow functions. + +[cut] + +Arrow functions are not just a "shorthand" for writing small stuff. + +Javascript is full of situations where we need to write a small function, that's executed somewhere else. + +For instance: + +- `arr.forEach(func)` -- `func` is executed by `forEach` for every array item. +- `setTimeout(func)` -- `func` is executed by the built-in scheduler. +- ...there are more. + +It's very in the spirit of Javascript to create a function and pass it somewhere. + +And in such functions we usually don't want to leave the current context. + +## Arrow functions have no "this" + +As we remember from the chapter , arrow functions do not have `this`. If `this` is accessed, it is taken from the outside. + +For instance, we can use it to iterate inside an object method: + +```js run +let group = { + title: "Our Group", + students: ["John", "Pete", "Alice"], + + showList() { +*!* + this.students.forEach( + student => alert(this.title + ': ' + student) + ); +*/!* + } +}; + +group.showList(); +``` + +Here in `forEach`, the arrow function is used, so `this.title` in it is exactly the same as in the outer method `showList`. That is: `group.title`. + +If we used a "regular" function, there would be an error: + +```js run +let group = { + title: "Our Group", + students: ["John", "Pete", "Alice"], + + showList() { +*!* + this.students.forEach(function(student) { + // Error: Cannot read property 'title' of undefined + alert(this.title + ': ' + student) + }); +*/!* + } +}; + +group.showList(); +``` + +The error occurs because `forEach` runs functions with `this=undefined` by default, so the attempt to access `undefined.title` is made. + +That doesn't affect arrow functions, because they just don't have `this`. + +```warn header="Arrow functions can't run with `new`" +Not having `this` naturally means another limitation: arrow functions can't be used as constructors. They can't be called with `new`. +``` + +```smart header="Arrow functions VS bind" +There's a subtle difference between an arrow function `=>` and a regular function called with `.bind(this)`: + +- `.bind(this)` creates a "bound version" of the function. +- The arrow `=>` doesn't create any binding. The function simply doesn't have `this`. The lookup of `this` is made exactly the same way as a regular variable search: in the outer lexical environment. +``` + +## Arrows have no "arguments" + +Arrow functions also have no `arguments` variable. + +That's great for decorators, when we need to forward a call with the current `this` and `arguments`. + +For instance, `defer(f, ms)` gets a function and returns a wrapper around it that delays the call by `ms` milliseconds: + +```js run +function defer(f, ms) { + return function() { + setTimeout(() => f.apply(this, arguments), ms) + }; +} + +function sayHi(who) { + alert('Hello, ' + who); +} + +let sayHiDeferred = defer(sayHi, 2000); +sayHiDeferred("John"); // Hello, John after 2 seconds +``` + +The same without an arrow function would look like: + +Аналогичная реализация без функции-стрелки выглядела бы так: + +```js +function defer(f, ms) { + return function(...args) { + let ctx = this; + setTimeout(function() { + return f.apply(ctx, args); + }, ms); + }; +} +``` + +Here we had to create additional variables `args` and `ctx` so that the function inside `setTimeout` could take them. + +## Summary + +Arrow functions: + +- Do not have `this`. +- Do not have `arguments`. +- Can't be called with `new`. +- (They also don't have `super`, but we didn't study it. Will be in the chapter ). + +That's because they are meant for short pieces of code that does not have the own "context", but rather works in the current one. And they really shine in that use case. diff --git a/1-js/9-object-inheritance/04-function-prototype/article.md b/1-js/9-object-inheritance/04-function-prototype/article.md index 3cdf4f1947..6c055bde86 100644 --- a/1-js/9-object-inheritance/04-function-prototype/article.md +++ b/1-js/9-object-inheritance/04-function-prototype/article.md @@ -108,15 +108,13 @@ That's handy when we have an object, don't know which constructor was used for i ...But probably the most important thing about `"constructor"` is that... -**JavaScript itself does not use the `"constructor"` property at all.** +**JavaScript itself does not ensure the right `"constructor"` at all.** -Yes, it exists in the default `"prototype"` for functions, but that's literally all about it. No language function relies on it and nothing controls its validity. - -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. 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. -Such assignment won't break native methods or syntax, because nothing in the language uses the `"constructor"` property. But we may want to keep `"constructor"` for convenience by adding properties to the default `"prototype"` instead of overwriting it as a whole: +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() {} diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype-1.png b/1-js/9-object-inheritance/04-function-prototype/object-prototype-1.png index c004d0331f..e7c6bdb212 100644 Binary files a/1-js/9-object-inheritance/04-function-prototype/object-prototype-1.png and b/1-js/9-object-inheritance/04-function-prototype/object-prototype-1.png differ diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype-1@2x.png b/1-js/9-object-inheritance/04-function-prototype/object-prototype-1@2x.png index 16abecef1f..34c256bbfd 100644 Binary files a/1-js/9-object-inheritance/04-function-prototype/object-prototype-1@2x.png and b/1-js/9-object-inheritance/04-function-prototype/object-prototype-1@2x.png differ diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype.png b/1-js/9-object-inheritance/04-function-prototype/object-prototype.png index 390e7cd4ed..d97d87d2c3 100644 Binary files a/1-js/9-object-inheritance/04-function-prototype/object-prototype.png and b/1-js/9-object-inheritance/04-function-prototype/object-prototype.png differ diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype@2x.png b/1-js/9-object-inheritance/04-function-prototype/object-prototype@2x.png index 2a10ef1edd..76ce4c9ac5 100644 Binary files a/1-js/9-object-inheritance/04-function-prototype/object-prototype@2x.png and b/1-js/9-object-inheritance/04-function-prototype/object-prototype@2x.png differ diff --git a/1-js/9-object-inheritance/05-native-prototypes/2-defer-to-prototype-extended/solution.md b/1-js/9-object-inheritance/05-native-prototypes/2-defer-to-prototype-extended/solution.md index 004906a427..5dae0e6540 100644 --- a/1-js/9-object-inheritance/05-native-prototypes/2-defer-to-prototype-extended/solution.md +++ b/1-js/9-object-inheritance/05-native-prototypes/2-defer-to-prototype-extended/solution.md @@ -3,9 +3,8 @@ ```js run Function.prototype.defer = function(ms) { var f = this; - return function() { - var args = arguments, - context = this; + return function(...args) { + let context = this; setTimeout(function() { f.apply(context, args); }, ms); @@ -19,4 +18,3 @@ function f(a, b) { f.defer(1000)(1, 2); // выведет 3 через 1 секунду. ``` - diff --git a/1-js/9-object-inheritance/05-native-prototypes/article.md b/1-js/9-object-inheritance/05-native-prototypes/article.md index 8cc56eb8e8..c6a0505590 100644 --- a/1-js/9-object-inheritance/05-native-prototypes/article.md +++ b/1-js/9-object-inheritance/05-native-prototypes/article.md @@ -54,7 +54,7 @@ Here's the overall picture (for 3 built-ins to fit): ![](native-prototypes-classes.png) -Let's check the prototypes: is the picture correct? +Let's check the prototypes manually: ```js run let arr = [1, 2, 3]; @@ -82,43 +82,29 @@ As we've seen before, `Object.prototype` has `toString` as well, but `Array.prot ![](native-prototypes-array-tostring.png) -Functions also work the same way. 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. +In-browser tools like Chrome developer console also show inheritance (may need to use `console.dir` for built-in objects): -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="Constructors `String/Number/Boolean` are for internal use only" -Technically, we can create "wrapper objects" for primitives manually using `new Number(1)`. But things will go crazy in many places. +![](console_dir_array.png) -For instance: +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 -alert( typeof 1 ); // "number" +function f() {} -alert( typeof new Number(1) ); // "object"! +alert(f.__proto__ == Function.prototype); // true +alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects ``` -And, because it's an object: - -```js run -let zero = new Number(0); +## Primitives -if (zero) { // zero is true, because it's an object - alert( "zero is true in the boolean context?!?" ); -} -``` +The most intricate thing happens with strings, numbers and booleans. -The same functions `String/Number/Boolean` without `new` have a totally different behavior: they convert a value to the corresponding type: to a string, a number, or a boolean. +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. -This is totally valid: -```js -let num = Number("123"); // convert a string to number -``` -```` +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. +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] diff --git a/1-js/9-object-inheritance/10-class-inheritance/console_dir_array.png b/1-js/9-object-inheritance/05-native-prototypes/console_dir_array.png similarity index 100% rename from 1-js/9-object-inheritance/10-class-inheritance/console_dir_array.png rename to 1-js/9-object-inheritance/05-native-prototypes/console_dir_array.png diff --git a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1.png b/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1.png index c004d0331f..e7c6bdb212 100644 Binary files a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1.png and b/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1.png differ diff --git a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1@2x.png b/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1@2x.png index 16abecef1f..34c256bbfd 100644 Binary files a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1@2x.png and b/1-js/9-object-inheritance/05-native-prototypes/object-prototype-1@2x.png differ diff --git a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-2.png b/1-js/9-object-inheritance/05-native-prototypes/object-prototype-2.png deleted file mode 100644 index ee42f6b98c..0000000000 Binary files a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-2.png and /dev/null differ diff --git a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-2@2x.png b/1-js/9-object-inheritance/05-native-prototypes/object-prototype-2@2x.png deleted file mode 100644 index 1917c6631d..0000000000 Binary files a/1-js/9-object-inheritance/05-native-prototypes/object-prototype-2@2x.png and /dev/null differ diff --git a/1-js/9-object-inheritance/05-native-prototypes/object-prototype.png b/1-js/9-object-inheritance/05-native-prototypes/object-prototype.png index 390e7cd4ed..d97d87d2c3 100644 Binary files a/1-js/9-object-inheritance/05-native-prototypes/object-prototype.png and b/1-js/9-object-inheritance/05-native-prototypes/object-prototype.png differ diff --git a/1-js/9-object-inheritance/05-native-prototypes/object-prototype@2x.png b/1-js/9-object-inheritance/05-native-prototypes/object-prototype@2x.png index 2a10ef1edd..76ce4c9ac5 100644 Binary files a/1-js/9-object-inheritance/05-native-prototypes/object-prototype@2x.png and b/1-js/9-object-inheritance/05-native-prototypes/object-prototype@2x.png differ diff --git a/1-js/9-object-inheritance/06-prototype-methods/article.md b/1-js/9-object-inheritance/06-prototype-methods/article.md index 0643798c62..d6ab9358a2 100644 --- a/1-js/9-object-inheritance/06-prototype-methods/article.md +++ b/1-js/9-object-inheritance/06-prototype-methods/article.md @@ -212,10 +212,19 @@ So when `rabbit.hasOwnProperty` is called, the method `hasOwnProperty` is taken 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 [todo] +## Summary -The `"prototype"` property of constructor functions is essential for Javascript built-in methods. +Here's a brief list of methods we discussed in this chapter -- as a recap: -In this chapter we saw how it works for objects: +- [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. -In the next chapter we'll see the bigger picture: how other built-ins rely on it to inherit from each other. +`Object.create(null)` doesn't have any object properties and methods, and `__proto__` also doesn't exist for it. diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype-2.png b/1-js/9-object-inheritance/06-prototype-methods/object-prototype-2.png similarity index 100% rename from 1-js/9-object-inheritance/04-function-prototype/object-prototype-2.png rename to 1-js/9-object-inheritance/06-prototype-methods/object-prototype-2.png diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype-2@2x.png b/1-js/9-object-inheritance/06-prototype-methods/object-prototype-2@2x.png similarity index 100% rename from 1-js/9-object-inheritance/04-function-prototype/object-prototype-2@2x.png rename to 1-js/9-object-inheritance/06-prototype-methods/object-prototype-2@2x.png diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype-null.png b/1-js/9-object-inheritance/06-prototype-methods/object-prototype-null.png similarity index 100% rename from 1-js/9-object-inheritance/04-function-prototype/object-prototype-null.png rename to 1-js/9-object-inheritance/06-prototype-methods/object-prototype-null.png diff --git a/1-js/9-object-inheritance/04-function-prototype/object-prototype-null@2x.png b/1-js/9-object-inheritance/06-prototype-methods/object-prototype-null@2x.png similarity index 100% rename from 1-js/9-object-inheritance/04-function-prototype/object-prototype-null@2x.png rename to 1-js/9-object-inheritance/06-prototype-methods/object-prototype-null@2x.png diff --git a/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static.png b/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static.png index f7095d6c49..998c823300 100644 Binary files a/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static.png and b/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static.png differ diff --git a/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static@2x.png b/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static@2x.png index 04eb0538db..98a80d38fa 100644 Binary files a/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static@2x.png and b/1-js/9-object-inheritance/10-class-inheritance/animal-rabbit-static@2x.png differ diff --git a/1-js/9-object-inheritance/10-class-inheritance/article.md b/1-js/9-object-inheritance/10-class-inheritance/article.md index 7810d3d262..c613df024f 100644 --- a/1-js/9-object-inheritance/10-class-inheritance/article.md +++ b/1-js/9-object-inheritance/10-class-inheritance/article.md @@ -138,7 +138,28 @@ rabbit.stop(); // White Rabbit stopped. White rabbit hides! Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the process. -## Custom constructor +````smart header="Arrow functions have no `super`" +As was mentioned in the chapter , arrow functions do not have `super`. + +If accessed, it's taken from the outer function. For instance: +```js +class Rabbit extends Animal { + stop() { + setTimeout(() => super.stop(), 1000); // call parent stop after 1sec + } +} +``` + +The `super` in the arrow function is the same as in `stop()`, so it works as intended. If we specified a "regular" function here, there would be an error: + +```js +// Unexpected super +setTimeout(function() { super.stop() }, 1000); +``` +```` + + +## Overriding constructor With constructors, things are is a little bit tricky. @@ -242,17 +263,17 @@ alert(rabbit.earLength); // 10 ## Super: internals, [[HomeObject]] -Let's get a little deeper under the hood of `super`. There are some interesting things by the way. +Let's get a little deeper under the hood of `super`. We'll see some interesting things by the way. First to say, from all that we've learned till now, it's impossible for `super` to work. -Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, how it can get that method? +Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, how to retrieve that method? In other words, we need to take the `method` from the parent prototype of the current object. How, technically, we (or a Javascript engine) can do it? -Maybe it can just take it from `[[Prototype]]` of `this`? Unfortunately, no. +Maybe we can get it `[[Prototype]]` of `this`, as `this.__proto__.method`? Unfortunately, that won't work. Let's try to do it. Without classes, using plain objects for sheer simplicity. -Here, `rabbit.eat()` should call `animal.eat()`. +Here, `rabbit.eat()` should call `animal.eat()` method of the parent object: ```js run let animal = { @@ -292,16 +313,16 @@ let animal = { let rabbit = { __proto__: animal, eat() { - // bounce around rabbit-style and call parent - this.__proto__.eat.call(this); + // ...bounce around rabbit-style and call parent (animal) method + this.__proto__.eat.call(this); // (*) } }; let longEar = { __proto__: rabbit, eat() { - // do something with long ears and call parent - this.__proto__.eat.call(this); + // ...do something with long ears and call parent (rabbit) method + this.__proto__.eat.call(this); // (**) } }; @@ -310,10 +331,34 @@ longEar.eat(); // Error: Maximum call stack size exceeded */!* ``` -Doesn't work any more! If we trace `longEar.eat()` call, it becomes obvious, why: +The code doesn't work any more! We can see the error trying to call `longEar.eat()`. + +It may be not that obvious, but if we trace `longEar.eat()` call, then we can see why. In both lines `(*)` and `(**)` the value of `this` is the current object (`longEar`). That's essential: all object methods get the current object as `this`, not a prototype or something. + +So, in both lines `(*)` and `(**)` the value of `this.__proto__` is exactly the same: `rabbit`. They both call `rabbit.eat` without going up the chain. + +In other words: 1. Inside `longEar.eat()`, we pass the call up to `rabbit.eat` giving it the same `this=longEar`. + ```js + // inside longEar.eat() we have this = longEar + this.__proto__.eat.call(this) // (**) + // becomes + longEar.__proto__.eat.call(this) + // or + rabbit.eat.call(this); + ``` 2. Inside `rabbit.eat`, we want to pass the call even higher in the chain, but `this=longEar`, so `this.__proto__.eat` is `rabbit.eat`! + + ```js + // inside rabbit.eat() we also have this = longEar + this.__proto__.eat.call(this) // (*) + // becomes + longEar.__proto__.eat.call(this) + // or (again) + rabbit.eat.call(this); + ``` + 3. ...So `rabbit.eat` calls itself in the endless loop, because it can't ascend any further. ![](this-super-loop.png) @@ -464,3 +509,66 @@ Here's the picture structure for `Date` and `Object`: Note, there's no link between `Date` and `Object`. Both `Object` and `Date` exist independently. `Date.prototype` inherits from `Object.prototype`, but that's all. Such difference exists for historical reasons: there was no thought about class syntax and inheriting static methods at the dawn of JavaScript language. + +## Natives are extendable + +Built-in classes like Array, Map and others are extendable also. + +For instance, here `PowerArray` inherits from the native `Array`: + +```js run +// add one more method to it (can do more) +class PowerArray extends Array { + isEmpty() { + return this.length == 0; + } +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +let filteredArr = arr.filter(item => item >= 10); +alert(filteredArr); // 10, 50 +alert(filteredArr.isEmpty()); // false +``` + +Please note one very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type. They rely on the `constructor` property to do so. + +In the example above, +```js +arr.constructor === PowerArray +``` + +So when `arr.filter()` is called, it internally creates the new array of results exactly as `new PowerArray`. And we can keep using its methods further down the chain. + +Even more, we can customize that behavior. The static getter `Symbol.species`, if exists, returns the constructor to use in such cases. + +For example, here due to `Symbol.species` built-in methods like `map`, `filter` will return "normal" arrays: + +```js run +class PowerArray extends Array { + isEmpty() { + return this.length == 0; + } + +*!* + // built-in methods will use this as the constructor + static get [Symbol.species]() { + return Array; + } +*/!* +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +// filter creates new array using arr.constructor[Symbol.species] as constructor +let filteredArr = arr.filter(item => item >= 10); + +*!* +// filteredArr is not PowerArray, but Array +*/!* +alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function +``` + +We can use it in more advanced keys to strip extended functionality from resulting values if not needed. Or, maybe, to extend it even further. diff --git a/1-js/9-object-inheritance/10-class-inheritance/console_dir_array@2x.png b/1-js/9-object-inheritance/10-class-inheritance/console_dir_array@2x.png deleted file mode 100644 index cd8506f418..0000000000 Binary files a/1-js/9-object-inheritance/10-class-inheritance/console_dir_array@2x.png and /dev/null differ diff --git a/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance.png b/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance.png index 1746cb9af2..542a0c9fa9 100644 Binary files a/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance.png and b/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance.png differ diff --git a/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance@2x.png b/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance@2x.png index bf0866f808..21485062a8 100644 Binary files a/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance@2x.png and b/1-js/9-object-inheritance/10-class-inheritance/object-date-inheritance@2x.png differ diff --git a/1-js/9-object-inheritance/09-class/this-super-loop.png b/1-js/9-object-inheritance/10-class-inheritance/this-super-loop.png similarity index 100% rename from 1-js/9-object-inheritance/09-class/this-super-loop.png rename to 1-js/9-object-inheritance/10-class-inheritance/this-super-loop.png diff --git a/1-js/9-object-inheritance/09-class/this-super-loop@2x.png b/1-js/9-object-inheritance/10-class-inheritance/this-super-loop@2x.png similarity index 100% rename from 1-js/9-object-inheritance/09-class/this-super-loop@2x.png rename to 1-js/9-object-inheritance/10-class-inheritance/this-super-loop@2x.png diff --git a/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/solution.md b/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/solution.md index fd0c01a2b4..d41d90edf4 100644 --- a/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/solution.md +++ b/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/solution.md @@ -1,7 +1,7 @@ -Да, это выглядит достаточно странно, поскольку объект `a` не создавался функцией `B`. +Yeah, looks strange indeed. -Но методу `instanceof` на самом деле вообще не важна функция. Он смотрит на её `prototype` и сверяет его с цепочкой `__proto__` объекта. +But `instanceof` does not care about the function, but rather about its `prototype`, that it matches against the prototype chain. -В данном случае `a.__proto__ == B.prototype`, поэтому `instanceof` возвращает `true`. +And here `a.__proto__ == B.prototype`, so `instanceof` returns `true`. -По логике `instanceof` именно прототип задаёт "тип объекта", поэтому `instanceof` работает именно так. \ No newline at end of file +So, by the logic of `instanceof`, the `prototype` actually defines the type, not the constructor function. diff --git a/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/task.md b/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/task.md index feae0b4fd2..e9481912ae 100644 --- a/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/task.md +++ b/1-js/9-object-inheritance/11-instanceof/1-strange-instanceof/task.md @@ -2,21 +2,19 @@ importance: 5 --- -# Странное поведение instanceof +# Strange instanceof -Почему `instanceof` в коде ниже возвращает `true`, ведь объект `a` явно создан не `B()`? +Why `instanceof` below returns `true`? We can easily see that `a` is not created by `B()`. ```js run function A() {} - function B() {} A.prototype = B.prototype = {}; -var a = new A(); +let a = new A(); *!* alert( a instanceof B ); // true */!* ``` - diff --git a/1-js/9-object-inheritance/11-instanceof/2-instanceof-result/solution.md b/1-js/9-object-inheritance/11-instanceof/2-instanceof-result/solution.md deleted file mode 100644 index 115797e3c8..0000000000 --- a/1-js/9-object-inheritance/11-instanceof/2-instanceof-result/solution.md +++ /dev/null @@ -1,17 +0,0 @@ -Да, распознает. - -Он проверяет наследование с учётом цепочки прототипов. - -```js run -function Animal() {} - -function Rabbit() {} -Rabbit.prototype = Object.create(Animal.prototype); - -var rabbit = new Rabbit(); - -alert( rabbit instanceof Rabbit ); // true -alert( rabbit instanceof Animal ); // true -alert( rabbit instanceof Object ); // true -``` - diff --git a/1-js/9-object-inheritance/11-instanceof/2-instanceof-result/task.md b/1-js/9-object-inheritance/11-instanceof/2-instanceof-result/task.md deleted file mode 100644 index a08ac34869..0000000000 --- a/1-js/9-object-inheritance/11-instanceof/2-instanceof-result/task.md +++ /dev/null @@ -1,25 +0,0 @@ -importance: 5 - ---- - -# Что выведет instanceof? - -В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`. - -Что выведет [instanceof](/instanceof)? - -Распознает ли он `rabbit` как `Animal`, `Rabbit` и к тому же `Object`? - -```js -function Animal() {} - -function Rabbit() {} -Rabbit.prototype = Object.create(Animal.prototype); - -var rabbit = new Rabbit(); - -alert( rabbit instanceof Rabbit ); -alert( rabbit instanceof Animal ); -alert( rabbit instanceof Object ); -``` - diff --git a/1-js/9-object-inheritance/11-instanceof/article.md b/1-js/9-object-inheritance/11-instanceof/article.md index 8859ac2b15..392d0001ab 100644 --- a/1-js/9-object-inheritance/11-instanceof/article.md +++ b/1-js/9-object-inheritance/11-instanceof/article.md @@ -1,82 +1,209 @@ # Class checking: "instanceof" -The `instanceof` operator allows to check if an object belongs to the certain class. The inheritance is also taken into account. +The `instanceof` operator allows to check whether an object belongs to a certain class. It also takes inheritance into account. + +Such a check may be necessary in many cases, here we'll use it for building a *polymorphic* function, the one that treats arguments differently depending on their type. [cut] -## Алгоритм работы instanceof [#ref-instanceof] +## The instanceof operator [#ref-instanceof] + +The syntax is: +```js +obj instanceof Class +``` -Вызов `obj instanceof Constructor` возвращает `true`, если объект принадлежит классу `Constructor` или классу, наследующему от него. +It returns `true` if `obj` belongs to the `Class` (or a class inheriting from it). -Пример использования: +For instance: ```js run -function Rabbit() {} +class Rabbit {} +let rabbit = new Rabbit(); +// is it an object of Rabbit class? *!* -// создаём объект +alert( rabbit instanceof Rabbit ); // true */!* -var rabbit = new Rabbit(); +``` + +It also works with constructor functions: -// проверяем -- этот объект создан Rabbit? +```js run *!* -alert( rabbit instanceof Rabbit ); // true, верно +// instead of class +function Rabbit() {} */!* + +alert( new Rabbit() instanceof Rabbit ); // true ``` -Массив `arr` принадлежит классу `Array`, но также и является объектом `Object`. Это верно, так как массивы наследуют от объектов: +...And with built-in classes like `Array`: ```js run -var arr = []; +let arr = [1, 2, 3]; alert( arr instanceof Array ); // true alert( arr instanceof Object ); // true ``` -Как это часто бывает в JavaScript, здесь есть ряд тонкостей. Проверка происходит через сравнение прототипов, поэтому в некоторых ситуациях может даже ошибаться! +Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypally inherits from `Object`. + +The `instanceof` operator examines the prototype chain for the check, and is also fine-tunable using the static method `Symbol.hasInstance`. + +The algorithm of `obj instanceof Class` works roughly as follows: + +1. If there's a static method `Symbol.hasInstance`, then use it. Like this: + + ```js run + // assume anything that canEat is an animal + class Animal { + static [Symbol.hasInstance](obj) { + if (obj.canEat) return true; + } + } -**Алгоритм проверки `obj instanceof Constructor`:** + let obj = { canEat: true }; + alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called + ``` -1. Получить `obj.__proto__` -2. Сравнить `obj.__proto__` с `Constructor.prototype` -3. Если не совпадает, тогда заменить `obj` на `obj.__proto__` и повторить проверку на шаге 2 до тех пор, пока либо не найдется совпадение (результат `true`), либо цепочка прототипов не закончится (результат `false`). +2. Most classes do not have `Symbol.hasInstance`. In that case, check if `Class.prototype` equals to one of prototypes in the `obj` prototype chain. -В проверке `rabbit instanceof Rabbit` совпадение происходит на первом же шаге этого алгоритма, так как: `rabbit.__proto__ == Rabbit.prototype`. + In other words, compare: + ```js + obj.__proto__ == Class.prototype + obj.__proto__.__proto__ == Class.prototype + obj.__proto__.__proto__.__proto__ == Class.prototype + ... + ``` -А если рассмотреть `arr instanceof Object`, то совпадение будет найдено на следующем шаге, так как `arr.__proto__.__proto__ == Object.prototype`. + In the example above `Rabbit.prototype == rabbit.__proto__`, so that gives the answer immediately. -Забавно, что сама функция-конструктор не участвует в процессе проверки! Важна только цепочка прототипов для проверяемого объекта. + In the case of an inheritance, `rabbit` is an instance of the parent class as well: -Это может приводить к забавному результату и даже ошибкам в проверке при изменении `prototype`, например: + ```js run + class Animal {} + class Rabbit extends Animal {} + + let rabbit = new Rabbit(); + *!* + alert(rabbit instanceof Animal); // true + */!* + // rabbit.__proto__ == Rabbit.prototype + // rabbit.__proto__.__proto__ == Animal.prototype (match!) + ``` + +By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), that returns `true` if `objA` is somewhere in the chain of prototypes for `objB`. So the test of `obj instanceof Class` can be rephrased as `Class.prototype.isPrototypeOf(obj)`. + +Please note: that's funny, but the `Class` constructor itself does not participate in the check! Only the chain of prototypes and `Class.prototype` matters. + +That can lead to interesting consequences when `prototype` is changed. + +Like here: ```js run -// Создаём объект rabbit, как обычно function Rabbit() {} -var rabbit = new Rabbit(); +let rabbit = new Rabbit(); -// изменили prototype... +// changed the prototype Rabbit.prototype = {}; -// ...instanceof перестал работать! +// ...not a rabbit any more! *!* alert( rabbit instanceof Rabbit ); // false */!* ``` -Стоит ли говорить, что это один из доводов для того, чтобы никогда не менять `prototype`? Так сказать, во избежание. +That's one of reasons to evade changing `prototype`. Just to keep safe. + +## Bonus: Object toString for the type + +We already know that plain objects are converted to string as `[object Object]`: + +```js run +let obj = {}; + +alert(obj); // [object Object] +alert(obj.toString()); // the same +``` + +That's their implementation of `toString`. But there's a hidden feature thank makes `toString` actually much more powerful than that. We can use it as an extended `typeof` and an alternative for `instanceof`. + +Sounds strange? Indeed. Let's demistify. + +By [specification](https://tc39.github.io/ecma262/#sec-object.prototype.tostring), the built-in `toString` can be extracted from the object and executed in the context of any other value. And its result depends on that value. + +- For a number, it will be `[object Number]` +- For a boolean, it will be `[object Boolean]` +- For `null`: `[object Null]` +- For `undefined`: `[object Undefined]` +- For arrays: `[object Array]` +- ...etc (customizable). + +Let's demonstrate: + +```js run +// copy toString method into a variable for convenience +let objectToString = Object.prototype.toString; + +// what type is this? +let arr = []; + +alert( objectToString.call(arr) ); // [object Array] +``` + +Here we used [call](mdn:js/function/call) as described in the chapter [](info:call-apply-decorators) to execute the function `objectToString` in the context `this=arr`. + +Internally, the `toString` algorithm examines `this` and returns the corresponding result. More examples: + +```js run +let s = Object.prototype.toString; + +alert( s.call(123) ); // [object Number] +alert( s.call(null) ); // [object Null] +alert( s.call(alert) ); // [object Function] +``` + +### Symbol.toStringTag -```warn header="Не друзья: `instanceof` и фреймы" -Оператор `instanceof` не срабатывает, когда значение приходит из другого окна или фрейма. +The behavior of Object `toString` can be customized using a special object property `Symbol.toStringTag`. -Например, массив, который создан в ифрейме и передан родительскому окну -- будет массивом *в том ифрейме*, но не в родительском окне. Проверка `instanceof Array` в родительском окне вернёт `false`. +For instance: -Вообще, у каждого окна и фрейма -- своя иерархия объектов и свой `window` . +```js run +let user = { + [Symbol.toStringTag]: 'User' +}; -Как правило, эта проблема возникает со встроенными объектами, в этом случае используется проверка внутреннего свойства `[[Class]]`, которое подробнее описано в главе . +alert( {}.toString.call(user) ); // [object User] ``` -## Итого +For most environment-specific objects, there is such a property. Here are few browser specific examples: + +```js run +// toStringTag for the envinronment-specific object and class: +alert( window[Symbol.toStringTag]); // window +alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest + +alert( {}.toString.call(window) ); // [object Window] +alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] +``` + +As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped into `[object ...]`. + +At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized. + +It can be used instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check. + +## Summary + +Let's recap the type-checking methods that we know: + +| | works for | returns | +|---------------|-------------|---------------| +| `typeof` | primitives | string | +| `{}.toString` | primitives, built-in objects, objects with `Symbol.toStringTag` | string | +| `instanceof` | objects | true/false | -- Оператор `obj instanceof Func` проверяет тот факт, что `obj` является результатом вызова `new Func`. Он учитывает цепочку `__proto__`, поэтому наследование поддерживается. -- Оператор `instanceof` не сможет проверить тип значения, если объект создан в одном окне/фрейме, а проверяется в другом. Это потому, что в каждом окне -- своя иерархия объектов. Для точной проверки типов встроенных объектов можно использовать свойство `[[Class]]`. +As we can see, `{}.toString` is technically a "more advanced" `typeof`. -Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования. +And `instanceof` operator really shines when we are working with a class hierarchy and want to check for the class taking into account inheritance. diff --git a/1-js/9-object-inheritance/13-mixins/article.md b/1-js/9-object-inheritance/13-mixins/article.md index 44c49f4720..7b6c5c2949 100644 --- a/1-js/9-object-inheritance/13-mixins/article.md +++ b/1-js/9-object-inheritance/13-mixins/article.md @@ -1,77 +1,116 @@ -# Примеси +# Mixins -В JavaScript невозможно унаследовать от двух и более объектов. Ссылка `__proto__` -- только одна. +In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]`. -Но потребность такая существует -- к примеру, мы написали код, релизующий методы работы с шаблонизатором или методы по обмену событиями, и хочется легко и непринуждённо добавлять эти возможности к любому классу. +But sometimes we need such kind of thing. For instance, we have a code that implements events exchange or templating, and we'd like to be able to add these capabilities to any class easily. -Обычно это делают через примеси. +What can help here is *mixins* or *traits*. -Примесь (англ. mixin) -- класс или объект, реализующий какое-либо чётко выделенное поведение. Используется для уточнения поведения других классов, не предназначен для самостоятельного использования. +A *mixin* is a class or an object that implements a certain behavior. We do not use it alone, but rather use to complement other classes. +[cut] - +## A mixin example -## Пример примеси +The simplest way to make a mixin -- is to make an object with useful methods, that we can just copy into the prototype. -Самый простой вариант примеси -- это объект с полезными методами, которые мы просто копируем в нужный прототип. - -Например: +For instance: ```js run *!* -// примесь +// mixin */!* -var sayHiMixin = { - sayHi: function() { - alert("Привет " + this.name); +let sayHiMixin = { + sayHi() { + alert("Hello " + this.name); }, - sayBye: function() { - alert("Пока " + this.name); + sayBye() { + alert("Bye " + this.name); } }; *!* -// использование: +// usage: */!* -function User(name) { - this.name = name; +class User { + constructor(name) { + this.name = name; + } } -// передать методы примеси -for(var key in sayHiMixin) User.prototype[key] = sayHiMixin[key]; +// copy the methods +Object.assign(User.prototype, sayHiMixin); -// User "умеет" sayHi -new User("Вася").sayHi(); // Привет Вася +// now User can say hi +new User("Dude").sayHi(); // Hi Dude! ``` -Как видно из примера, методы примеси активно используют `this` и предназначены именно для запуска в контексте "объекта-носителя примеси". +Mixins also can inherit from each other. -Если какие-то из методов примеси не нужны -- их можно перезаписать своими после копирования. +For instance, here `sayHiMixin` inherits from `sayMixin`: -## Примесь для событий +```js run +let sayMixin = { + say(phrase) { + alert(phrase); + } +}; + +let sayHiMixin = { + __proto__: sayMixin, // can use other prototype-setting methods instead +*!* + // call parent method +*/!* + sayHi() { + super.say("Hello " + this.name); + }, + sayBye() { + super.say("Bye " + this.name); + } +}; + +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hi Dude! +``` -Теперь пример из реальной жизни. +Please note that the call to `super.say()` from a mixin looks for the method in the prototype of that mixin, not the class. -Важный аспект, который может понадобиться объектам -- это умение работать с событиями. +That's because methods from `sayHiMixin` have `[[HomeObject]]` set to `sayHiMixin`. So `super` actually means `sayHiMixin.__proto__`, that is `sayMixin`. -То есть, чтобы объект мог специальным вызовом генерировать "уведомление о событии", а на эти уведомления другие объекты могли "подписываться", чтобы их получать. +## EventMixin -Например, объект "Пользователь" при входе на сайт может генерировать событие `"login"`, а другие объекты, например "Календарь" может такие уведомления получать и подгружать информацию о пользователе. +Now a mixin for the real life. -Или объект "Меню" может при выборе пункта меню генерировать событие `"select"` с информацией о выбранном пункте меню, а другие объекты -- подписавшись на это событие, будут узнавать об этом. +The important feature of many objects is working with events. -События -- это средство "поделиться информацией" с неопределённым кругом заинтересованных лиц. А там уже кому надо -- тот среагирует. +That is: an object should have a method to "generate an event" when something important happens to him, and other objects should be able to "subscribe" to receive such notifications. -Примесь `eventMixin`, реализующая события: +An event must have a name and, if necessary, the attached data. -```js -var eventMixin = { +For instance, an object `user` can generate an event `"login"` when the visitor logs in. And an object `calendar` may want to receive such notifications and load the information about that visitor. + +Or, the object `menu` can generate the event `"select"` when a menu item is selected, and other objects may want to get that information and react on that event. + +Events is a way to "share information" with anyone who wants it. + +The `eventMixin` to implement the corresponding methods: + +```js run +let eventMixin = { /** - * Подписка на событие - * Использование: + * Subscribe to event, usage: * menu.on('select', function(item) { ... } */ - on: function(eventName, handler) { + on(eventName, handler) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; @@ -80,13 +119,13 @@ var eventMixin = { }, /** - * Прекращение подписки - * menu.off('select', handler) + * Cancel the subscription, usage: + * menu.off('select', handler) */ - off: function(eventName, handler) { - var handlers = this._eventHandlers && this._eventHandlers[eventName]; + off(eventName, handler) { + let handlers = this._eventHandlers && this._eventHandlers[eventName]; if (!handlers) return; - for(var i=0; i handler.apply(this, args)); } }; ``` -Здесь есть три метода: +There are 3 methods here: -1. `.on(имя события, функция)` -- назначает функцию к выполнению при наступлении события с данным именем. Такие функции хранятся в защищённом свойстве объекта `_eventHandlers`. -2. `.off(имя события, функция)` -- удаляет функцию из списка предназначенных к выполнению. -3. `.trigger(имя события, аргументы)` -- генерирует событие, при этом вызываются все назначенные на него функции, и им передаются аргументы. +1. `.on(eventName, handler)` -- assigns the function `handler` to run when the event with that name happens. The handlers are stored in `_eventHandlers` property. +2. `.off(eventName, handler)` -- removes the function from the handlers list. +3. `.trigger(eventName, ...args)` -- generates the event: all assigned handlers are called and `args` are passed as arguments to them. -Использование: -```js -// Класс Menu с примесью eventMixin -function Menu() { - // ... -} - -for(var key in eventMixin) { - Menu.prototype[key] = eventMixin[key]; -} +Usage: -// Генерирует событие select при выборе значения -Menu.prototype.choose = function(value) { -*!* - this.trigger("select", value); -*/!* +```js run +// Make a class +class Menu { + choose(value) { + this.trigger("select", value); + } } +// Add the mixin +Object.assign(Menu.prototype, eventMixin); -// Создадим меню -var menu = new Menu(); +let menu = new Menu(); -// При наступлении события select вызвать эту функцию +// call the handler on selection: *!* -menu.on("select", function(value) { - alert("Выбрано значение " + value); -}); +menu.on("select", value => alert("Value selected: " + value)); */!* -// Запускаем выбор (событие select вызовет обработчики) -menu.choose("123"); +// the choice has happened! +menu.choose("123"); // value selected ``` -...То есть, смысл событий -- обычно в том, что объект, в процессе своей деятельности, внутри себя (`this.trigger`) генерирует уведомления, на которые внешний код через `menu.on(...)` может быть подписан. И узнавать из них ценную информацию о происходящем, например -- что выбран некий пункт меню. +Now if we have the code interested to react on user selection, we can bind it with `menu.on(...)`. -Один раз написав методы `on/off/trigger` в примеси, мы затем можем использовать их во множестве прототипов. +And the mixin can be added to as many classes as we'd like. -## Итого +## Summary [todo] - Примесь -- объект, содержащий методы и свойства для реализации конкретного функционала. Возможны вариации этого приёма проектирования. Например, примесь может предусматривать конструктор, который должен запускаться в конструкторе объекта. Но как правило просто набора методов хватает. - Для добавления примеси в класс -- её просто "подмешивают" в прототип. - "Подмешать" можно сколько угодно примесей, но если имена методов в разных примесях совпадают, то возможны конфликты. Их уже разрешать -- разработчику. Например, можно заменить конфликтующий метод на свой, который будет решать несколько задач сразу. Конфликты при грамотно оформленных примесях возникают редко. - diff --git a/1-js/9-object-inheritance/13-mixins/head.html b/1-js/9-object-inheritance/13-mixins/head.html new file mode 100644 index 0000000000..77ea38b204 --- /dev/null +++ b/1-js/9-object-inheritance/13-mixins/head.html @@ -0,0 +1,43 @@ + diff --git a/figures.bug.sketch b/figures.bug.sketch deleted file mode 100644 index 47f22bca98..0000000000 Binary files a/figures.bug.sketch and /dev/null differ diff --git a/figures.new.sketch b/figures.new.sketch deleted file mode 100644 index 6b392a99f4..0000000000 Binary files a/figures.new.sketch and /dev/null differ diff --git a/figures.sketch b/figures.sketch index aee74ea0a5..a5677be06d 100644 Binary files a/figures.sketch and b/figures.sketch differ