Repository: azat-io/you-dont-know-js-ru Branch: master Commit: d763a343b438 Files: 67 Total size: 1.6 MB Directory structure: gitextract_bdk32l8f/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── async & performance/ │ ├── README.md │ ├── apA.md │ ├── apB.md │ ├── apC.md │ ├── ch1.md │ ├── ch2.md │ ├── ch3.md │ ├── ch4.md │ ├── ch5.md │ ├── ch6.md │ ├── foreword.md │ └── toc.md ├── es6 & beyond/ │ ├── README.md │ ├── apA.md │ ├── ch1.md │ ├── ch2.md │ ├── ch3.md │ ├── ch4.md │ ├── ch5.md │ ├── ch6.md │ ├── ch7.md │ ├── ch8.md │ ├── foreword.md │ └── toc.md ├── preface.md ├── scope & closures/ │ ├── README.md │ ├── apA.md │ ├── apB.md │ ├── apC.md │ ├── apD.md │ ├── ch1.md │ ├── ch2.md │ ├── ch3.md │ ├── ch4.md │ ├── ch5.md │ └── toc.md ├── this & object prototypes/ │ ├── README.md │ ├── apA.md │ ├── apB.md │ ├── ch1.md │ ├── ch2.md │ ├── ch3.md │ ├── ch4.md │ ├── ch5.md │ ├── ch6.md │ ├── foreword.md │ └── toc.md ├── types & grammar/ │ ├── README.md │ ├── apA.md │ ├── apB.md │ ├── ch1.md │ ├── ch2.md │ ├── ch3.md │ ├── ch4.md │ ├── ch5.md │ ├── foreword.md │ └── toc.md └── up & going/ ├── README.md ├── apA.md ├── ch1.md ├── ch2.md ├── ch3.md ├── foreword.md └── toc.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ================================================ FILE: LICENSE.txt ================================================ Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. License THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 1. Definitions "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. "Distribute" means to make available to the public the original and copies of the Work through sale or other transfer of ownership. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; and, to Distribute and Publicly Perform the Work including as incorporated in Collections. The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats, but otherwise you have no rights to make Adaptations. Subject to 8(f), all rights not expressly granted by Licensor are hereby reserved, including but not limited to the rights set forth in Section 4(d). 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. You may not exercise any of the rights granted to You in Section 3 above in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation. The exchange of the Work for other copyrighted works by means of digital file-sharing or otherwise shall not be considered to be intended for or directed toward commercial advantage or private monetary compensation, provided there is no payment of any monetary compensation in connection with the exchange of copyrighted works. If You Distribute, or Publicly Perform the Work or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work. The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Collection, at a minimum such credit will appear, if a credit for all contributing authors of Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. For the avoidance of doubt: Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License if Your exercise of such rights is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b) and otherwise waives the right to collect royalties through any statutory or compulsory licensing scheme; and, Voluntary License Schemes. The Licensor reserves the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License that is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b). Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. 5. Representations, Warranties and Disclaimer UNLESS OTHERWISE MUTUALLY AGREED BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. Termination This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 8. Miscellaneous Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. Creative Commons Notice Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License. Creative Commons may be contacted at http://creativecommons.org/. ================================================ FILE: README.md ================================================ # Вы не знаете JS (серия книг) Представляю вам серию книг, погружение в которую позволит вам окунуться в основные механизмы языка JavaScript. Первое издание серии книг в настоящий момент завершено.           Не стесняйтесь вносить свой вклад в улучшение качества материала данной серии книг, отправляйте свои PR для улучшения фрагментов кода, пояснений и т.д. Исправление опечаток также приветствуется. **О том, что послужило причиной для написания данной серии книг, вы можете узнать из [Предисловия](preface.md).** ## Содержимое * Читать онлайн (бесплатно!): [«Начните и Совершенствуйтесь»](up%20%26%20going/README.md#Вы-не-знаете-js-Начните-и-Совершенствуйтесь), Опубликовано: [Купить сейчас](http://shop.oreilly.com/product/0636920039303.do) в бумажном варианте, электронная книга бесплатна! * Читать онлайн (бесплатно!): [«Область Видимости и Замыкания»](scope%20%26%20closures/README.md#Вы-не-знаете-js-Область-видимости-и-замыкания), Опубликовано: [Купить сейчас](http://shop.oreilly.com/product/0636920026327.do) * Читать онлайн (бесплатно!): [«This и Прототипы Объектов»](this%20%26%20object%20prototypes/README.md#you-dont-know-js-this--object-prototypes), Опубликовано: [Купить сейчас](http://shop.oreilly.com/product/0636920033738.do) * Читать онлайн (бесплатно!): [«Типы и Синтаксис»](types%20%26%20grammar/README.md#you-dont-know-js-types--grammar), Опубликовано: [Купить сейчас](http://shop.oreilly.com/product/0636920033745.do) * Читать онлайн (бесплатно!): [«Асинхронность и Производительность»](async%20%26%20performance/README.md#you-dont-know-js-async--performance), Опубликовано: [Купить сейчас](http://shop.oreilly.com/product/0636920033752.do) * Читать онлайн (бесплатно!): [«ES6 и не только»](es6%20%26%20beyond/README.md#you-dont-know-js-es6--beyond), Опубликовано: [Купить сейчас](https://www.ozon.ru/context/detail/id/137473815/) ## Издательство Серия книг свободно распространяется для чтения, но редактируется, производится и публикуется в книжном издательстве O'Reilly. Если вам нравится содержание книг, и вы хотите поддержать развитие серии, пожалуйста, приобретите книги, как только они будут доступны для продажи, там, где вы обычно их покупаете. :) Если же вы хотите поддержать данную работу финансово, но не хотите покупать книги, вы можете это сделать через сервис [Patreon](https://www.patreon.com/getify); я буду глубоко признателен вам за вашу щедрость. [![patreon.png](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/getify) ## Очное обучение Содержание этих книг в значительной степени состоит из учебных материалов, которые я преподаю профессионально в формате следующих семинаров (как бесплатных, так и платных): "Deep JavaScript Foundations", "Rethinking Async" и "ES6: The Right Parts". Если вам нравятся мои книги, и вы бы хотели связаться со мной по поводу очного обучения в рамках вышеупомянутого учебного курса или других различных моих семинаров по JS / HTML5 / Node.JS, пожалуйста, обратитесь ко мне по электронной почте getify@gmail.com ## Онлайн обучение Кроме этого, я также преподаю некоторый учебный материал в формате видео. Я веду дистанционные курсы на сервисе [Frontend Masters](https://FrontendMasters.com), такие как [Глубокое понимание основ JavaScript](https://frontendmasters.com/courses/javascript-foundations/). Вы можете найти [все мои курсы здесь](https://frontendmasters.com/kyle-simpson/). Некоторые из этих курсов также доступны на других платформах, таких как Pluralsight, Lynda.com и O'Reilly Safari Online. ## Вклад в развитие контента Любой вклад в развитие данного проекта **будет высоко оценён**. Тем не менее, учитывайте то, что если вы желаете внести свой вклад в содержание книг (а не только исправить опечатки) в этом репозитории, вы соглашаетесь на то, что вы даёте мне абсолютные права на использование добавленного вами контента для серии книг таким образом, каким я (и O'Reilly) сочту это целесообразным. Бла-бла-бла... :) ## Лицензия и Авторские права Автор материалов данной серии книг © 2013-2015 Kyle Simpson. Creative Commons License
Лицензируется в соответствии с Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License. ================================================ FILE: async & performance/README.md ================================================ # Вы не знаете JS: Асинхронность и Производительность ----- **[Купить цифровую или печатную книгу от издательства O'Reilly (англ.)](http://shop.oreilly.com/product/0636920033752.do)** ----- [Оглавление](toc.md) * [Введение](foreword.md) (от [Jake Archibald](http://jakearchibald.com)) * [Предисловие](../preface.md) * [Глава 1: Асинхронность: Сейчас и Тогда](ch1.md) * [Глава 2: Колбеки](ch2.md) * [Глава 3: Обещания](ch3.md) * [Глава 4: Генераторы](ch4.md) * [Глава 5: Производительность программы](ch5.md) * [Глава 6: Бенчмаркинг и настройка](ch6.md) * [Приложение A: Библиотека: asynquence](apA.md) * [Приложение B: Расширенные асинхронные шаблоны](apB.md) * [Приложение C: Благодарности!](apC.md) ================================================ FILE: async & performance/apA.md ================================================ # You Don't Know JS: Async & Performance # Appendix A: *asynquence* Library Chapters 1 and 2 went into quite a bit of detail about typical asynchronous programming patterns and how they're commonly solved with callbacks. But we also saw why callbacks are fatally limited in capability, which led us to Chapters 3 and 4, with Promises and generators offering a much more solid, trustable, and reason-able base to build your asynchrony on. I referenced my own asynchronous library *asynquence* (http://github.com/getify/asynquence) -- "async" + "sequence" = "asynquence" -- several times in this book, and I want to now briefly explain how it works and why its unique design is important and helpful. In the next appendix, we'll explore some advanced async patterns, but you'll probably want a library to make those palatable enough to be useful. We'll use *asynquence* to express those patterns, so you'll want to spend a little time here getting to know the library first. *asynquence* is obviously not the only option for good async coding; certainly there are many great libraries in this space. But *asynquence* provides a unique perspective by combining the best of all these patterns into a single library, and moreover is built on a single basic abstraction: the (async) sequence. My premise is that sophisticated JS programs often need bits and pieces of various different asynchronous patterns woven together, and this is usually left entirely up to each developer to figure out. Instead of having to bring in two or more different async libraries that focus on different aspects of asynchrony, *asynquence* unifies them into variated sequence steps, with just one core library to learn and deploy. I believe the value is strong enough with *asynquence* to make async flow control programming with Promise-style semantics super easy to accomplish, so that's why we'll exclusively focus on that library here. To begin, I'll explain the design principles behind *asynquence*, and then we'll illustrate how its API works with code examples. ## Sequences, Abstraction Design Understanding *asynquence* begins with understanding a fundamental abstraction: any series of steps for a task, whether they separately are synchronous or asynchronous, can be collectively thought of as a "sequence". In other words, a sequence is a container that represents a task, and is comprised of individual (potentially async) steps to complete that task. Each step in the sequence is controlled under the covers by a Promise (see Chapter 3). That is, every step you add to a sequence implicitly creates a Promise that is wired to the previous end of the sequence. Because of the semantics of Promises, every single step advancement in a sequence is asynchronous, even if you synchronously complete the step. Moreover, a sequence will always proceed linearly from step to step, meaning that step 2 always comes after step 1 finishes, and so on. Of course, a new sequence can be forked off an existing sequence, meaning the fork only occurs once the main sequence reaches that point in the flow. Sequences can also be combined in various ways, including having one sequence subsumed by another sequence at a particular point in the flow. A sequence is kind of like a Promise chain. However, with Promise chains, there is no "handle" to grab that references the entire chain. Whichever Promise you have a reference to only represents the current step in the chain plus any other steps hanging off it. Essentially, you cannot hold a reference to a Promise chain unless you hold a reference to the first Promise in the chain. There are many cases where it turns out to be quite useful to have a handle that references the entire sequence collectively. The most important of those cases is with sequence abort/cancel. As we covered extensively in Chapter 3, Promises themselves should never be able to be canceled, as this violates a fundamental design imperative: external immutability. But sequences have no such immutability design principle, mostly because sequences are not passed around as future-value containers that need immutable value semantics. So sequences are the proper level of abstraction to handle abort/cancel behavior. *asynquence* sequences can be `abort()`ed at any time, and the sequence will stop at that point and not go for any reason. There's plenty more reasons to prefer a sequence abstraction on top of Promise chains, for flow control purposes. First, Promise chaining is a rather manual process -- one that can get pretty tedious once you start creating and chaining Promises across a wide swath of your programs -- and this tedium can act counterproductively to dissuade the developer from using Promises in places where they are quite appropriate. Abstractions are meant to reduce boilerplate and tedium, so the sequence abstraction is a good solution to this problem. With Promises, your focus is on the individual step, and there's little assumption that you will keep the chain going. With sequences, the opposite approach is taken, assuming the sequence will keep having more steps added indefinitely. This abstraction complexity reduction is especially powerful when you start thinking about higher-order Promise patterns (beyond `race([..])` and `all([..])`. For example, in the middle of a sequence, you may want to express a step that is conceptually like a `try..catch` in that the step will always result in success, either the intended main success resolution or a positive nonerror signal for the caught error. Or, you might want to express a step that is like a retry/until loop, where it keeps trying the same step over and over until success occurs. These sorts of abstractions are quite nontrivial to express using only Promise primitives, and doing so in the middle of an existing Promise chain is not pretty. But if you abstract your thinking to a sequence, and consider a step as a wrapper around a Promise, that step wrapper can hide such details, freeing you to think about the flow control in the most sensible way without being bothered by the details. Second, and perhaps more importantly, thinking of async flow control in terms of steps in a sequence allows you to abstract out the details of what types of asynchronicity are involved with each individual step. Under the covers, a Promise will always control the step, but above the covers, that step can look either like a continuation callback (the simple default), or like a real Promise, or as a run-to-completion generator, or ... Hopefully, you get the picture. Third, sequences can more easily be twisted to adapt to different modes of thinking, such as event-, stream-, or reactive-based coding. *asynquence* provides a pattern I call "reactive sequences" (which we'll cover later) as a variation on the "reactive observable" ideas in RxJS ("Reactive Extensions"), that lets a repeatable event fire off a new sequence instance each time. Promises are one-shot-only, so it's quite awkward to express repetitious asynchrony with Promises alone. Another alternate mode of thinking inverts the resolution/control capability in a pattern I call "iterable sequences". Instead of each individual step internally controlling its own completion (and thus advancement of the sequence), the sequence is inverted so the advancement control is through an external iterator, and each step in the *iterable sequence* just responds to the `next(..)` *iterator* control. We'll explore all of these different variations as we go throughout the rest of this appendix, so don't worry if we ran over those bits far too quickly just now. The takeaway is that sequences are a more powerful and sensible abstraction for complex asynchrony than just Promises (Promise chains) or just generators, and *asynquence* is designed to express that abstraction with just the right level of sugar to make async programming more understandable and more enjoyable. ## *asynquence* API To start off, the way you create a sequence (an *asynquence* instance) is with the `ASQ(..)` function. An `ASQ()` call with no parameters creates an empty initial sequence, whereas passing one or more values or functions to `ASQ(..)` sets up the sequence with each argument representing the initial steps of the sequence. **Note:** For the purposes of all code examples here, I will use the *asynquence* top-level identifier in global browser usage: `ASQ`. If you include and use *asynquence* through a module system (browser or server), you of course can define whichever symbol you prefer, and *asynquence* won't care! Many of the API methods discussed here are built into the core of *asynquence*, but others are provided through including the optional "contrib" plug-ins package. See the documentation for *asynquence* for whether a method is built in or defined via plug-in: http://github.com/getify/asynquence ### Steps If a function represents a normal step in the sequence, that function is invoked with the first parameter being the continuation callback, and any subsequent parameters being any messages passed on from the previous step. The step will not complete until the continuation callback is called. Once it's called, any arguments you pass to it will be sent along as messages to the next step in the sequence. To add an additional normal step to the sequence, call `then(..)` (which has essentially the exact same semantics as the `ASQ(..)` call): ```js ASQ( // step 1 function(done){ setTimeout( function(){ done( "Hello" ); }, 100 ); }, // step 2 function(done,greeting) { setTimeout( function(){ done( greeting + " World" ); }, 100 ); } ) // step 3 .then( function(done,msg){ setTimeout( function(){ done( msg.toUpperCase() ); }, 100 ); } ) // step 4 .then( function(done,msg){ console.log( msg ); // HELLO WORLD } ); ``` **Note:** Though the name `then(..)` is identical to the native Promises API, this `then(..)` is different. You can pass as few or as many functions or values to `then(..)` as you'd like, and each is taken as a separate step. There's no two-callback fulfilled/rejected semantics involved. Unlike with Promises, where to chain one Promise to the next you have to create and `return` that Promise from a `then(..)` fulfillment handler, with *asynquence*, all you need to do is call the continuation callback -- I always call it `done()` but you can name it whatever suits you -- and optionally pass it completion messages as arguments. Each step defined by `then(..)` is assumed to be asynchronous. If you have a step that's synchronous, you can either just call `done(..)` right away, or you can use the simpler `val(..)` step helper: ```js // step 1 (sync) ASQ( function(done){ done( "Hello" ); // manually synchronous } ) // step 2 (sync) .val( function(greeting){ return greeting + " World"; } ) // step 3 (async) .then( function(done,msg){ setTimeout( function(){ done( msg.toUpperCase() ); }, 100 ); } ) // step 4 (sync) .val( function(msg){ console.log( msg ); } ); ``` As you can see, `val(..)`-invoked steps don't receive a continuation callback, as that part is assumed for you -- and the parameter list is less cluttered as a result! To send a message along to the next step, you simply use `return`. Think of `val(..)` as representing a synchronous "value-only" step, which is useful for synchronous value operations, logging, and the like. ### Errors One important difference with *asynquence* compared to Promises is with error handling. With Promises, each individual Promise (step) in a chain can have its own independent error, and each subsequent step has the ability to handle the error or not. The main reason for this semantic comes (again) from the focus on individual Promises rather than on the chain (sequence) as a whole. I believe that most of the time, an error in one part of a sequence is generally not recoverable, so the subsequent steps in the sequence are moot and should be skipped. So, by default, an error at any step of a sequence throws the entire sequence into error mode, and the rest of the normal steps are ignored. If you *do* need to have a step where its error is recoverable, there are several different API methods that can accommodate, such as `try(..)` -- previously mentioned as a kind of `try..catch` step -- or `until(..)` -- a retry loop that keeps attempting the step until it succeeds or you manually `break()` the loop. *asynquence* even has `pThen(..)` and `pCatch(..)` methods, which work identically to how normal Promise `then(..)` and `catch(..)` work (see Chapter 3), so you can do localized mid-sequence error handling if you so choose. The point is, you have both options, but the more common one in my experience is the default. With Promises, to get a chain of steps to ignore all steps once an error occurs, you have to take care not to register a rejection handler at any step; otherwise, that error gets swallowed as handled, and the sequence may continue (perhaps unexpectedly). This kind of desired behavior is a bit awkward to properly and reliably handle. To register a sequence error notification handler, *asynquence* provides an `or(..)` sequence method, which also has an alias of `onerror(..)`. You can call this method anywhere in the sequence, and you can register as many handlers as you'd like. That makes it easy for multiple different consumers to listen in on a sequence to know if it failed or not; it's kind of like an error event handler in that respect. Just like with Promises, all JS exceptions become sequence errors, or you can programmatically signal a sequence error: ```js var sq = ASQ( function(done){ setTimeout( function(){ // signal an error for the sequence done.fail( "Oops" ); }, 100 ); } ) .then( function(done){ // will never get here } ) .or( function(err){ console.log( err ); // Oops } ) .then( function(done){ // won't get here either } ); // later sq.or( function(err){ console.log( err ); // Oops } ); ``` Another really important difference with error handling in *asynquence* compared to native Promises is the default behavior of "unhandled exceptions". As we discussed at length in Chapter 3, a rejected Promise without a registered rejection handler will just silently hold (aka swallow) the error; you have to remember to always end a chain with a final `catch(..)`. In *asynquence*, the assumption is reversed. If an error occurs on a sequence, and it **at that moment** has no error handlers registered, the error is reported to the `console`. In other words, unhandled rejections are by default always reported so as not to be swallowed and missed. As soon as you register an error handler against a sequence, it opts that sequence out of such reporting, to prevent duplicate noise. There may, in fact, be cases where you want to create a sequence that may go into the error state before you have a chance to register the handler. This isn't common, but it can happen from time to time. In those cases, you can also **opt a sequence instance out** of error reporting by calling `defer()` on the sequence. You should only opt out of error reporting if you are sure that you're going to eventually handle such errors: ```js var sq1 = ASQ( function(done){ doesnt.Exist(); // will throw exception to console } ); var sq2 = ASQ( function(done){ doesnt.Exist(); // will throw only a sequence error } ) // opt-out of error reporting .defer(); setTimeout( function(){ sq1.or( function(err){ console.log( err ); // ReferenceError } ); sq2.or( function(err){ console.log( err ); // ReferenceError } ); }, 100 ); // ReferenceError (from sq1) ``` This is better error handling behavior than Promises themselves have, because it's the Pit of Success, not the Pit of Failure (see Chapter 3). **Note:** If a sequence is piped into (aka subsumed by) another sequence -- see "Combining Sequences" for a complete description -- then the source sequence is opted out of error reporting, but now the target sequence's error reporting or lack thereof must be considered. ### Parallel Steps Not all steps in your sequences will have just a single (async) task to perform; some will need to perform multiple steps "in parallel" (concurrently). A step in a sequence in which multiple substeps are processing concurrently is called a `gate(..)` -- there's an `all(..)` alias if you prefer -- and is directly symmetric to native `Promise.all([..])`. If all the steps in the `gate(..)` complete successfully, all success messages will be passed to the next sequence step. If any of them generate errors, the whole sequence immediately goes into an error state. Consider: ```js ASQ( function(done){ setTimeout( done, 100 ); } ) .gate( function(done){ setTimeout( function(){ done( "Hello" ); }, 100 ); }, function(done){ setTimeout( function(){ done( "World", "!" ); }, 100 ); } ) .val( function(msg1,msg2){ console.log( msg1 ); // Hello console.log( msg2 ); // [ "World", "!" ] } ); ``` For illustration, let's compare that example to native Promises: ```js new Promise( function(resolve,reject){ setTimeout( resolve, 100 ); } ) .then( function(){ return Promise.all( [ new Promise( function(resolve,reject){ setTimeout( function(){ resolve( "Hello" ); }, 100 ); } ), new Promise( function(resolve,reject){ setTimeout( function(){ // note: we need a [ ] array here resolve( [ "World", "!" ] ); }, 100 ); } ) ] ); } ) .then( function(msgs){ console.log( msgs[0] ); // Hello console.log( msgs[1] ); // [ "World", "!" ] } ); ``` Yuck. Promises require a lot more boilerplate overhead to express the same asynchronous flow control. That's a great illustration of why the *asynquence* API and abstraction make dealing with Promise steps a lot nicer. The improvement only goes higher the more complex your asynchrony is. #### Step Variations There are several variations in the contrib plug-ins on *asynquence*'s `gate(..)` step type that can be quite helpful: * `any(..)` is like `gate(..)`, except just one segment has to eventually succeed to proceed on the main sequence. * `first(..)` is like `any(..)`, except as soon as any segment succeeds, the main sequence proceeds (ignoring subsequent results from other segments). * `race(..)` (symmetric with `Promise.race([..])`) is like `first(..)`, except the main sequence proceeds as soon as any segment completes (either success or failure). * `last(..)` is like `any(..)`, except only the latest segment to complete successfully sends its message(s) along to the main sequence. * `none(..)` is the inverse of `gate(..)`: the main sequence proceeds only if all the segments fail (with all segment error message(s) transposed as success message(s) and vice versa). Let's first define some helpers to make illustration cleaner: ```js function success1(done) { setTimeout( function(){ done( 1 ); }, 100 ); } function success2(done) { setTimeout( function(){ done( 2 ); }, 100 ); } function failure3(done) { setTimeout( function(){ done.fail( 3 ); }, 100 ); } function output(msg) { console.log( msg ); } ``` Now, let's demonstrate these `gate(..)` step variations: ```js ASQ().race( failure3, success1 ) .or( output ); // 3 ASQ().any( success1, failure3, success2 ) .val( function(){ var args = [].slice.call( arguments ); console.log( args // [ 1, undefined, 2 ] ); } ); ASQ().first( failure3, success1, success2 ) .val( output ); // 1 ASQ().last( failure3, success1, success2 ) .val( output ); // 2 ASQ().none( failure3 ) .val( output ) // 3 .none( failure3 success1 ) .or( output ); // 1 ``` Another step variation is `map(..)`, which lets you asynchronously map elements of an array to different values, and the step doesn't proceed until all the mappings are complete. `map(..)` is very similar to `gate(..)`, except it gets the initial values from an array instead of from separately specified functions, and also because you define a single function callback to operate on each value: ```js function double(x,done) { setTimeout( function(){ done( x * 2 ); }, 100 ); } ASQ().map( [1,2,3], double ) .val( output ); // [2,4,6] ``` Also, `map(..)` can receive either of its parameters (the array or the callback) from messages passed from the previous step: ```js function plusOne(x,done) { setTimeout( function(){ done( x + 1 ); }, 100 ); } ASQ( [1,2,3] ) .map( double ) // message `[1,2,3]` comes in .map( plusOne ) // message `[2,4,6]` comes in .val( output ); // [3,5,7] ``` Another variation is `waterfall(..)`, which is kind of like a mixture between `gate(..)`'s message collection behavior but `then(..)`'s sequential processing. Step 1 is first executed, then the success message from step 1 is given to step 2, and then both success messages go to step 3, and then all three success messages go to step 4, and so on, such that the messages sort of collect and cascade down the "waterfall". Consider: ```js function double(done) { var args = [].slice.call( arguments, 1 ); console.log( args ); setTimeout( function(){ done( args[args.length - 1] * 2 ); }, 100 ); } ASQ( 3 ) .waterfall( double, // [ 3 ] double, // [ 6 ] double, // [ 6, 12 ] double // [ 6, 12, 24 ] ) .val( function(){ var args = [].slice.call( arguments ); console.log( args ); // [ 6, 12, 24, 48 ] } ); ``` If at any point in the "waterfall" an error occurs, the whole sequence immediately goes into an error state. #### Error Tolerance Sometimes you want to manage errors at the step level and not let them necessarily send the whole sequence into the error state. *asynquence* offers two step variations for that purpose. `try(..)` attempts a step, and if it succeeds, the sequence proceeds as normal, but if the step fails, the failure is turned into a success message formated as `{ catch: .. }` with the error message(s) filled in: ```js ASQ() .try( success1 ) .val( output ) // 1 .try( failure3 ) .val( output ) // { catch: 3 } .or( function(err){ // never gets here } ); ``` You could instead set up a retry loop using `until(..)`, which tries the step and if it fails, retries the step again on the next event loop tick, and so on. This retry loop can continue indefinitely, but if you want to break out of the loop, you can call the `break()` flag on the completion trigger, which sends the main sequence into an error state: ```js var count = 0; ASQ( 3 ) .until( double ) .val( output ) // 6 .until( function(done){ count++; setTimeout( function(){ if (count < 5) { done.fail(); } else { // break out of the `until(..)` retry loop done.break( "Oops" ); } }, 100 ); } ) .or( output ); // Oops ``` #### Promise-Style Steps If you would prefer to have, inline in your sequence, Promise-style semantics like Promises' `then(..)` and `catch(..)` (see Chapter 3), you can use the `pThen` and `pCatch` plug-ins: ```js ASQ( 21 ) .pThen( function(msg){ return msg * 2; } ) .pThen( output ) // 42 .pThen( function(){ // throw an exception doesnt.Exist(); } ) .pCatch( function(err){ // caught the exception (rejection) console.log( err ); // ReferenceError } ) .val( function(){ // main sequence is back in a // success state because previous // exception was caught by // `pCatch(..)` } ); ``` `pThen(..)` and `pCatch(..)` are designed to run in the sequence, but behave as if it was a normal Promise chain. As such, you can either resolve genuine Promises or *asynquence* sequences from the "fulfillment" handler passed to `pThen(..)` (see Chapter 3). ### Forking Sequences One feature that can be quite useful about Promises is that you can attach multiple `then(..)` handler registrations to the same promise, effectively "forking" the flow-control at that promise: ```js var p = Promise.resolve( 21 ); // fork 1 (from `p`) p.then( function(msg){ return msg * 2; } ) .then( function(msg){ console.log( msg ); // 42 } ) // fork 2 (from `p`) p.then( function(msg){ console.log( msg ); // 21 } ); ``` The same "forking" is easy in *asynquence* with `fork()`: ```js var sq = ASQ(..).then(..).then(..); var sq2 = sq.fork(); // fork 1 sq.then(..)..; // fork 2 sq2.then(..)..; ``` ### Combining Sequences The reverse of `fork()`ing, you can combine two sequences by subsuming one into another, using the `seq(..)` instance method: ```js var sq = ASQ( function(done){ setTimeout( function(){ done( "Hello World" ); }, 200 ); } ); ASQ( function(done){ setTimeout( done, 100 ); } ) // subsume `sq` sequence into this sequence .seq( sq ) .val( function(msg){ console.log( msg ); // Hello World } ) ``` `seq(..)` can either accept a sequence itself, as shown here, or a function. If a function, it's expected that the function when called will return a sequence, so the preceding code could have been done with: ```js // .. .seq( function(){ return sq; } ) // .. ``` Also, that step could instead have been accomplished with a `pipe(..)`: ```js // .. .then( function(done){ // pipe `sq` into the `done` continuation callback sq.pipe( done ); } ) // .. ``` When a sequence is subsumed, both its success message stream and its error stream are piped in. **Note:** As mentioned in an earlier note, piping (manually with `pipe(..)` or automatically with `seq(..)`) opts the source sequence out of error-reporting, but doesn't affect the error reporting status of the target sequence. ## Value and Error Sequences If any step of a sequence is just a normal value, that value is just mapped to that step's completion message: ```js var sq = ASQ( 42 ); sq.val( function(msg){ console.log( msg ); // 42 } ); ``` If you want to make a sequence that's automatically errored: ```js var sq = ASQ.failed( "Oops" ); ASQ() .seq( sq ) .val( function(msg){ // won't get here } ) .or( function(err){ console.log( err ); // Oops } ); ``` You also may want to automatically create a delayed-value or a delayed-error sequence. Using the `after` and `failAfter` contrib plug-ins, this is easy: ```js var sq1 = ASQ.after( 100, "Hello", "World" ); var sq2 = ASQ.failAfter( 100, "Oops" ); sq1.val( function(msg1,msg2){ console.log( msg1, msg2 ); // Hello World } ); sq2.or( function(err){ console.log( err ); // Oops } ); ``` You can also insert a delay in the middle of a sequence using `after(..)`: ```js ASQ( 42 ) // insert a delay into the sequence .after( 100 ) .val( function(msg){ console.log( msg ); // 42 } ); ``` ## Promises and Callbacks I think *asynquence* sequences provide a lot of value on top of native Promises, and for the most part you'll find it more pleasant and more powerful to work at that level of abstraction. However, integrating *asynquence* with other non-*asynquence* code will be a reality. You can easily subsume a promise (e.g., thenable -- see Chapter 3) into a sequence using the `promise(..)` instance method: ```js var p = Promise.resolve( 42 ); ASQ() .promise( p ) // could also: `function(){ return p; }` .val( function(msg){ console.log( msg ); // 42 } ); ``` And to go the opposite direction and fork/vend a promise from a sequence at a certain step, use the `toPromise` contrib plug-in: ```js var sq = ASQ.after( 100, "Hello World" ); sq.toPromise() // this is a standard promise chain now .then( function(msg){ return msg.toUpperCase(); } ) .then( function(msg){ console.log( msg ); // HELLO WORLD } ); ``` To adapt *asynquence* to systems using callbacks, there are several helper facilities. To automatically generate an "error-first style" callback from your sequence to wire into a callback-oriented utility, use `errfcb`: ```js var sq = ASQ( function(done){ // note: expecting "error-first style" callback someAsyncFuncWithCB( 1, 2, done.errfcb ) } ) .val( function(msg){ // .. } ) .or( function(err){ // .. } ); // note: expecting "error-first style" callback anotherAsyncFuncWithCB( 1, 2, sq.errfcb() ); ``` You also may want to create a sequence-wrapped version of a utility -- compare to "promisory" in Chapter 3 and "thunkory" in Chapter 4 -- and *asynquence* provides `ASQ.wrap(..)` for that purpose: ```js var coolUtility = ASQ.wrap( someAsyncFuncWithCB ); coolUtility( 1, 2 ) .val( function(msg){ // .. } ) .or( function(err){ // .. } ); ``` **Note:** For the sake of clarity (and for fun!), let's coin yet another term, for a sequence-producing function that comes from `ASQ.wrap(..)`, like `coolUtility` here. I propose "sequory" ("sequence" + "factory"). ## Iterable Sequences The normal paradigm for a sequence is that each step is responsible for completing itself, which is what advances the sequence. Promises work the same way. The unfortunate part is that sometimes you need external control over a Promise/step, which leads to awkward "capability extraction". Consider this Promises example: ```js var domready = new Promise( function(resolve,reject){ // don't want to put this here, because // it belongs logically in another part // of the code document.addEventListener( "DOMContentLoaded", resolve ); } ); // .. domready.then( function(){ // DOM is ready! } ); ``` The "capability extraction" anti-pattern with Promises looks like this: ```js var ready; var domready = new Promise( function(resolve,reject){ // extract the `resolve()` capability ready = resolve; } ); // .. domready.then( function(){ // DOM is ready! } ); // .. document.addEventListener( "DOMContentLoaded", ready ); ``` **Note:** This anti-pattern is an awkward code smell, in my opinion, but some developers like it, for reasons I can't grasp. *asynquence* offers an inverted sequence type I call "iterable sequences", which externalizes the control capability (it's quite useful in use cases like the `domready`): ```js // note: `domready` here is an *iterator* that // controls the sequence var domready = ASQ.iterable(); // .. domready.val( function(){ // DOM is ready } ); // .. document.addEventListener( "DOMContentLoaded", domready.next ); ``` There's more to iterable sequences than what we see in this scenario. We'll come back to them in Appendix B. ## Running Generators In Chapter 4, we derived a utility called `run(..)` which can run generators to completion, listening for `yield`ed Promises and using them to async resume the generator. *asynquence* has just such a utility built in, called `runner(..)`. Let's first set up some helpers for illustration: ```js function doublePr(x) { return new Promise( function(resolve,reject){ setTimeout( function(){ resolve( x * 2 ); }, 100 ); } ); } function doubleSeq(x) { return ASQ( function(done){ setTimeout( function(){ done( x * 2) }, 100 ); } ); } ``` Now, we can use `runner(..)` as a step in the middle of a sequence: ```js ASQ( 10, 11 ) .runner( function*(token){ var x = token.messages[0] + token.messages[1]; // yield a real promise x = yield doublePr( x ); // yield a sequence x = yield doubleSeq( x ); return x; } ) .val( function(msg){ console.log( msg ); // 84 } ); ``` ### Wrapped Generators You can also create a self-packaged generator -- that is, a normal function that runs your specified generator and returns a sequence for its completion -- by `ASQ.wrap(..)`ing it: ```js var foo = ASQ.wrap( function*(token){ var x = token.messages[0] + token.messages[1]; // yield a real promise x = yield doublePr( x ); // yield a sequence x = yield doubleSeq( x ); return x; }, { gen: true } ); // .. foo( 8, 9 ) .val( function(msg){ console.log( msg ); // 68 } ); ``` There's a lot more awesome that `runner(..)` is capable of, but we'll come back to that in Appendix B. ## Review *asynquence* is a simple abstraction -- a sequence is a series of (async) steps -- on top of Promises, aimed at making working with various asynchronous patterns much easier, without any compromise in capability. There are other goodies in the *asynquence* core API and its contrib plug-ins beyond what we saw in this appendix, but we'll leave that as an exercise for the reader to go check the rest of the capabilities out. You've now seen the essence and spirit of *asynquence*. The key take away is that a sequence is comprised of steps, and those steps can be any of dozens of different variations on Promises, or they can be a generator-run, or... The choice is up to you, you have all the freedom to weave together whatever async flow control logic is appropriate for your tasks. No more library switching to catch different async patterns. If these *asynquence* snippets have made sense to you, you're now pretty well up to speed on the library; it doesn't take that much to learn, actually! If you're still a little fuzzy on how it works (or why!), you'll want to spend a little more time examining the previous examples and playing around with *asynquence* yourself, before going on to the next appendix. Appendix B will push *asynquence* into several more advanced and powerful async patterns. ================================================ FILE: async & performance/apB.md ================================================ # You Don't Know JS: Async & Performance # Appendix B: Advanced Async Patterns Appendix A introduced the *asynquence* library for sequence-oriented async flow control, primarily based on Promises and generators. Now we'll explore other advanced asynchronous patterns built on top of that existing understanding and functionality, and see how *asynquence* makes those sophisticated async techniques easy to mix and match in our programs without needing lots of separate libraries. ## Iterable Sequences We introduced *asynquence*'s iterable sequences in the previous appendix, but we want to revisit them in more detail. To refresh, recall: ```js var domready = ASQ.iterable(); // .. domready.val( function(){ // DOM is ready } ); // .. document.addEventListener( "DOMContentLoaded", domready.next ); ``` Now, let's define a sequence of multiple steps as an iterable sequence: ```js var steps = ASQ.iterable(); steps .then( function STEP1(x){ return x * 2; } ) .steps( function STEP2(x){ return x + 3; } ) .steps( function STEP3(x){ return x * 4; } ); steps.next( 8 ).value; // 16 steps.next( 16 ).value; // 19 steps.next( 19 ).value; // 76 steps.next().done; // true ``` As you can see, an iterable sequence is a standard-compliant *iterator* (see Chapter 4). So, it can be iterated with an ES6 `for..of` loop, just like a generator (or any other *iterable*) can: ```js var steps = ASQ.iterable(); steps .then( function STEP1(){ return 2; } ) .then( function STEP2(){ return 4; } ) .then( function STEP3(){ return 6; } ) .then( function STEP4(){ return 8; } ) .then( function STEP5(){ return 10; } ); for (var v of steps) { console.log( v ); } // 2 4 6 8 10 ``` Beyond the event triggering example shown in the previous appendix, iterable sequences are interesting because in essence they can be seen as a stand-in for generators or Promise chains, but with even more flexibility. Consider a multiple Ajax request example -- we've seen the same scenario in Chapters 3 and 4, both as a Promise chain and as a generator, respectively -- expressed as an iterable sequence: ```js // sequence-aware ajax var request = ASQ.wrap( ajax ); ASQ( "http://some.url.1" ) .runner( ASQ.iterable() .then( function STEP1(token){ var url = token.messages[0]; return request( url ); } ) .then( function STEP2(resp){ return ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); } ) .then( function STEP3(r1,r2){ return r1 + r2; } ) ) .val( function(msg){ console.log( msg ); } ); ``` The iterable sequence expresses a sequential series of (sync or async) steps that looks awfully similar to a Promise chain -- in other words, it's much cleaner looking than just plain nested callbacks, but not quite as nice as the `yield`-based sequential syntax of generators. But we pass the iterable sequence into `ASQ#runner(..)`, which runs it to completion the same as if it was a generator. The fact that an iterable sequence behaves essentially the same as a generator is notable for a couple of reasons. First, iterable sequences are kind of a pre-ES6 equivalent to a certain subset of ES6 generators, which means you can either author them directly (to run anywhere), or you can author ES6 generators and transpile/convert them to iterable sequences (or Promise chains for that matter!). Thinking of an async-run-to-completion generator as just syntactic sugar for a Promise chain is an important recognition of their isomorphic relationship. Before we move on, we should note that the previous snippet could have been expressed in *asynquence* as: ```js ASQ( "http://some.url.1" ) .seq( /*STEP 1*/ request ) .seq( function STEP2(resp){ return ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); } ) .val( function STEP3(r1,r2){ return r1 + r2; } ) .val( function(msg){ console.log( msg ); } ); ``` Moreover, step 2 could have even been expressed as: ```js .gate( function STEP2a(done,resp) { request( "http://some.url.2/?v=" + resp ) .pipe( done ); }, function STEP2b(done,resp) { request( "http://some.url.3/?v=" + resp ) .pipe( done ); } ) ``` So, why would we go to the trouble of expressing our flow control as an iterable sequence in a `ASQ#runner(..)` step, when it seems like a simpler/flatter *asyquence* chain does the job well? Because the iterable sequence form has an important trick up its sleeve that gives us more capability. Read on. ### Extending Iterable Sequences Generators, normal *asynquence* sequences, and Promise chains, are all **eagerly evaluated** -- whatever flow control is expressed initially *is* the fixed flow that will be followed. However, iterable sequences are **lazily evaluated**, which means that during execution of the iterable sequence, you can extend the sequence with more steps if desired. **Note:** You can only append to the end of an iterable sequence, not inject into the middle of the sequence. Let's first look at a simpler (synchronous) example of that capability to get familiar with it: ```js function double(x) { x *= 2; // should we keep extending? if (x < 500) { isq.then( double ); } return x; } // setup single-step iterable sequence var isq = ASQ.iterable().then( double ); for (var v = 10, ret; (ret = isq.next( v )) && !ret.done; ) { v = ret.value; console.log( v ); } ``` The iterable sequence starts out with only one defined step (`isq.then(double)`), but the sequence keeps extending itself under certain conditions (`x < 500`). Both *asynquence* sequences and Promise chains technically *can* do something similar, but we'll see in a little bit why their capability is insufficient. Though this example is rather trivial and could otherwise be expressed with a `while` loop in a generator, we'll consider more sophisticated cases. For instance, you could examine the response from an Ajax request and if it indicates that more data is needed, you conditionally insert more steps into the iterable sequence to make the additional request(s). Or you could conditionally add a value-formatting step to the end of your Ajax handling. Consider: ```js var steps = ASQ.iterable() .then( function STEP1(token){ var url = token.messages[0].url; // was an additional formatting step provided? if (token.messages[0].format) { steps.then( token.messages[0].format ); } return request( url ); } ) .then( function STEP2(resp){ // add another Ajax request to the sequence? if (/x1/.test( resp )) { steps.then( function STEP5(text){ return request( "http://some.url.4/?v=" + text ); } ); } return ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); } ) .then( function STEP3(r1,r2){ return r1 + r2; } ); ``` You can see in two different places where we conditionally extend `steps` with `steps.then(..)`. And to run this `steps` iterable sequence, we just wire it into our main program flow with an *asynquence* sequence (called `main` here) using `ASQ#runner(..)`: ```js var main = ASQ( { url: "http://some.url.1", format: function STEP4(text){ return text.toUpperCase(); } } ) .runner( steps ) .val( function(msg){ console.log( msg ); } ); ``` Can the flexibility (conditional behavior) of the `steps` iterable sequence be expressed with a generator? Kind of, but we have to rearrange the logic in a slightly awkward way: ```js function *steps(token) { // **STEP 1** var resp = yield request( token.messages[0].url ); // **STEP 2** var rvals = yield ASQ().gate( request( "http://some.url.2/?v=" + resp ), request( "http://some.url.3/?v=" + resp ) ); // **STEP 3** var text = rvals[0] + rvals[1]; // **STEP 4** // was an additional formatting step provided? if (token.messages[0].format) { text = yield token.messages[0].format( text ); } // **STEP 5** // need another Ajax request added to the sequence? if (/foobar/.test( resp )) { text = yield request( "http://some.url.4/?v=" + text ); } return text; } // note: `*steps()` can be run by the same `ASQ` sequence // as `steps` was previously ``` Setting aside the already identified benefits of the sequential, synchronous-looking syntax of generators (see Chapter 4), the `steps` logic had to be reordered in the `*steps()` generator form, to fake the dynamicism of the extendable iterable sequence `steps`. What about expressing the functionality with Promises or sequences, though? You *can* do something like this: ```js var steps = something( .. ) .then( .. ) .then( function(..){ // .. // extending the chain, right? steps = steps.then( .. ); // .. }) .then( .. ); ``` The problem is subtle but important to grasp. So, consider trying to wire up our `steps` Promise chain into our main program flow -- this time expressed with Promises instead of *asynquence*: ```js var main = Promise.resolve( { url: "http://some.url.1", format: function STEP4(text){ return text.toUpperCase(); } } ) .then( function(..){ return steps; // hint! } ) .val( function(msg){ console.log( msg ); } ); ``` Can you spot the problem now? Look closely! There's a race condition for sequence steps ordering. When you `return steps`, at that moment `steps` *might* be the originally defined promise chain, or it might now point to the extended promise chain via the `steps = steps.then(..)` call, depending on what order things happen. Here are the two possible outcomes: * If `steps` is still the original promise chain, once it's later "extended" by `steps = steps.then(..)`, that extended promise on the end of the chain is **not** considered by the `main` flow, as it's already tapped the `steps` chain. This is the unfortunately limiting **eager evaluation**. * If `steps` is already the extended promise chain, it works as we expect in that the extended promise is what `main` taps. Other than the obvious fact that a race condition is intolerable, the first case is the concern; it illustrates **eager evaluation** of the promise chain. By contrast, we easily extended the iterable sequence without such issues, because iterable sequences are **lazily evaluated**. The more dynamic you need your flow control, the more iterable sequences will shine. **Tip:** Check out more information and examples of iterable sequences on the *asynquence* site (https://github.com/getify/asynquence/blob/master/README.md#iterable-sequences). ## Event Reactive It should be obvious from (at least!) Chapter 3 that Promises are a very powerful tool in your async toolbox. But one thing that's clearly lacking is in their capability to handle streams of events, as a Promise can only be resolved once. And frankly, this exact same weakness is true of plain *asynquence* sequences, as well. Consider a scenario where you want to fire off a series of steps every time a certain event is fired. A single Promise or sequence cannot represent all occurrences of that event. So, you have to create a whole new Promise chain (or sequence) for *each* event occurrence, such as: ```js listener.on( "foobar", function(data){ // create a new event handling promise chain new Promise( function(resolve,reject){ // .. } ) .then( .. ) .then( .. ); } ); ``` The base functionality we need is present in this approach, but it's far from a desirable way to express our intended logic. There are two separate capabilities conflated in this paradigm: the event listening, and responding to the event; separation of concerns would implore us to separate out these capabilities. The carefully observant reader will see this problem as somewhat symmetrical to the problems we detailed with callbacks in Chapter 2; it's kind of an inversion of control problem. Imagine uninverting this paradigm, like so: ```js var observable = listener.on( "foobar" ); // later observable .then( .. ) .then( .. ); // elsewhere observable .then( .. ) .then( .. ); ``` The `observable` value is not exactly a Promise, but you can *observe* it much like you can observe a Promise, so it's closely related. In fact, it can be observed many times, and it will send out notifications every time its event (`"foobar"`) occurs. **Tip:** This pattern I've just illustrated is a **massive simplification** of the concepts and motivations behind reactive programming (aka RP), which has been implemented/expounded upon by several great projects and languages. A variation on RP is functional reactive programming (FRP), which refers to applying functional programming techniques (immutability, referential integrity, etc.) to streams of data. "Reactive" refers to spreading this functionality out over time in response to events. The interested reader should consider studying "Reactive Observables" in the fantastic "Reactive Extensions" library ("RxJS" for JavaScript) by Microsoft (http://rxjs.codeplex.com/); it's much more sophisticated and powerful than I've just shown. Also, Andre Staltz has an excellent write-up (https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) that pragmatically lays out RP in concrete examples. ### ES7 Observables At the time of this writing, there's an early ES7 proposal for a new data type called "Observable" (https://github.com/jhusain/asyncgenerator#introducing-observable), which in spirit is similar to what we've laid out here, but is definitely more sophisticated. The notion of this kind of Observable is that the way you "subscribe" to the events from a stream is to pass in a generator -- actually the *iterator* is the interested party -- whose `next(..)` method will be called for each event. You could imagine it sort of like this: ```js // `someEventStream` is a stream of events, like from // mouse clicks, and the like. var observer = new Observer( someEventStream, function*(){ while (var evt = yield) { console.log( evt ); } } ); ``` The generator you pass in will `yield` pause the `while` loop waiting for the next event. The *iterator* attached to the generator instance will have its `next(..)` called each time `someEventStream` has a new event published, and so that event data will resume your generator/*iterator* with the `evt` data. In the subscription to events functionality here, it's the *iterator* part that matters, not the generator. So conceptually you could pass in practically any iterable, including `ASQ.iterable()` iterable sequences. Interestingly, there are also proposed adapters to make it easy to construct Observables from certain types of streams, such as `fromEvent(..)` for DOM events. If you look at a suggested implementation of `fromEvent(..)` in the earlier linked ES7 proposal, it looks an awful lot like the `ASQ.react(..)` we'll see in the next section. Of course, these are all early proposals, so what shakes out may very well look/behave differently than shown here. But it's exciting to see the early alignments of concepts across different libraries and language proposals! ### Reactive Sequences With that crazy brief summary of Observables (and F/RP) as our inspiration and motivation, I will now illustrate an adaptation of a small subset of "Reactive Observables," which I call "Reactive Sequences." First, let's start with how to create an Observable, using an *asynquence* plug-in utility called `react(..)`: ```js var observable = ASQ.react( function setup(next){ listener.on( "foobar", next ); } ); ``` Now, let's see how to define a sequence that "reacts" -- in F/RP, this is typically called "subscribing" -- to that `observable`: ```js observable .seq( .. ) .then( .. ) .val( .. ); ``` So, you just define the sequence by chaining off the Observable. That's easy, huh? In F/RP, the stream of events typically channels through a set of functional transforms, like `scan(..)`, `map(..)`, `reduce(..)`, and so on. With reactive sequences, each event channels through a new instance of the sequence. Let's look at a more concrete example: ```js ASQ.react( function setup(next){ document.getElementById( "mybtn" ) .addEventListener( "click", next, false ); } ) .seq( function(evt){ var btnID = evt.target.id; return request( "http://some.url.1/?id=" + btnID ); } ) .val( function(text){ console.log( text ); } ); ``` The "reactive" portion of the reactive sequence comes from assigning one or more event handlers to invoke the event trigger (calling `next(..)`). The "sequence" portion of the reactive sequence is exactly like the sequences we've already explored: each step can be whatever asynchronous technique makes sense, from continuation callback to Promise to generator. Once you set up a reactive sequence, it will continue to initiate instances of the sequence as long as the events keep firing. If you want to stop a reactive sequence, you can call `stop()`. If a reactive sequence is `stop()`'d, you likely want the event handler(s) to be unregistered as well; you can register a teardown handler for this purpose: ```js var sq = ASQ.react( function setup(next,registerTeardown){ var btn = document.getElementById( "mybtn" ); btn.addEventListener( "click", next, false ); // will be called once `sq.stop()` is called registerTeardown( function(){ btn.removeEventListener( "click", next, false ); } ); } ) .seq( .. ) .then( .. ) .val( .. ); // later sq.stop(); ``` **Note:** The `this` binding reference inside the `setup(..)` handler is the same `sq` reactive sequence, so you can use the `this` reference to add to the reactive sequence definition, call methods like `stop()`, and so on. Here's an example from the Node.js world, using reactive sequences to handle incoming HTTP requests: ```js var server = http.createServer(); server.listen(8000); // reactive observer var request = ASQ.react( function setup(next,registerTeardown){ server.addListener( "request", next ); server.addListener( "close", this.stop ); registerTeardown( function(){ server.removeListener( "request", next ); server.removeListener( "close", request.stop ); } ); }); // respond to requests request .seq( pullFromDatabase ) .val( function(data,res){ res.end( data ); } ); // node teardown process.on( "SIGINT", request.stop ); ``` The `next(..)` trigger can also adapt to node streams easily, using `onStream(..)` and `unStream(..)`: ```js ASQ.react( function setup(next){ var fstream = fs.createReadStream( "/some/file" ); // pipe the stream's "data" event to `next(..)` next.onStream( fstream ); // listen for the end of the stream fstream.on( "end", function(){ next.unStream( fstream ); } ); } ) .seq( .. ) .then( .. ) .val( .. ); ``` You can also use sequence combinations to compose multiple reactive sequence streams: ```js var sq1 = ASQ.react( .. ).seq( .. ).then( .. ); var sq2 = ASQ.react( .. ).seq( .. ).then( .. ); var sq3 = ASQ.react(..) .gate( sq1, sq2 ) .then( .. ); ``` The main takeaway is that `ASQ.react(..)` is a lightweight adaptation of F/RP concepts, enabling the wiring of an event stream to a sequence, hence the term "reactive sequence." Reactive sequences are generally capable enough for basic reactive uses. **Note:** Here's an example of using `ASQ.react(..)` in managing UI state (http://jsbin.com/rozipaki/6/edit?js,output), and another example of handling HTTP request/response streams with `ASQ.react(..)` (https://gist.github.com/getify/bba5ec0de9d6047b720e). ## Generator Coroutine Hopefully Chapter 4 helped you get pretty familiar with ES6 generators. In particular, we want to revisit the "Generator Concurrency" discussion, and push it even further. We imagined a `runAll(..)` utility that could take two or more generators and run them concurrently, letting them cooperatively `yield` control from one to the next, with optional message passing. In addition to being able to run a single generator to completion, the `ASQ#runner(..)` we discussed in Appendix A is a similar implementation of the concepts of `runAll(..)`, which can run multiple generators concurrently to completion. So let's see how we can implement the concurrent Ajax scenario from Chapter 4: ```js ASQ( "http://some.url.2" ) .runner( function*(token){ // transfer control yield token; var url1 = token.messages[0]; // "http://some.url.1" // clear out messages to start fresh token.messages = []; var p1 = request( url1 ); // transfer control yield token; token.messages.push( yield p1 ); }, function*(token){ var url2 = token.messages[0]; // "http://some.url.2" // message pass and transfer control token.messages[0] = "http://some.url.1"; yield token; var p2 = request( url2 ); // transfer control yield token; token.messages.push( yield p2 ); // pass along results to next sequence step return token.messages; } ) .val( function(res){ // `res[0]` comes from "http://some.url.1" // `res[1]` comes from "http://some.url.2" } ); ``` The main differences between `ASQ#runner(..)` and `runAll(..)` are as follows: * Each generator (coroutine) is provided an argument we call `token`, which is the special value to `yield` when you want to explicitly transfer control to the next coroutine. * `token.messages` is an array that holds any messages passed in from the previous sequence step. It's also a data structure that you can use to share messages between coroutines. * `yield`ing a Promise (or sequence) value does not transfer control, but instead pauses the coroutine processing until that value is ready. * The last `return`ed or `yield`ed value from the coroutine processing run will be forward passed to the next step in the sequence. It's also easy to layer helpers on top of the base `ASQ#runner(..)` functionality to suit different uses. ### State Machines One example that may be familiar to many programmers is state machines. You can, with the help of a simple cosmetic utility, create an easy-to-express state machine processor. Let's imagine such a utility. We'll call it `state(..)`, and will pass it two arguments: a state value and a generator that handles that state. `state(..)` will do the dirty work of creating and returning an adapter generator to pass to `ASQ#runner(..)`. Consider: ```js function state(val,handler) { // make a coroutine handler for this state return function*(token) { // state transition handler function transition(to) { token.messages[0] = to; } // set initial state (if none set yet) if (token.messages.length < 1) { token.messages[0] = val; } // keep going until final state (false) is reached while (token.messages[0] !== false) { // current state matches this handler? if (token.messages[0] === val) { // delegate to state handler yield *handler( transition ); } // transfer control to another state handler? if (token.messages[0] !== false) { yield token; } } }; } ``` If you look closely, you'll see that `state(..)` returns back a generator that accepts a `token`, and then it sets up a `while` loop that will run until the state machine reaches its final state (which we arbitrarily pick as the `false` value); that's exactly the kind of generator we want to pass to `ASQ#runner(..)`! We also arbitrarily reserve the `token.messages[0]` slot as the place where the current state of our state machine will be tracked, which means we can even seed the initial state as the value passed in from the previous step in the sequence. How do we use the `state(..)` helper along with `ASQ#runner(..)`? ```js var prevState; ASQ( /* optional: initial state value */ 2 ) // run our state machine // transitions: 2 -> 3 -> 1 -> 3 -> false .runner( // state `1` handler state( 1, function *stateOne(transition){ console.log( "in state 1" ); prevState = 1; yield transition( 3 ); // goto state `3` } ), // state `2` handler state( 2, function *stateTwo(transition){ console.log( "in state 2" ); prevState = 2; yield transition( 3 ); // goto state `3` } ), // state `3` handler state( 3, function *stateThree(transition){ console.log( "in state 3" ); if (prevState === 2) { prevState = 3; yield transition( 1 ); // goto state `1` } // all done! else { yield "That's all folks!"; prevState = 3; yield transition( false ); // terminal state } } ) ) // state machine complete, so move on .val( function(msg){ console.log( msg ); // That's all folks! } ); ``` It's important to note that the `*stateOne(..)`, `*stateTwo(..)`, and `*stateThree(..)` generators themselves are reinvoked each time that state is entered, and they finish when you `transition(..)` to another value. While not shown here, of course these state generator handlers can be asynchronously paused by `yield`ing Promises/sequences/thunks. The underneath hidden generators produced by the `state(..)` helper and actually passed to `ASQ#runner(..)` are the ones that continue to run concurrently for the length of the state machine, and each of them handles cooperatively `yield`ing control to the next, and so on. **Note:** See this "ping pong" example (http://jsbin.com/qutabu/1/edit?js,output) for more illustration of using cooperative concurrency with generators driven by `ASQ#runner(..)`. ## Communicating Sequential Processes (CSP) "Communicating Sequential Processes" (CSP) was first described by C. A. R. Hoare in a 1978 academic paper (http://dl.acm.org/citation.cfm?doid=359576.359585), and later in a 1985 book (http://www.usingcsp.com/) of the same name. CSP describes a formal method for concurrent "processes" to interact (aka "communicate") during processing. You may recall that we examined concurrent "processes" back in Chapter 1, so our exploration of CSP here will build upon that understanding. Like most great concepts in computer science, CSP is heavily steeped in academic formalism, expressed as a process algebra. However, I suspect symbolic algebra theorems won't make much practical difference to the reader, so we will want to find some other way of wrapping our brains around CSP. I will leave much of the formal description and proof of CSP to Hoare's writing, and to many other fantastic writings since. Instead, we will try to just briefly explain the idea of CSP in as un-academic and hopefully intuitively understandable a way as possible. ### Message Passing The core principle in CSP is that all communication/interaction between otherwise independent processes must be through formal message passing. Perhaps counter to your expectations, CSP message passing is described as a synchronous action, where the sender process and the receiver process have to mutually be ready for the message to be passed. How could such synchronous messaging possibly be related to asynchronous programming in JavaScript? The concreteness of relationship comes from the nature of how ES6 generators are used to produce synchronous-looking actions that under the covers can indeed either be synchronous or (more likely) asynchronous. In other words, two or more concurrently running generators can appear to synchronously message each other while preserving the fundamental asynchrony of the system because each generator's code is paused (aka "blocked") waiting on resumption of an asynchronous action. How does this work? Imagine a generator (aka "process") called "A" that wants to send a message to generator "B." First, "A" `yield`s the message (thus pausing "A") to be sent to "B." When "B" is ready and takes the message, "A" is then resumed (unblocked). Symmetrically, imagine a generator "A" that wants a message **from** "B." "A" `yield`s its request (thus pausing "A") for the message from "B," and once "B" sends a message, "A" takes the message and is resumed. One of the more popular expressions of this CSP message passing theory comes from ClojureScript's core.async library, and also from the *go* language. These takes on CSP embody the described communication semantics in a conduit that is opened between processes called a "channel." **Note:** The term *channel* is used in part because there are modes in which more than one value can be sent at once into the "buffer" of the channel; this is similar to what you may think of as a stream. We won't go into depth about it here, but it can be a very powerful technique for managing streams of data. In the simplest notion of CSP, a channel that we create between "A" and "B" would have a method called `take(..)` for blocking to receive a value, and a method called `put(..)` for blocking to send a value. This might look like: ```js var ch = channel(); function *foo() { var msg = yield take( ch ); console.log( msg ); } function *bar() { yield put( ch, "Hello World" ); console.log( "message sent" ); } run( foo ); run( bar ); // Hello World // "message sent" ``` Compare this structured, synchronous(-looking) message passing interaction to the informal and unstructured message sharing that `ASQ#runner(..)` provides through the `token.messages` array and cooperative `yield`ing. In essence, `yield put(..)` is a single operation that both sends the value and pauses execution to transfer control, whereas in earlier examples we did those as separate steps. Moreover, CSP stresses that you don't really explicitly "transfer control," but rather you design your concurrent routines to block expecting either a value received from the channel, or to block expecting to try to send a message on the channel. The blocking around receiving or sending messages is how you coordinate sequencing of behavior between the coroutines. **Note:** Fair warning: this pattern is very powerful but it's also a little mind twisting to get used to at first. You will want to practice this a bit to get used to this new way of thinking about coordinating your concurrency. There are several great libraries that have implemented this flavor of CSP in JavaScript, most notably "js-csp" (https://github.com/ubolonton/js-csp), which James Long (http://twitter.com/jlongster) forked (https://github.com/jlongster/js-csp) and has written extensively about (http://jlongster.com/Taming-the-Asynchronous-Beast-with-CSP-in-JavaScript). Also, it cannot be stressed enough how amazing the many writings of David Nolen (http://twitter.com/swannodette) are on the topic of adapting ClojureScript's go-style core.async CSP into JS generators (http://swannodette.github.io/2013/08/24/es6-generators-and-csp/). ### asynquence CSP emulation Because we've been discussing async patterns here in the context of my *asynquence* library, you might be interested to see that we can fairly easily add an emulation layer on top of `ASQ#runner(..)` generator handling as a nearly perfect porting of the CSP API and behavior. This emulation layer ships as an optional part of the "asynquence-contrib" package alongside *asynquence*. Very similar to the `state(..)` helper from earlier, `ASQ.csp.go(..)` takes a generator -- in go/core.async terms, it's known as a goroutine -- and adapts it to use with `ASQ#runner(..)` by returning a new generator. Instead of being passed a `token`, your goroutine receives an initially created channel (`ch` below) that all goroutines in this run will share. You can create more channels (which is often quite helpful!) with `ASQ.csp.chan(..)`. In CSP, we model all asynchrony in terms of blocking on channel messages, rather than blocking waiting for a Promise/sequence/thunk to complete. So, instead of `yield`ing the Promise returned from `request(..)`, `request(..)` should return a channel that you `take(..)` a value from. In other words, a single-value channel is roughly equivalent in this context/usage to a Promise/sequence. Let's first make a channel-aware version of `request(..)`: ```js function request(url) { var ch = ASQ.csp.channel(); ajax( url ).then( function(content){ // `putAsync(..)` is a version of `put(..)` that // can be used outside of a generator. It returns // a promise for the operation's completion. We // don't use that promise here, but we could if // we needed to be notified when the value had // been `take(..)`n. ASQ.csp.putAsync( ch, content ); } ); return ch; } ``` From Chapter 3, "promisory" is a Promise-producing utility, "thunkory" from Chapter 4 is a thunk-producing utility, and finally, in Appendix A we invented "sequory" for a sequence-producing utility. Naturally, we need to coin a symmetric term here for a channel-producing utility. So let's unsurprisingly call it a "chanory" ("channel" + "factory"). As an exercise for the reader, try your hand at defining a `channelify(..)` utility similar to `Promise.wrap(..)`/`promisify(..)` (Chapter 3), `thunkify(..)` (Chapter 4), and `ASQ.wrap(..)` (Appendix A). Now consider the concurrent Ajax example using *asyquence*-flavored CSP: ```js ASQ() .runner( ASQ.csp.go( function*(ch){ yield ASQ.csp.put( ch, "http://some.url.2" ); var url1 = yield ASQ.csp.take( ch ); // "http://some.url.1" var res1 = yield ASQ.csp.take( request( url1 ) ); yield ASQ.csp.put( ch, res1 ); } ), ASQ.csp.go( function*(ch){ var url2 = yield ASQ.csp.take( ch ); // "http://some.url.2" yield ASQ.csp.put( ch, "http://some.url.1" ); var res2 = yield ASQ.csp.take( request( url2 ) ); var res1 = yield ASQ.csp.take( ch ); // pass along results to next sequence step ch.buffer_size = 2; ASQ.csp.put( ch, res1 ); ASQ.csp.put( ch, res2 ); } ) ) .val( function(res1,res2){ // `res1` comes from "http://some.url.1" // `res2` comes from "http://some.url.2" } ); ``` The message passing that trades the URL strings between the two goroutines is pretty straightforward. The first goroutine makes an Ajax request to the first URL, and that response is put onto the `ch` channel. The second goroutine makes an Ajax request to the second URL, then gets the first response `res1` off the `ch` channel. At that point, both responses `res1` and `res2` are completed and ready. If there are any remaining values in the `ch` channel at the end of the goroutine run, they will be passed along to the next step in the sequence. So, to pass out message(s) from the final goroutine, `put(..)` them into `ch`. As shown, to avoid the blocking of those final `put(..)`s, we switch `ch` into buffering mode by setting its `buffer_size` to `2` (default: `0`). **Note:** See many more examples of using *asynquence*-flavored CSP here (https://gist.github.com/getify/e0d04f1f5aa24b1947ae). ## Review Promises and generators provide the foundational building blocks upon which we can build much more sophisticated and capable asynchrony. *asynquence* has utilities for implementing *iterable sequences*, *reactive sequences* (aka "Observables"), *concurrent coroutines*, and even *CSP goroutines*. Those patterns, combined with the continuation-callback and Promise capabilities, gives *asynquence* a powerful mix of different asynchronous functionalities, all integrated in one clean async flow control abstraction: the sequence. ================================================ FILE: async & performance/apC.md ================================================ # Вы не знаете JS: Асинхронность и Производительность # Приложение C: Благодарности Есть множество людей, которых нужно поблагодарить за то, что появилась на свет эта книга и вся серия. Во-первых, я должен поблагодарить мою жену Кристен Симпсон (Christen Simpson) и двух моих детей Итана (Ethan) и Эмили (Emily), за то, что мирились с тем, что их папу вечно надо было отрывать от компьютера. Даже когда я не писал книги, моя одержимость JavaScript приклеивала мой взгляд к экрану больше, чем следовало. То время, которое я занял у моей семьи, и есть причина, по которой эти книги могут так глубоко и полностью объяснить JavaScript для вас, читатель. Я в большом долгу перед своей семьей. Хочу поблагодарить моих редакторов в O'Reilly, а именно Simon St.Laurent и Brian MacDonald, как и остальных в команде редакторов и маркетинга. Работать с ними — одно удовольствие, и хочется особенно поблагодарить их за создание подходящих условий во время этого эксперимента с написанием "open source"-книги, за редактирование и выпуск. Спасибо всем тем, кто участвовал в улучшении этой серии книг присылая предложения по редактированию и корректировке, включая Shelley Powers, Tim Ferro, Evan Borden, Forrest L. Norvell, Jennifer Davis, Jesse Harlin и многих других. Большое спасибо Shane Hudson за написание предисловия к этой серии книг. Спасибо бесчисленным участникам сообщества, включая членов комитета TC39, кто поделился столько многим с нами и особенно за терпеливое отношение к моим бесконечным вопросам и изысканиям. John-David Dalton, Juriy "kangax" Zaytsev, Mathias Bynens, Axel Rauschmayer, Nicholas Zakas, Angus Croll, Reginald Braithwaite, Dave Herman, Brendan Eich, Allen Wirfs-Brock, Bradley Meck, Domenic Denicola, David Walsh, Tim Disney, Peter van der Zee, Andrea Giammarchi, Kit Cambridge, Eric Elliott и многие другие, я упомянул лишь малую часть. Серия книг *Вы не знаете JS* родилась на Kickstarter, поэтому я также хочу поблагодарить всех моих (почти) 500 щедрых инвесторов, без которых эта серия книг не появилась бы: > Jan Szpila, nokiko, Murali Krishnamoorthy, Ryan Joy, Craig Patchett, pdqtrader, Dale Fukami, ray hatfield, R0drigo Perez [Mx], Dan Petitt, Jack Franklin, Andrew Berry, Brian Grinstead, Rob Sutherland, Sergi Meseguer, Phillip Gourley, Mark Watson, Jeff Carouth, Alfredo Sumaran, Martin Sachse, Marcio Barrios, Dan, AimelyneM, Matt Sullivan, Delnatte Pierre-Antoine, Jake Smith, Eugen Tudorancea, Iris, David Trinh, simonstl, Ray Daly, Uros Gruber, Justin Myers, Shai Zonis, Mom & Dad, Devin Clark, Dennis Palmer, Brian Panahi Johnson, Josh Marshall, Marshall, Dennis Kerr, Matt Steele, Erik Slagter, Sacah, Justin Rainbow, Christian Nilsson, Delapouite, D.Pereira, Nicolas Hoizey, George V. Reilly, Dan Reeves, Bruno Laturner, Chad Jennings, Shane King, Jeremiah Lee Cohick, od3n, Stan Yamane, Marko Vucinic, Jim B, Stephen Collins, Ægir Þorsteinsson, Eric Pederson, Owain, Nathan Smith, Jeanetteurphy, Alexandre ELISÉ, Chris Peterson, Rik Watson, Luke Matthews, Justin Lowery, Morten Nielsen, Vernon Kesner, Chetan Shenoy, Paul Tregoing, Marc Grabanski, Dion Almaer, Andrew Sullivan, Keith Elsass, Tom Burke, Brian Ashenfelter, David Stuart, Karl Swedberg, Graeme, Brandon Hays, John Christopher, Gior, manoj reddy, Chad Smith, Jared Harbour, Minoru TODA, Chris Wigley, Daniel Mee, Mike, Handyface, Alex Jahraus, Carl Furrow, Rob Foulkrod, Max Shishkin, Leigh Penny Jr., Robert Ferguson, Mike van Hoenselaar, Hasse Schougaard, rajan venkataguru, Jeff Adams, Trae Robbins, Rolf Langenhuijzen, Jorge Antunes, Alex Koloskov, Hugh Greenish, Tim Jones, Jose Ochoa, Michael Brennan-White, Naga Harish Muvva, Barkóczi Dávid, Kitt Hodsden, Paul McGraw, Sascha Goldhofer, Andrew Metcalf, Markus Krogh, Michael Mathews, Matt Jared, Juanfran, Georgie Kirschner, Kenny Lee, Ted Zhang, Amit Pahwa, Inbal Sinai, Dan Raine, Schabse Laks, Michael Tervoort, Alexandre Abreu, Alan Joseph Williams, NicolasD, Cindy Wong, Reg Braithwaite, LocalPCGuy, Jon Friskics, Chris Merriman, John Pena, Jacob Katz, Sue Lockwood, Magnus Johansson, Jeremy Crapsey, Grzegorz Pawłowski, nico nuzzaci, Christine Wilks, Hans Bergren, charles montgomery, Ariel בר-לבב Fogel, Ivan Kolev, Daniel Campos, Hugh Wood, Christian Bradford, Frédéric Harper, Ionuţ Dan Popa, Jeff Trimble, Rupert Wood, Trey Carrico, Pancho Lopez, Joël kuijten, Tom A Marra, Jeff Jewiss, Jacob Rios, Paolo Di Stefano, Soledad Penades, Chris Gerber, Andrey Dolganov, Wil Moore III, Thomas Martineau, Kareem, Ben Thouret, Udi Nir, Morgan Laupies, jory carson-burson, Nathan L Smith, Eric Damon Walters, Derry Lozano-Hoyland, Geoffrey Wiseman, mkeehner, KatieK, Scott MacFarlane, Brian LaShomb, Adrien Mas, christopher ross, Ian Littman, Dan Atkinson, Elliot Jobe, Nick Dozier, Peter Wooley, John Hoover, dan, Martin A. Jackson, Héctor Fernando Hurtado, andy ennamorato, Paul Seltmann, Melissa Gore, Dave Pollard, Jack Smith, Philip Da Silva, Guy Israeli, @megalithic, Damian Crawford, Felix Gliesche, April Carter Grant, Heidi, jim tierney, Andrea Giammarchi, Nico Vignola, Don Jones, Chris Hartjes, Alex Howes, john gibbon, David J. Groom, BBox, Yu 'Dilys' Sun, Nate Steiner, Brandon Satrom, Brian Wyant, Wesley Hales, Ian Pouncey, Timothy Kevin Oxley, George Terezakis, sanjay raj, Jordan Harband, Marko McLion, Wolfgang Kaufmann, Pascal Peuckert, Dave Nugent, Markus Liebelt, Welling Guzman, Nick Cooley, Daniel Mesquita, Robert Syvarth, Chris Coyier, Rémy Bach, Adam Dougal, Alistair Duggin, David Loidolt, Ed Richer, Brian Chenault, GoldFire Studios, Carles Andrés, Carlos Cabo, Yuya Saito, roberto ricardo, Barnett Klane, Mike Moore, Kevin Marx, Justin Love, Joe Taylor, Paul Dijou, Michael Kohler, Rob Cassie, Mike Tierney, Cody Leroy Lindley, tofuji, Shimon Schwartz, Raymond, Luc De Brouwer, David Hayes, Rhys Brett-Bowen, Dmitry, Aziz Khoury, Dean, Scott Tolinski - Level Up, Clement Boirie, Djordje Lukic, Anton Kotenko, Rafael Corral, Philip Hurwitz, Jonathan Pidgeon, Jason Campbell, Joseph C., SwiftOne, Jan Hohner, Derick Bailey, getify, Daniel Cousineau, Chris Charlton, Eric Turner, David Turner, Joël Galeran, Dharma Vagabond, adam, Dirk van Bergen, dave ♥♫★ furf, Vedran Zakanj, Ryan McAllen, Natalie Patrice Tucker, Eric J. Bivona, Adam Spooner, Aaron Cavano, Kelly Packer, Eric J, Martin Drenovac, Emilis, Michael Pelikan, Scott F. Walter, Josh Freeman, Brandon Hudgeons, vijay chennupati, Bill Glennon, Robin R., Troy Forster, otaku_coder, Brad, Scott, Frederick Ostrander, Adam Brill, Seb Flippence, Michael Anderson, Jacob, Adam Randlett, Standard, Joshua Clanton, Sebastian Kouba, Chris Deck, SwordFire, Hannes Papenberg, Richard Woeber, hnzz, Rob Crowther, Jedidiah Broadbent, Sergey Chernyshev, Jay-Ar Jamon, Ben Combee, luciano bonachela, Mark Tomlinson, Kit Cambridge, Michael Melgares, Jacob Adams, Adrian Bruinhout, Bev Wieber, Scott Puleo, Thomas Herzog, April Leone, Daniel Mizieliński, Kees van Ginkel, Jon Abrams, Erwin Heiser, Avi Laviad, David newell, Jean-Francois Turcot, Niko Roberts, Erik Dana, Charles Neill, Aaron Holmes, Grzegorz Ziółkowski, Nathan Youngman, Timothy, Jacob Mather, Michael Allan, Mohit Seth, Ryan Ewing, Benjamin Van Treese, Marcelo Santos, Denis Wolf, Phil Keys, Chris Yung, Timo Tijhof, Martin Lekvall, Agendine, Greg Whitworth, Helen Humphrey, Dougal Campbell, Johannes Harth, Bruno Girin, Brian Hough, Darren Newton, Craig McPheat, Olivier Tille, Dennis Roethig, Mathias Bynens, Brendan Stromberger, sundeep, John Meyer, Ron Male, John F Croston III, gigante, Carl Bergenhem, B.J. May, Rebekah Tyler, Ted Foxberry, Jordan Reese, Terry Suitor, afeliz, Tom Kiefer, Darragh Duffy, Kevin Vanderbeken, Andy Pearson, Simon Mac Donald, Abid Din, Chris Joel, Tomas Theunissen, David Dick, Paul Grock, Brandon Wood, John Weis, dgrebb, Nick Jenkins, Chuck Lane, Johnny Megahan, marzsman, Tatu Tamminen, Geoffrey Knauth, Alexander Tarmolov, Jeremy Tymes, Chad Auld, Sean Parmelee, Rob Staenke, Dan Bender, Yannick derwa, Joshua Jones, Geert Plaisier, Tom LeZotte, Christen Simpson, Stefan Bruvik, Justin Falcone, Carlos Santana, Michael Weiss, Pablo Villoslada, Peter deHaan, Dimitris Iliopoulos, seyDoggy, Adam Jordens, Noah Kantrowitz, Amol M, Matthew Winnard, Dirk Ginader, Phinam Bui, David Rapson, Andrew Baxter, Florian Bougel, Michael George, Alban Escalier, Daniel Sellers, Sasha Rudan, John Green, Robert Kowalski, David I. Teixeira (@ditma, Charles Carpenter, Justin Yost, Sam S, Denis Ciccale, Kevin Sheurs, Yannick Croissant, Pau Fracés, Stephen McGowan, Shawn Searcy, Chris Ruppel, Kevin Lamping, Jessica Campbell, Christopher Schmitt, Sablons, Jonathan Reisdorf, Bunni Gek, Teddy Huff, Michael Mullany, Michael Fürstenberg, Carl Henderson, Rick Yoesting, Scott Nichols, Hernán Ciudad, Andrew Maier, Mike Stapp, Jesse Shawl, Sérgio Lopes, jsulak, Shawn Price, Joel Clermont, Chris Ridmann, Sean Timm, Jason Finch, Aiden Montgomery, Elijah Manor, Derek Gathright, Jesse Harlin, Dillon Curry, Courtney Myers, Diego Cadenas, Arne de Bree, João Paulo Dubas, James Taylor, Philipp Kraeutli, Mihai Păun, Sam Gharegozlou, joshjs, Matt Murchison, Eric Windham, Timo Behrmann, Andrew Hall, joshua price, Théophile Villard Эта серия книг выпускается в стиле "open source", включая редактирование и выпуск. Мы отдаем дань благодарности GitHub за предоставление такой возможности для сообщества! Еще раз спасибо всех бесчисленным людям, которых я не перечислил по имени, но кого я тем не менее должен поблагодарить. Пусть эта серия книг будет "принадлежать" всем нам и служить вкладом в увеличение информированности и понимании языка JavaScript, на благо все нынешних и будущих вкладчиков в общее дело сообщества. ================================================ FILE: async & performance/ch1.md ================================================ # Вы не знаете JS: Асинхронность и Выполнение # Глава 1: Асинхронность: Сейчас и Потом Управление программой в течении её времени выполнения является глубокой и, в то же время, важной темой для понимания Javascript. Имеется ввиду не только то, что происходит от начала 'for' цикла и до его завершения, выполнение которого занимает, в основном, небольшой промежуток времени (от микро до миллисекунд), но и, то, что случается между запуском одной части программы *сейчас* и другой *позже* -- в этом промежутке, когда она не в активном исполнении. Практически, все нетривиальные программы (особенно, написанные на Javascript) управляют выше упомянутым промежутком, будь то ожидание ввода данных пользователем, запрос информации с базы данных или файловой системы, отправка данных по сети и ожидание ответа, или выполнение повторяющихся действий в заданном интервале (анимация). Во всех этих случаях, ваша программа должна управлять своим состоянием во время этой "паузы" в ходе своего исполнения. Как говорят в Лондоне "Mind the gap!"(дословно -- "Помни о разрыве") -- надпись на платформе, предупреждающая о расстоянии между её краем и дверью поезда. На самом деле, отношение *сейчас* и *потом* частей вашей программы является сердцем асинхронного программирования. Асинхронное программирование существовало в JS изначально. Но многие разработчики не вдавались в подробности как именно оно работает в их программах, или находили *другие* способы управления им. *Почти хорошим* подходом считалась скромная callback функция. Многие до сих пор считают, что callback'и являются эффективным решением. Но с ростом масштаба и сложности JS, для удовлетворения постоянно расширяющихся требований первоклассного языка программирования, работающего как в браузерах так и на серверах, и на любых мыслимых устройствах, между прочим, росла и боль с которой приходилось управлять асинхронностью. Сейчас это все может показаться довольно абстрактным. Но я вас уверяю, что мы углубимся в детали и изучим множество новых методов для асинхронного программирования в JavaScript в следующих главах. Для этого нам надо разобраться во всех тонкостях асинхронности в JS. ## Программа по кусочкам Даже, если вы пишете ваш код в одном *.js* файле, ваша программа почти наверняка состоит из нескольких *кусочков*, один из которых выполнится *сейчас*, а остальные *потом*. Наиболее распространенная единица *кусочка* - 'function'. Большинство новоприбывших в JS разработчиков думают, что асинхронный код приостанавливается для выполнения, но это не так. Давайте посмотрим: ```js // ajax(..) обычная ajax функция var data = ajax( "http://some.url.1" ); console.log( data ); // Упс! `данные` не получены ``` Вы вероятно знаете, что Ajax запросы не выполняются синхронно, а это значит, что `ajax(..)` функция ещё не получила данных для присвоения в переменную `data`. Если бы `ajax(..)` запрос мог бы приостановить код для получения данных, тогда бы `data` не была пуста. Простейшая (но далеко не самая лучшая) реализация Ajax -- это использование функции callback. ```js ajax( "http://some.url.1", function myCallbackFunction(data){ console.log( data ); // Ура, Я получил `data`! } ); ``` **Предупреждение.** Возможно, вы слышали, что можно выполнять синхронные запросы Ajax. Хотя технически это верно, вы никогда не должны этого делать ни при каких обстоятельствах, потому что это блокирует пользовательский интерфейс браузера (кнопки, меню, прокрутку и т.д.) и предотвращает любое взаимодействие с пользователем. Это ужасная идея, и ее всегда следует избегать. Прежде чем возражать против несогласия, нет, ваше желание избежать путаницы с колбеками *не* является оправданием для блокировки синхронного Ajax. Например, рассмотрим этот код: ```js function now() { return 21; } function later() { answer = answer * 2; console.log( "Meaning of life:", answer ); } var answer = now(); setTimeout( later, 1000 ); // Meaning of life: 42 ``` В этой программе есть две части: то, что будет запущено *сейчас*, и то, что будет запущено *позже*. Должно быть довольно очевидно, что это за два фрагмента, но давайте будем очень явными: Сейчас: ```js function now() { return 21; } function later() { .. } var answer = now(); setTimeout( later, 1000 ); ``` Позже: ```js answer = answer * 2; console.log( "Meaning of life:", answer ); ``` Блок *now* запускается сразу же, как только вы запускаете свою программу. Но `setTimeout(..)` также устанавливает событие (тайм-аут), которое произойдет *позже*, поэтому содержимое функции `later()` будет выполнено позже (через 1000 миллисекунд). Каждый раз, когда вы заключаете часть кода в `функцию` и указываете, что она должна выполняться в ответ на какое-то событие (таймер, щелчок мышью, ответ Ajax и т.д.), вы создаете более позднюю* часть своего кода, и, таким образом, вводя асинхронность в вашу программу. ### Асинхронная консоль Не существует спецификации или набора требований относительно того, как работают методы `console.*` — они официально не являются частью JavaScript, а вместо этого добавляются в JS *средой всплытия* (см. заголовок *Типы и грамматика* эту серию книг). Таким образом, разные браузеры и среды JS делают то, что им заблагорассудится, что иногда может привести к путанице. В частности, есть некоторые браузеры и некоторые условия, при которых `console.log(..)` на самом деле не сразу выводит то, что ему дано. Основная причина, по которой это может произойти, заключается в том, что ввод-вывод является очень медленной и блокирующей частью многих программ (не только JS). Таким образом, браузер может лучше (с точки зрения страницы/интерфейса) обрабатывать «консольный» ввод-вывод асинхронно в фоновом режиме, и вы, возможно, даже не подозреваете об этом. Не очень распространенный, но возможный сценарий, в котором это может быть *наблюдаемо* (не из самого кода, а извне): ```js var a = { index: 1 }; // позже console.log( a ); // ?? // ещё позже a.index++; ``` Обычно мы ожидаем, что снимок объекта `a` будет сделан точно в момент выполнения оператора `console.log(..)`, выводя что-то вроде `{ index: 1 }`, так что в следующем операторе, когда `a.index++`, он изменяет что-то отличное от вывода `a` или сразу после него. В большинстве случаев приведенный выше код, вероятно, будет создавать представление объекта в консоли инструментов разработчика, которое вы ожидаете. Но возможно, что этот же код может работать в ситуации, когда браузер считает необходимым отложить консольный ввод-вывод в фоновом режиме, и в этом случае *возможно*, что к тому времени, когда объект будет представлен в консоли браузера, ` a.index++` уже произошло, и он показывает `{ index: 2 }`. Это движущаяся цель, при каких условиях именно «консольный» ввод-вывод будет отложен, или даже будет ли он наблюдаться. Просто имейте в виду эту возможную асинхронность в вводе/выводе на тот случай, если вы когда-нибудь столкнетесь с проблемами при отладке, когда объекты были изменены *после* оператора `console.log(..)`, и тем не менее вы видите неожиданные изменения. **Примечание.** Если вы столкнулись с этим редким случаем, лучше всего использовать точки останова в отладчике JS вместо того, чтобы полагаться на вывод консоли. Следующим лучшим вариантом было бы принудительно сделать «моментальный снимок» рассматриваемого объекта, сериализовав его в «строку», например, с «JSON.stringify(..)». ## Цикл событий (Event Loop) Давайте сделаем (возможно, шокирующее) утверждение: несмотря на явное разрешение асинхронного кода JS (например, тайм-аут, который мы только что рассмотрели), до недавнего времени (ES6) сам JavaScript на самом деле никогда не имел прямого встроенного понятия асинхронности. **Что!?** Это кажется безумием, верно? На самом деле, это правда. Сам JS-движок никогда не делал ничего, кроме как выполнял один фрагмент вашей программы в любой момент, когда его об этом попросят. "Попросят" Кто? Это важная часть! Движок JS не работает изолированно. Он работает внутри *среды хостинга*, которая для большинства разработчиков является типичным веб-браузером. За последние несколько лет (но не исключительно) JS расширился за пределы браузера в другие среды, такие как серверы, с помощью таких вещей, как Node.js. Фактически, в наши дни JavaScript внедряется во все виды устройств, от роботов до лампочек. Но один общий «поток» (это не очень тонкая асинхронная шутка, чего бы это ни стоило) всех этих сред заключается в том, что в них есть механизм, который обрабатывает выполнение нескольких фрагментов вашей программы *с течением времени*, в каждый момент времени. момент вызова JS-движка, называемый «циклом событий». Другими словами, движок JS не имеет врожденного чувства *времени*, а вместо этого является средой выполнения по требованию для любого произвольного фрагмента JS. Это окружающая среда, которая всегда *планирует* «события» (выполнения кода JS). Так, например, когда ваша программа JS делает запрос Ajax для получения некоторых данных с сервера, вы настраиваете код «ответа» в функции (обычно называемой «обратным вызовом»), и механизм JS сообщает среде хостинга, «Эй, я собираюсь приостановить выполнение на данный момент, но когда вы закончите с этим сетевым запросом, и у вас есть какие-то данные, *вызовите* эту функцию *назад*». Затем браузер настраивается на прослушивание ответа из сети, и когда ему есть что вам дать, он планирует выполнение функции обратного вызова, вставляя ее в *цикл событий*. Так что же такое *цикл событий*? Давайте сначала концептуализируем это с помощью некоторого фальшивого кода: ```js // `eventLoop` - это массив, который действует как очередь (первым пришел, первым ушел) var eventLoop = [ ]; var event; // будет работать "всегда" while (true) { // выполняем "такт" if (eventLoop.length > 0) { // получаем следующее событие из очереди event = eventLoop.shift(); // теперь выполняем следующее событие try { event(); } catch (err) { reportError(err); } } } ``` Это, конечно, сильно упрощенный псевдокод для иллюстрации концепций. Но этого должно быть достаточно, чтобы помочь лучше понять. Как видите, существует непрерывно работающий цикл, представленный циклом while, и каждая итерация этого цикла называется «тактом». Для каждого такта, если событие ожидает в очереди, оно снимается и выполняется. Эти события являются обратными вызовами ваших функций. Важно отметить, что `setTimeout(..)` не помещает ваш обратный вызов в очередь цикла событий. Что он делает, так это устанавливает таймер; когда таймер истекает, среда помещает ваш обратный вызов в цикл событий, так что какой-то будущий такт подберет его и выполнит. Что делать, если в этот момент в цикле событий уже 20 элементов? Ваш обратный вызов ждет. Он становится в очередь позади других — обычно нет пути для опережения очереди и пропуска вперед в очереди. Это объясняет, почему таймеры `setTimeout(..)` могут не срабатывать с идеальной временной точностью. Вам гарантируется (грубо говоря), что ваш обратный вызов не сработает *до* указанного вами временного интервала, но это может произойти в это время или позже, в зависимости от состояния очереди событий. Таким образом, другими словами, ваша программа обычно разбивается на множество небольших фрагментов, которые выполняются один за другим в очереди цикла событий. И технически, другие события, не связанные напрямую с вашей программой, также могут чередоваться в очереди. **Примечание.** Мы упоминали «до недавнего времени» в связи с тем, что ES6 изменил характер управления очередью цикла событий. В основном это формальная техническая особенность, но теперь ES6 определяет, как работает цикл обработки событий, что означает, что технически это находится в компетенции JS-движка, а не только *среды хостинга*. Одной из основных причин этого изменения является введение промисов ES6, которые мы обсудим в главе 3, потому что они требуют возможности иметь прямой, детальный контроль над операциями планирования в очереди цикла событий (см. обсуждение `setTimeout (..0)` в разделе "Cooperation"). ## Параллельная обработка Очень часто смешивают термины «асинхронный» и «параллельный», но на самом деле они совершенно разные. Помните, что асинхронность — это разрыв между *сейчас* и *позже*. Но параллель означает, что вещи могут происходить одновременно. Наиболее распространенными инструментами для параллельных вычислений являются процессы и потоки. Процессы и потоки выполняются независимо и могут выполняться одновременно: на разных процессорах или даже на разных компьютерах, но несколько потоков могут совместно использовать память одного процесса. Цикл событий, напротив, разбивает свою работу на задачи и выполняет их последовательно, запрещая параллельный доступ и изменения в разделяемой памяти. Параллелизм и «сериализм» могут сосуществовать в виде взаимодействующих циклов событий в отдельных потоках. Чередование параллельных потоков выполнения и чередование асинхронных событий происходит на очень разных уровнях детализации. Например: ```js function later() { answer = answer * 2; console.log( "Meaning of life:", answer ); } ``` В то время как все содержимое `later()` будет рассматриваться как одна запись в очереди цикла событий, если подумать о потоке, в котором будет выполняться этот код, на самом деле существует, возможно, дюжина различных низкоуровневых операций. Например, `answer = answer * 2` требует сначала загрузить текущее значение `answer`, затем куда-то поместить `2`, затем выполнить умножение, затем взять результат и сохранить его обратно в `answer`. В однопоточной среде на самом деле не имеет значения, что элементы в очереди потоков являются низкоуровневыми операциями, потому что ничто не может прервать поток. Но если у вас есть параллельная система, в которой два разных потока работают в одной и той же программе, вы, скорее всего, получите непредсказуемое поведение. Рассмотрим: ```js var a = 20; function foo() { a = a + 1; } function bar() { a = a * 2; } // ajax(..) — это произвольная функция Ajax, заданная библиотекой ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` В однопоточном поведении JavaScript, если `foo()` запускается до `bar()`, результатом будет то, что `a` имеет `42`, но если `bar()` запускается до `foo()`, результатом будет `a` будет `41`. Если бы события JS, совместно использующие одни и те же данные, выполнялись параллельно, проблемы были бы гораздо более тонкими. Рассмотрим эти два списка задач псевдокода как потоки, которые могли бы соответственно запускать код в `foo()` и `bar()`, и подумайте, что произойдет, если они будут выполняться точно в одно и то же время: Поток 1 («X» и «Y» — временные ячейки памяти): ``` foo(): а. загрузить значение `a` в `X` б. сохранить `1` в `Y` в. добавить `X` и `Y`, сохранить результат в `X` д. сохранить значение `X` в `a` ``` Поток 2 («X» и «Y» — временные ячейки памяти): ``` bar(): а. загрузить значение `a` в `X` б. сохранить `2` в `Y` в. умножить `X` и `Y`, сохранить результат в `X` д. сохранить значение `X` в `a` ``` Теперь предположим, что два потока действительно выполняются параллельно. Вы, вероятно, можете определить проблему, верно? Они используют разделяемые ячейки памяти `X` и `Y` для своих временных шагов. Каков конечный результат в `a`, если шаги происходят так? ``` 1a (загрузить значение `a` в `X` ==> `20`) 2a (загрузить значение `a` в `X` ==> `20`) 1b (сохранить `1` в `Y` ==> `1`) 2b (сохранить `2` в `Y` ==> `2`) 1c (добавьте `X` и `Y`, сохраните результат в `X` ==> `22`) 1d (сохранить значение `X` в `a` ==> `22`) 2c (умножить `X` и `Y`, сохранить результат в `X` ==> `44`) 2d (сохранить значение `X` в `a` ==> `44`) ``` Результатом в «а» будет «44». Но как быть с этим заказом? ``` 1a (загрузить значение `a` в `X` ==> `20`) 2a (загрузить значение `a` в `X` ==> `20`) 2b (сохранить `2` в `Y` ==> `2`) 1b (сохранить `1` в `Y` ==> `1`) 2c (умножить `X` и `Y`, сохранить результат в `X` ==> `20`) 1c (добавьте `X` и `Y`, сохраните результат в `X` ==> `21`) 1d (сохранить значение `X` в `a` ==> `21`) 2d (сохранить значение `X` в `a` ==> `21`) ``` Результатом в «а» будет «21». Таким образом, многопоточное программирование очень сложно, потому что, если вы не предпримете специальных шагов для предотвращения такого прерывания/перемежения, вы можете получить очень неожиданное, недетерминированное поведение, которое часто приводит к головной боли. JavaScript никогда не делится данными между потоками, а это означает, что *этот* уровень недетерминизма не имеет значения. Но это не значит, что JS всегда детерминирован. Помните ранее, где относительное упорядочение `foo()` и `bar()` дает два разных результата (`41` или `42`)? **Примечание.** Возможно, это еще не очевидно, но не всякий недетерминизм плох. Иногда это неуместно, а иногда намеренно. Мы увидим больше примеров этого в этой и следующих нескольких главах. ### Выполнить до завершения Из-за однопоточности JavaScript код внутри `foo()` (и `bar()`) является атомарным, что означает, что как только `foo()` запускается, весь его код завершится до того, как любой из кода в `bar()` начнет работать, и наоборот. Это называется поведением «запуск до завершения». На самом деле семантика выполнения до завершения более очевидна, когда `foo()` и `bar()` содержат больше кода, например: ```js var a = 1; var b = 2; function foo() { a++; b = b * a; a = b + 3; } function bar() { b--; a = 8 + b; b = a * 2; } // ajax(..) — это произвольная функция Ajax, заданная библиотекой ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` Поскольку `foo()` не может быть прервана `bar()`, а `bar()` не может быть прервана `foo()`, эта программа имеет только два возможных результата в зависимости от того, какой из них запустится первым: - если бы существовала многопоточность и отдельные операторы в `foo()` и `bar()` могли чередоваться, количество возможных результатов было бы значительно увеличено! Блок 1 является синхронным (происходит *сейчас*), но фрагменты 2 и 3 асинхронны (происходят *позже*), что означает, что их выполнение будет разделено временным промежутком. Блок 1: ```js var a = 1; var b = 2; ``` Блок 2 (`foo()`): ```js a++; b = b * a; a = b + 3; ``` Блок 3 (`bar()`): ```js b--; a = 8 + b; b = a * 2; ``` Блоки 2 и 3 могут выполняться в любом порядке, поэтому для этой программы есть два возможных результата, как показано здесь: Вывод 1: ```js var a = 1; var b = 2; // foo() a++; b = b * a; a = b + 3; // bar() b--; a = 8 + b; b = a * 2; a; // 11 b; // 22 ``` Вывод 2: ```js var a = 1; var b = 2; // bar() b--; a = 8 + b; b = a * 2; // foo() a++; b = b * a; a = b + 3; a; // 183 b; // 180 ``` Два результата одного и того же кода означают, что у нас все еще есть недетерминизм! Но это на уровне упорядочения функций (событий), а не на уровне упорядочения операторов (или, фактически, на уровне упорядочения операций выражений), как в случае с потоками. Другими словами, это *более детерминировано*, чем потоки. Применительно к поведению JavaScript этот недетерминизм упорядочения функций является общим термином «состояние гонки», поскольку `foo()` и `bar()` соревнуются друг с другом, чтобы увидеть, какой из них запустится первым. В частности, это «состояние гонки», потому что вы не можете надежно предсказать, как окажутся «a» и «b». **Примечание:** Если бы в JS была функция, которая каким-то образом не имела бы поведения выполнения до завершения, у нас могло бы быть гораздо больше возможных результатов, верно? Оказывается, ES6 представляет именно такую вещь (см. главу 4 «Генераторы»), но не волнуйтесь прямо сейчас, мы еще вернемся к этому! ## Конкурентность Давайте представим себе сайт, на котором отображается список обновлений статуса (например, лента новостей в социальной сети), который постепенно загружается по мере того, как пользователь прокручивает список вниз. Чтобы такая функция работала правильно, (по крайней мере) два отдельных «процесса» должны выполняться *одновременно* (т.е. в течение одного и того же промежутка времени, но не обязательно в один и тот же момент). **Примечание.** Мы используем здесь слово «процесс» в кавычках, потому что это не настоящие процессы уровня операционной системы с точки зрения информатики. Это виртуальные процессы или задачи, которые представляют собой логически связанные последовательные серии операций. Мы просто предпочтем «процесс», а не «задачу», потому что с точки зрения терминологии это будет соответствовать определениям изучаемых нами понятий. Первый «процесс» будет реагировать на события «onscroll» (запросы Ajax для нового контента), поскольку они срабатывают, когда пользователь прокручивает страницу дальше вниз. Второй «процесс» будет получать ответы Ajax (для отображения содержимого на странице). Очевидно, что если пользователь прокручивает страницу достаточно быстро, вы можете увидеть два или более события onscroll, запускаемых в течение времени, необходимого для получения и обработки первого ответа, и, таким образом, у вас будут события onscroll и события ответа Ajax, которые будут **конкурировать** друг с другом. Конкурентность — это когда два или более «процессов» выполняются одновременно в течение одного и того же периода, независимо от того, происходят ли их отдельные составляющие операции *параллельно* (в один и тот же момент на разных процессорах или ядрах) или нет. Тогда вы можете думать о конкурентности как о параллелизме на уровне «процесса» (или на уровне задачи), в отличие от параллелизма на уровне операций (потоки, исполняемые на отдельных процессорах). **Примечание.** Конкурентность также вводит необязательное понятие этих «процессов», взаимодействующих друг с другом. Мы вернемся к этому позже. Для данного окна времени (несколько секунд пользовательской прокрутки) давайте визуализируем каждый независимый «процесс» как серию событий/операций: "Процесс" 1 (`onscroll` events): ``` onscroll, request 1 onscroll, request 2 onscroll, request 3 onscroll, request 4 onscroll, request 5 onscroll, request 6 onscroll, request 7 ``` "Процесс" 2 (Ajax response events): ``` response 1 response 2 response 3 response 4 response 5 response 6 response 7 ``` Вполне возможно, что событие `onscroll` и событие ответа Ajax могут быть готовы к обработке в один и тот же *момент*. Например, давайте визуализируем эти события на временной шкале: ``` onscroll, request 1 onscroll, request 2 response 1 onscroll, request 3 response 2 response 3 onscroll, request 4 onscroll, request 5 onscroll, request 6 response 4 onscroll, request 7 response 6 response 5 response 7 ``` Но, возвращаясь к нашему представлению о цикле событий из предыдущей главы, JS сможет обрабатывать только одно событие за раз, поэтому либо `onscroll, request 2` будет происходить первым, либо `response 1` должно произойти первым, но они не могут произойти буквально в один и тот же момент. Так же, как дети в школьной столовой, независимо от того, какую толпу они образуют за дверью, им придется выстроиться в одну очередь, чтобы получить свой обед! Давайте визуализируем чередование всех этих событий в очереди цикла событий. Очередь цикла событий: ``` onscroll, request 1 <--- Процесс 1 начинается onscroll, request 2 response 1 <--- Процесс 2 начинается onscroll, request 3 response 2 response 3 onscroll, request 4 onscroll, request 5 onscroll, request 6 response 4 onscroll, request 7 <--- Процесс 1 заканчивается response 6 response 5 response 7 <--- Процесс 2 заканчивается ``` «Процесс 1» и «Процесс 2» выполняются одновременно (параллельно на уровне задач), но их отдельные события выполняются последовательно в очереди цикла событий. Кстати, обратите внимание, как «ответ 6» и «ответ 5» вернулись не в том порядке, в котором ожидалось? Однопоточный цикл событий — это одно из проявлений конкурентности (конечно, есть и другие, к которым мы вернемся позже). ### Не взаимодействующий Поскольку два или более «процессов» одновременно чередуют свои шаги/события одновременно внутри одной программы, им не обязательно взаимодействовать друг с другом, если задачи не связаны между собой. **Если они не взаимодействуют, недетерминизм вполне приемлем.** Например: ```js var res = {}; function foo(results) { res.foo = results; } function bar(results) { res.bar = results; } ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` `foo()` и `bar()` - это два конкурирующих "процесса", и нельзя однозначно определить в каком порядке они будут вызваны. Однако мы построили программу таким образом, что порядок их вызова не имеет значения, так как они работают независимо и, следовательно, не нуждаются во взаимодействии. Это не ошибка «состояния гонки», так как код всегда будет работать правильно, независимо от порядка. ### Взаимодействие Чаще всего, конкурирующие «процессы» будут по необходимости взаимодействовать косвенно через область видимости и/или DOM. Когда такое взаимодействие произойдет, вам необходимо координировать эти взаимодействия, чтобы предотвратить «состояние гонки», как описано ранее. Вот простой пример двух конкурирующих «процессов», которые взаимодействуют из-за подразумеваемого порядка, который только *иногда нарушается*: ```js var res = []; function response(data) { res.push( data ); } ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); ``` Конкурирующие «процессы» — это два вызова `response()`, которые будут выполняться для обработки ответов Ajax. Они могут происходить в любом порядке. Предположим, ожидаемое поведение состоит в том, что `res[0]` имеет результаты вызова `"http://some.url.1"`, а `res[1]` имеет результаты `"http:/ /some.url.2"` вызов. Иногда это будет иметь место, но иногда они будут перевернуты, в зависимости от того, какой вызов завершится первым. Существует довольно большая вероятность того, что этот недетерминизм является ошибкой «состояния гонки». **Примечание.** Будьте крайне осторожны с предположениями, которые вы можете делать в подобных ситуациях. Например, разработчик нередко замечает, что «http://some.url.2» «всегда» отвечает намного медленнее, чем «http://some.url.1», возможно, из-за в силу того, какие задачи они выполняют (например, одна выполняет задачу базы данных, а другая просто извлекает статический файл), поэтому наблюдаемый порядок всегда выглядит так, как ожидалось. Даже если оба запроса отправляются на один и тот же сервер, и *он* намеренно отвечает в определенном порядке, нет *реальной* гарантии того, в каком порядке ответы будут возвращены в браузер. Таким образом, для устранения такого состояния гонки вы можете координировать порядок взаимодействий: ```js var res = []; function response(data) { if (data.url == "http://some.url.1") { res[0] = data; } else if (data.url == "http://some.url.2") { res[1] = data; } } ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); ``` Независимо от того, какой ответ Ajax возвращается первым, мы проверяем `data.url` (предполагая, что он возвращен с сервера, конечно!), чтобы выяснить, какую позицию данные ответа должны занимать в массиве `res`. `res[0]` всегда будет содержать результаты `"http://some.url.1"`, а `res[1]` всегда будет содержать результаты `"http://some.url.2"`. Путем простой координации мы устранили недетерминизм «состояния гонки». Те же рассуждения из этого сценария применимы, если несколько конкурирующих вызовов функций взаимодействуют друг с другом через общую модель DOM, например, один обновляет содержимое `
`, а другой обновляет стиль или атрибуты `
` (например, чтобы сделать элемент DOM видимым после того, как у него есть содержимое). Вы, вероятно, не захотите показывать элемент DOM до того, как у него будет содержимое, поэтому координация должна обеспечивать правильное взаимодействие с упорядочением. Некоторые сценарии конкуренции *всегда ломаются* (а не просто *иногда*) без скоординированного взаимодействия. Рассмотрим: ```js var a, b; function foo(x) { a = x * 2; baz(); } function bar(y) { b = y * 2; baz(); } function baz() { console.log(a + b); } ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` В этом примере, независимо от того, срабатывает ли сначала `foo()` или `bar()`, `baz()` всегда будет запускаться слишком рано (либо `a`, либо `b` все еще будут `undefined`), но второй вызов `baz()` будет работать, так как будут доступны и `a`, и `b`. Существуют разные способы справиться с таким состоянием. Вот один простой способ: ```js var a, b; function foo(x) { a = x * 2; if (a && b) { baz(); } } function bar(y) { b = y * 2; if (a && b) { baz(); } } function baz() { console.log( a + b ); } ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` Условие `if (a && b)` вокруг вызова `baz()` традиционно называется "воротами", потому что мы не уверены, какой порядок `a` и `b` прибудет, но мы ждем оба из них, чтобы попасть туда, прежде чем мы приступим к открытию ворот (вызов `baz()`). Еще одно условие конкурентного взаимодействия, с которым вы можете столкнуться, иногда называют «гонкой», но правильнее называть его «защелкой». Характеризуется поведением «побеждает только первый». Здесь приемлем недетерминизм, поскольку вы явно говорите, что в «гонке» до финиша может быть только один победитель. Рассмотрим этот сломанный код: ```js var a; function foo(x) { a = x * 2; baz(); } function bar(x) { a = x / 2; baz(); } function baz() { console.log( a ); } ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` Какой бы из них (`foo()` или `bar()`) не срабатывал последним, он не только перезапишет назначенное значение `a` из другого, но также будет дублировать вызов `baz()` (вероятно, нежелательный). Итак, мы можем согласовать взаимодействие с простой защелкой, чтобы пропускать только первую: ```js var a; function foo(x) { if (a == undefined) { a = x * 2; baz(); } } function bar(x) { if (a == undefined) { a = x / 2; baz(); } } function baz() { console.log( a ); } ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); ``` Условие `if (a == undefined)` допускает только первый из `foo()` или `bar()`, а второй (и любые последующие) вызовы будут просто проигнорированы. Нет ничего хорошего в том, чтобы занять второе место! **Примечание.** Во всех этих сценариях мы использовали глобальные переменные для упрощения иллюстрации, но в наших рассуждениях здесь нет ничего, что требовало бы этого. Пока рассматриваемые функции могут получить доступ к переменным (через область видимости), они будут работать так, как задумано. Использование переменных с лексической областью видимости (см. название этой серии книг *Scope & Closures*) и фактически глобальных переменных, как в этих примерах, является очевидным недостатком этих форм координации в условиях конкуренции. В следующих нескольких главах мы увидим другие способы координации, которые в этом отношении намного чище. ### Кооперация Другое выражение координации параллелизма называется «кооперативной конкуренцией» (cooperative concurrency). Здесь основное внимание уделяется не столько взаимодействию через совместное использование значений в областях видимости (хотя это, очевидно, все еще разрешено!). Цель состоит в том, чтобы взять длительный «процесс» и разбить его на шаги или пакеты, чтобы другие конкурирующие «процессы» имели возможность вставить свои операции в очереди цикла событий. Например, рассмотрим обработчик ответа Ajax, которому необходимо просмотреть длинный список результатов для преобразования значений. Мы будем использовать `Array#map(..)`, чтобы сделать код короче: ```js var res = []; // `response(..)` получает массив результатов от Ajax-вызова function response(data) { // добавляем в существующий массив `res` res = res.concat( // создаем новый преобразованный массив со всеми удвоенными значениями `data` data.map( function(val){ return val * 2; } ) ); } ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); ``` Если `"http://some.url.1"` сначала получит свои результаты, весь список будет сразу отображен в `res`. Если это несколько тысяч или меньше записей, это, как правило, не имеет большого значения. Но если это, скажем, 10 миллионов записей, это может занять некоторое время (несколько секунд на мощном ноутбуке, намного дольше на мобильном устройстве и т.д.). Во время выполнения такого «процесса» на странице не может происходить ничего другого, включая другие вызовы `response(..)`, обновления пользовательского интерфейса, даже пользовательские события, такие как прокрутка, ввод текста, нажатие кнопки и тому подобное. Это довольно болезненно. Таким образом, чтобы сделать более кооперативно конкурентную систему, более дружелюбную и не перегружающую очередь цикла событий, вы можете обрабатывать эти результаты асинхронными пакетами, после того как каждый из них «уступает» обратно в цикл событий, чтобы позволить произойти другим ожидающим событиям. Вот очень простой подход: ```js var res = []; // `response(..)` получает массив результатов от Ajax-вызова function response(data) { // давайте просто сделаем 1000 за раз var chunk = data.splice( 0, 1000 ); // добавляем в существующий массив `res` res = res.concat( // создаем новый преобразованный массив со всеми удвоенными значениями `chunk` chunk.map( function(val){ return val * 2; } ) ); // осталось что-нибудь обработать? if (data.length > 0) { // асинхронно планируем следующую партию setTimeout( function(){ response( data ); }, 0 ); } } ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); ``` Мы обрабатываем набор данных блоками максимального размера по 1000 элементов. Поступая таким образом, мы обеспечиваем кратковременный «процесс», даже если это означает гораздо больше последующих «процессов», поскольку чередование с очередью цикла событий даст нам гораздо более отзывчивый (производительный) сайт/приложение. Конечно, мы не координируем порядок взаимодействия любого из этих «процессов», поэтому порядок результатов в `res` не будет предсказуемым. Если бы требовался порядок, вам нужно было бы использовать методы взаимодействия, подобные тем, которые мы обсуждали ранее, или те, которые мы рассмотрим в следующих главах этой книги. Мы используем `setTimeout(..0)` (хак) для асинхронного планирования, что в основном просто означает «вставить эту функцию в конец текущей очереди цикла событий». **Примечание:** `setTimeout(..0)` технически не вставляет элемент непосредственно в очередь цикла событий. Таймер вставит событие при следующей возможности. Например, два последовательных вызова `setTimeout(..0)` не будут строго гарантированы для обработки в порядке вызова, поэтому *возможно* увидеть различные условия, такие как дрейф таймера, когда порядок таких событий не предсказуем. В Node.js аналогичный подход — `process.nextTick(..)`. Несмотря на то, насколько удобным (и, как правило, более производительным) это было бы, не существует единого прямого способа (по крайней мере, пока) во всех средах для обеспечения асинхронного упорядочения событий. Мы рассмотрим эту тему более подробно в следующем разделе. ## Задачи Начиная с ES6, появилась новая концепция над очередью цикла событий, которая называется "Очередь задач". Скорее всего, вам придется столкнуться с асинхронным поведением промисов (см. главу 3). К сожалению, на данный момент это механизм без открытого API, поэтому демонстрация его немного сложнее. Так что нам нужно просто описать его концептуально, чтобы, когда мы будем обсуждать асинхронное поведение с промисами в главе 3, вы поняли, как эти действия планируются и обрабатываются. Итак, лучший способ думать об этом, который я нашел, состоит в том, что «Очередь задач» — это очередь, свисающая с конца каждого такта в очереди цикла событий. Некоторые подразумеваемые асинхронные действия, которые могут произойти во время такта цикла событий, не приведут к добавлению нового события в очередь цикла событий, а вместо этого добавят элемент (также известный как задача) в конец очереди задач текущего такта. Это все равно, что сказать: «О, вот еще одна вещь, которую мне нужно сделать *позже*, но убедитесь, что это произойдет прямо сейчас, прежде чем что-либо еще может произойти». Или, если использовать метафору: очередь цикла событий подобна аттракциону в парке развлечений, где, как только вы закончите кататься, вам нужно вернуться в конец очереди, чтобы прокатиться снова. Но очередь задач похожа на то, как если бы вы закончили поездку, но затем встали в очередь и сразу же вернулись. Задача также может привести к добавлению дополнительных задач в конец одной и той же очереди. Таким образом, теоретически возможно, что «цикл» задач (задача, которая продолжает добавлять другую задачу и т.д.) может вращаться бесконечно, лишая программу возможности перейти к следующему такту цикла событий. Концептуально это было бы почти так же, как простое выражение длительного или бесконечного цикла (например, `while (true) ..`) в вашем коде. Задачи похожи на дух хака `setTimeout(..0)`, но реализованы таким образом, чтобы иметь гораздо более четко определенный и гарантированный порядок: **позже, но как можно скорее**. Давайте представим API для планирования задач (напрямую, без хаков) и назовем его `schedule(..)`. ```js console.log( "A" ); setTimeout( function(){ console.log( "B" ); }, 0 ); // теоретический "API задачи" schedule( function(){ console.log( "C" ); schedule( function(){ console.log( "D" ); } ); } ); ``` Вы можете ожидать, что это напечатает `A B C D`, но вместо этого будет напечатано `A C D B`, потому что задачи происходят в конце текущего такта цикла событий, и таймер срабатывает, чтобы запланировать *следующий* такт цикла событий (если доступно!). В главе 3 мы увидим, что асинхронное поведение промисов основано на задачах, поэтому важно четко понимать, как это связано с поведением цикла обработки событий. ## Порядок операторов Порядок, в котором мы выражаем операторы в нашем коде, не обязательно совпадает с порядком, в котором JS-движок будет их выполнять. Это может показаться довольно странным утверждением, поэтому мы кратко рассмотрим его. Но прежде чем мы это сделаем, мы должны кое-что предельно ясно уяснить: правила/грамматика языка (см. название этой серии книг *Типы и грамматика*) диктуют очень предсказуемое и надежное поведение для упорядочения операторов с точки зрения программы. Итак, то, что мы собираемся обсудить, — это **не то, что вы когда-либо сможете наблюдать** в своей JS-программе. **Предупреждение:** Если вы когда-либо сможете *наблюдать* переупорядочивание операторов компилятора, как мы собираемся проиллюстрировать, это будет явным нарушением спецификации, и это, несомненно, будет связано с ошибкой в ​​движке JS. в вопросе - тот, который должен быть немедленно сообщен и исправлен! Но гораздо чаще вы *подозреваете* что-то сумасшедшее происходит в движке JS, когда на самом деле это просто ошибка (вероятно, "состояние гонки"!) в вашем собственном коде - так что сначала смотрите туда, и снова и снова . Отладчик JS, использующий точки останова и последовательно выполняющий код, станет вашим самым мощным инструментом для обнаружения таких ошибок в *вашем коде*. Рассмотреть возможность: ```js var a, b; a = 10; b = 30; a = a + 1; b = b + 1; console.log( a + b ); // 42 ``` Этот код не имеет выраженной асинхронности (за исключением редкого «консольного» асинхронного ввода-вывода, который обсуждался ранее!), поэтому наиболее вероятным предположением будет то, что он будет обрабатывать строку за строкой сверху вниз. Но *возможно*, что движок JS после компиляции этого кода (да, JS скомпилирован — см. название *Scope & Closures* этой серии книг!) может найти возможности для более быстрого запуска вашего кода, перестраивая (безопасно) порядок этих выражений. По сути, пока вы не можете наблюдать за изменением порядка, все в порядке. Например, движок может решить, что на самом деле быстрее выполнить такой код: ```js var a, b; a = 10; a++; b = 30; b++; console.log( a + b ); // 42 ``` Или это: ```js var a, b; a = 11; b = 31; console.log( a + b ); // 42 ``` Или даже: ```js // так как `a` и `b` больше не используются, мы можем // встроить их и даже не нуждаться в них! console.log( 42 ); // 42 ``` Во всех этих случаях движок JS выполняет безопасную оптимизацию во время компиляции, так как конечный *наблюдаемый* результат будет одним и тем же. Но вот сценарий, в котором эти конкретные оптимизации были бы небезопасными и, следовательно, не могли бы быть разрешены (конечно, нельзя сказать, что они вообще не оптимизированы): ```js var a, b; a = 10; b = 30; // нам нужны `a` и `b` в их предварительно увеличенном состоянии! console.log( a * b ); // 300 a = a + 1; b = b + 1; console.log( a + b ); // 42 ``` Другие примеры, когда переупорядочивание компилятора может создать наблюдаемые побочные эффекты (и, следовательно, должно быть запрещены), включают в себя такие вещи, как любой вызов функции с побочными эффектами (даже и особенно функции-получатели) или прокси-объекты ES6 (см. заголовок *ES6 & Beyond* статьи эту серию книг). Рассмотрим возможность: ```js function foo() { console.log( b ); return 1; } var a, b, c; // Синтаксис литерала геттера ES5.1 c = { get bar() { console.log( a ); return 1; } }; a = 10; b = 30; a += foo(); // 11 b += c.bar; // 31 console.log( a + b ); // 42 ``` Если бы не операторы `console.log(..)` в этом фрагменте (просто используемые как удобная форма наблюдаемого побочного эффекта для иллюстрации), движок JS, вероятно, мог бы свободно, если бы захотел (кто знает, захочет ли!?), изменить порядок кода: ```js // ... a = 10 + foo(); b = 30 + c.bar; // ... ``` В то время как семантика JS, к счастью, защищает нас от *наблюдаемых* кошмаров, которым может угрожать переупорядочивание операторов компилятора, по-прежнему важно понимать, насколько незначительна связь между способом создания исходного кода (сверху вниз) и как он работает после компиляции. Переупорядочивание операторов компилятора — это почти микрометафора конкурентности и взаимодействия. В целом такая осведомленность может помочь вам лучше понять проблемы потока асинхронного кода JS. ## Обзор Программа JavaScript (практически) всегда разбивается на две или более частей, где первая часть выполняется *сейчас*, а следующая часть выполняется *позже*, в ответ на событие. Несмотря на то, что программа выполняется по частям, все они имеют одинаковый доступ к области действия и состоянию программы, поэтому каждое изменение состояния выполняется поверх предыдущего состояния. Всякий раз, когда есть события для запуска, *цикл событий* выполняется до тех пор, пока очередь не станет пустой. Каждая итерация цикла событий — это "такт". Взаимодействие с пользователем, ввод-вывод и таймеры помещают события в очередь событий. В любой момент времени из очереди может быть обработано только одно событие. Во время выполнения события оно может прямо или косвенно вызывать одно или несколько последующих событий. Конкурентность — это когда две или более цепочек событий чередуются во времени, так что с точки зрения высокого уровня кажется, что они выполняются *одновременно* (даже если в любой момент обрабатывается только одно событие). Часто бывает необходимо выполнить какую-то форму координации взаимодействия между этими конкурирующими «процессами» (в отличие от процессов операционной системы), например, чтобы обеспечить упорядоченность или предотвратить «состояние гонки». Эти «процессы» также могут *взаимодействовать*, разбивая себя на более мелкие фрагменты и допуская чередование других «процессов». ================================================ FILE: async & performance/ch2.md ================================================ # Вы не знаете JS: Асинхронность и Выполнение # Глава 2: Колбеки В главе 1 мы рассмотрели терминологию и концепции асинхронного программирования в JavaScript. Наше внимание сосредоточено на понимании однопоточной (по одному за раз) очереди цикла событий, которая управляет всеми «событиями» (вызовами асинхронных функций). Мы также исследовали различные способы, которыми шаблоны конкурентности объясняют отношения (если они есть!) между *одновременно* запущенными цепочками событий или «процессами» (задачами, вызовами функций и т.д.). Во всех наших примерах в главе 1 функция использовалась как отдельная, неделимая единица операций, при этом внутри функции операторы выполняются в предсказуемом порядке (выше уровня компилятора!), но на уровне упорядочения функций события (также известные как асинхронные вызовы функций) могут происходить в различных порядках. Во всех этих случаях функция действует как «обратный вызов», потому что она служит целью цикла обработки событий для «обратного вызова» программы всякий раз, когда обрабатывается этот элемент в очереди. Как вы, несомненно, заметили, обратные вызовы являются наиболее распространенным способом выражения и управления асинхронностью в JS-программах. Действительно, обратный вызов является наиболее фундаментальным асинхронным шаблоном в языке. Бесчисленные JS-программы, даже очень изощренные и сложные, были написаны только на основе коллбеков (конечно, с использованием шаблонов параллелизма, которые мы рассмотрели в главе 1). Функция обратного вызова — это асинхронная рабочая лошадка для JavaScript, и она достойно выполняет свою работу. За исключением... обратные вызовы не лишены недостатков. Многие разработчики воодушевлены *обещанием* (каламбур!) улучшенных асинхронных шаблонов. Но невозможно эффективно использовать любую абстракцию, если вы не понимаете, что она абстрагирует и почему. В этой главе мы подробно рассмотрим пару из них, чтобы понять, почему необходимы и желательны более сложные асинхронные шаблоны (рассматриваемые в последующих главах этой книги). ## Продолжения Давайте вернемся к примеру с асинхронным обратным вызовом, с которого мы начали в главе 1, но позвольте мне немного изменить его, чтобы проиллюстрировать один момент: ```js // A ajax( "..", function(..){ // C } ); // B ``` `// A` и `// B` представляют первую половину программы (она же *сейчас*), а `//C` отмечает вторую половину программы (она же *позже*). Первая половина выполняется сразу, а потом идет "пауза" неопределенной длины. В какой-то момент в будущем, если вызов Ajax завершится, программа продолжит с того места, где остановилась, и *продолжит* со второй половиной. Другими словами, функция обратного вызова оборачивает или инкапсулирует *продолжение* программы. Сделаем код еще проще: ```js // A setTimeout( function(){ // C }, 1000 ); // B ``` Остановитесь на мгновение и спросите себя, как бы вы описали (кому-то другому, менее осведомленному о том, как работает JS) поведение этой программы. Давай, попробуй вслух. Это хорошее упражнение, которое поможет моим следующим пунктам стать более осмысленными. Большинство читателей сейчас, вероятно, подумали или сказали что-то вроде: «Выполните A, затем установите тайм-аут на 1000 миллисекунд, затем, как только это сработает, выполните C». Насколько точно было ваше толкование? Возможно, вы поймали себя на том, что сами отредактировали: «Выполните А, установите тайм-аут на 1000 миллисекунд, затем выполните B, затем, когда истечет тайм-аут, сделайте С». Это более точно, чем первая версия. Вы можете заметить разницу? Несмотря на то, что вторая версия является более точной, обе версии не могут объяснить этот код таким образом, чтобы наш мозг соответствовал коду, а код — движку JS. Это отключение одновременно тонкое и монументальное, и оно лежит в основе понимания недостатков обратных вызовов как асинхронного выражения и управления. Как только мы вводим одно продолжение (или несколько десятков, как многие программы!) в виде функции обратного вызова, мы позволяем образоваться расхождению между тем, как работает наш мозг, и тем, как будет работать код. Каждый раз, когда эти двое расходятся (и это далеко не единственное место, где это происходит, как я уверен, вы знаете!), мы сталкиваемся с неизбежным фактом, что наш код становится труднее понимать, анализировать, отлаживать и поддерживать. ## Последовательный мозг Я почти уверен, что большинство из вас, читатели, слышали, как кто-то сказал (даже сами заявили): «Я многозадачный». Эффекты от попытки действовать в качестве многозадачного человека варьируются от забавных (например, глупая детская игра «погладь голову-потирай-живот») до обыденных (жуй жвачку на ходу) и откровенно опасных (текстовые сообщения за рулем). Но многозадачны ли мы? Можем ли мы действительно совершать два сознательных, преднамеренных действия одновременно и думать/рассуждать о них обоих в один и тот же момент? Есть ли у нашего самого высокого уровня функциональности мозга параллельная многопоточность? Ответ может вас удивить: **вероятно, нет.** На самом деле наш мозг просто не так устроен. Мы гораздо больше занимаемся одиночными делами, чем многие из нас (особенно личности типа А!) хотели бы признать. На самом деле мы можем думать только об одном в любой данный момент. Я не говорю обо всех наших непроизвольных, подсознательных, автоматических функциях мозга, таких как сердцебиение, дыхание и моргание век. Все это жизненно важные задачи для нашей устойчивой жизни, но мы намеренно не выделяем для них никаких умственных способностей. К счастью, пока мы одержимы проверкой ленты социальных сетей в 15-й раз за три минуты, наш мозг продолжает выполнять все эти важные задачи в фоновом режиме (потоки!) Вместо этого мы говорим о любой задаче, которая находится в центре нашего внимания в данный момент. Для меня это написание текста в этой книге прямо сейчас. Выполняю ли я какую-либо другую функцию мозга более высокого уровня точно в этот же момент? Нет, не совсем. Я отвлекаюсь быстро и легко — несколько десятков раз в этих последних парах абзацев! Когда мы *имитируем* многозадачность, например, пытаемся что-то напечатать во время разговора с другом или членом семьи по телефону, на самом деле мы, скорее всего, действуем как быстрые переключатели контекста. Другими словами, мы переключаемся между двумя или более задачами в быстрой последовательности, *одновременно* продвигаясь по каждой задаче маленькими, быстрыми порциями. Мы делаем это так быстро, что внешнему миру кажется, будто мы делаем эти вещи *параллельно*. Звучит ли это для вас подозрительно как конкурентность с асинхронными событиями (подобная той, что происходит в JS)?! Если нет, вернитесь и прочитайте главу 1 еще раз! На самом деле, один из способов упростить (т. е. злоупотребить) невероятно сложный мир неврологии и превратить его в то, что я могу отдалённо надеяться обсудить здесь, заключается в том, что наш мозг работает примерно так же, как очередь цикла событий. Если вы думаете о каждой отдельной букве (или слове), которую я печатаю, как об одном асинхронном событии, только в этом предложении есть несколько десятков возможностей для моего мозга быть прерванным каким-то другим событием, например, от моих органов чувств или даже просто от моего сознания. случайные мысли. Меня не прерывают и не вовлекают в другой «процесс» при каждой возможности (к счастью, иначе эта книга никогда не была бы написана!). Но достаточно часто случается так, что я чувствую, что мой собственный мозг почти постоянно переключается на разные контексты (также известные как «процессы»). И это очень похоже на то, как, вероятно, чувствовал бы себя движок JS. ### Делать VS Планировать Итак, наш мозг можно представить как работающий в однопоточной очереди цикла событий подобно движку JS. Это звучит как хорошая аналогия. Но нам нужно быть более тонкими анализируя это. Существует большая, заметная разница между тем, как мы планируем различные задачи, и тем, как наш мозг на самом деле выполняет эти задачи. Опять же, вернемся к написанию этого текста как моей метафоры. Мой грубый ментальный план состоит в том, чтобы продолжать писать и писать, последовательно проходя через набор пунктов, которые я упорядочил в своих мыслях. Я не планирую никаких перерывов или нелинейной активности в этом письме. Но тем не менее мой мозг все время переключается. Несмотря на то, что на операционном уровне наш мозг работает асинхронно, кажется, что мы планируем задачи последовательно и синхронно. «Мне нужно сходить в магазин, потом купить молока, а потом сдать вещи из химчистки». Вы заметите, что это мышление более высокого уровня (планирование) не кажется очень асинхронным в своей формулировке. На самом деле, мы редко намеренно думаем исключительно с точки зрения событий. Вместо этого мы планируем все тщательно, последовательно (А затем Б затем С), и мы допускаем до некоторой степени своего рода временную блокировку, которая заставляет Б ждать А, а С ждать Б. Когда разработчик пишет код, он планирует ряд действий, которые должны произойти. Если он хороший разработчик, то он **тщательно планирует** это. «Мне нужно установить `z` в значение `x`, а затем `x` в значение `y`» и так далее. Когда мы пишем синхронный код, оператор за оператором, он работает во многом подобно нашему списку дел: ```js // поменять местами `x` и `y` (через временную переменную `z`) z = x; x = y; y = z; ``` Эти три оператора присваивания являются синхронными, поэтому `x = y` ожидает завершения `z = x`, а `y = z`, в свою очередь, ожидает завершения `x = y`. Другими словами, эти три утверждения связаны во времени с выполнением в определенном порядке, одно за другим. К счастью, нам не нужно беспокоиться о каких-либо подробностях, связанных с асинхронными событиями. Если бы мы это сделали, код быстро стал бы намного сложнее! Итак, если синхронное планирование мозга хорошо соотносится с операторами синхронного кода, насколько хорошо наш мозг справляется с планированием асинхронного кода? Оказывается, то, как мы выражаем асинхронность (с обратными вызовами) в нашем коде, совсем не соответствует этому синхронному поведению планирования мозга. Можете ли вы на самом деле представить, что у вас есть ход мыслей, который планирует ваши дела таким образом? > "Мне нужно в магазин, но по дороге я уверен, что мне позвонят, так что "Привет, мама", и пока она начнет говорить, я поищу адрес магазина по GPS, но это займет секунду, поэтому я убавлю радио, чтобы лучше слышать маму, а потом пойму, что забыл надеть куртку, а на улице холодно, но неважно, продолжаю ехать и разговариваю с мамой, а затем звон ремня безопасности напоминает мне о том, что нужно пристегнуться, так что «Да, мама, я пристегнут ремнем безопасности, я всегда пристегиваюсь!». Ах, наконец-то GPS получил направление, теперь...» Как бы нелепо это ни звучало как формулировка того, как мы планируем свой день и думаем о том, что делать и в каком порядке, тем не менее именно так работает наш мозг на функциональном уровне. Помните, что это не многозадачность, это просто быстрое переключение контекста. Причина, по которой нам, как разработчикам, трудно писать асинхронный код с событиями, особенно когда все, что у нас есть, это обратный вызов, заключается в том, что поток мыслей/планирование потока сознания неестественен для большинства из нас. Мы думаем пошагово, но инструменты (обратные вызовы), доступные нам в коде, не выражаются пошагово, как только мы переходим от синхронного к асинхронному. И **вот** почему так сложно точно написать и обосновать асинхронный код JS с обратными вызовами: потому что это не то, как работает наш мозг. **Примечание:** Единственное, что может быть хуже, чем не знать, почему некоторые коды не работают, — это не знать, почему они вообще работают! Это классический менталитет «карточного домика»: «он работает, но не знаю почему, поэтому никто его не трогает!» Возможно, вы слышали «Ад — это другие люди» (Сартр), а программистский мем — «Ад — это код других людей». Я искренне верю: «Ад - это не понимать собственный код». И обратные вызовы являются одним из основных виновников. ### Вложенные/связанные обратные вызовы Рассмотрим: ```js listen( "click", function handler(evt){ setTimeout( function request(){ ajax( "http://some.url.1", function response(text){ if (text == "hello") { handler(); } else if (text == "world") { request(); } } ); }, 500) ; } ); ``` Есть хорошие шансы, что такой код узнаваем для вас. У нас есть цепочка из трех вложенных друг в друга функций, каждая из которых представляет собой шаг в асинхронной последовательности (задача, «процесс»). Этот тип кода часто называют «ад обратных вызовов», а иногда также называют «пирамидой гибели» (из-за его треугольной формы, обращенной вбок из-за вложенного углубления). Но «ад обратных вызовов» на самом деле не имеет почти ничего общего с вложенностью/отступом. Это гораздо более глубокая проблема. Мы увидим, как и почему, в оставшейся части этой главы. Во-первых, мы ждем события «click», затем мы ждем срабатывания таймера, затем мы ждем возврата ответа Ajax, после чего он может повторить все это снова. На первый взгляд может показаться, что этот код естественным образом связывает свою асинхронность с последовательным мозговым планированием. Сначала (*сейчас*): ```js listen( "..", function handler(..){ // .. } ); ``` *позже*: ```js setTimeout( function request(..){ // .. }, 500) ; ``` *еще позже*: ```js ajax( "..", function response(..){ // .. } ); ``` Наконец (наиболее *позже*): ```js if ( .. ) { // .. } else .. ``` Но есть несколько проблем с линейными рассуждениями об этом коде. Во-первых, случайность примера состоит в том, что наши шаги находятся на следующих строках (1, 2, 3 и 4...). В настоящих асинхронных JS-программах часто присутствует гораздо больше шума, загромождающего вещи, которым мы должны ловко маневрировать в своем мозгу, когда переходим от одной функции к другой. Понимание асинхронного потока в таком перегруженном обратными вызовами коде не является невозможным, но это определенно не естественно и не легко, даже при большой практике. Но также есть и более глубокая ошибка, которая не очевидна только в этом примере кода. Позвольте мне придумать другой сценарий (псевдокодовый), чтобы проиллюстрировать его: ```js doA( function(){ doB(); doC( function(){ doD(); } ) doE(); } ); doF(); ``` Хотя опытные из вас правильно определят здесь истинный порядок операций, я держу пари, что на первый взгляд он более чем немного сбивает с толку, и для его достижения требуется несколько согласованных умственных циклов. Операции будут происходить в таком порядке: * `doA()` * `doF()` * `doB()` * `doC()` * `doE()` * `doD()` Вы поняли это правильно, когда впервые взглянули на код? Хорошо, некоторые из вас думают, что я несправедливо назвал свои функции, чтобы намеренно ввести вас в заблуждение. Клянусь, я просто называл в порядке появления сверху вниз. Но позвольте мне попробовать еще раз: ```js doA( function(){ doC(); doD( function(){ doF(); } ) doE(); } ); doB(); ``` Теперь я назвал их в алфавитном порядке в порядке фактического выполнения. Но я все еще держу пари, даже имея опыт в этом сценарии, отслеживание в порядке «A -> B -> C -> D -> E -> F» не является естественным для многих из вас, читатели. Конечно, ваши глаза очень много прыгают вверх и вниз по фрагменту кода, верно? Но даже если все это кажется вам естественным, есть еще одна опасность, которая может нанести ущерб. Можете ли вы определить, что это такое? Что, если `doA(..)` или `doD(..)` на самом деле не являются асинхронными, как мы, очевидно, предполагали? Ой, теперь порядок другой. Если они оба синхронизированы (и, возможно, только иногда, в зависимости от условий программы в то время), порядок теперь будет «A -> C -> D -> F -> E -> B». Тот звук, который вы только что услышали на заднем плане, — это вздохи тысяч JS-разработчиков, которые только что столкнулись лицом к лицу. Является ли вложение проблемой? Из-за этого так сложно отследить асинхронный поток? Это часть этого, конечно. Но позвольте мне переписать предыдущий пример вложенного события/тайм-аута/Ajax без использования вложенности: ```js listen( "click", handler ); function handler() { setTimeout( request, 500 ); } function request(){ ajax( "http://some.url.1", response ); } function response(text){ if (text == "hello") { handler(); } else if (text == "world") { request(); } } ``` Эта формулировка кода едва ли так узнаваема, как проблемы вложенности/отступов в его предыдущей форме, и все же она ничуть не менее подвержена «аду обратных вызовов». Почему? По мере того, как мы переходим к линейному (последовательному) анализу этого кода, мы должны переходить от одной функции к следующей, к следующей и перемещаться по кодовой базе, чтобы «увидеть» поток последовательности. И помните, это упрощенный код в наилучшем случае. Все мы знаем, что базы кода реальных асинхронных JS-программ зачастую фантастически более запутаны, что на несколько порядков усложняет такие рассуждения. Еще одна вещь, на которую следует обратить внимание: чтобы связать шаги 2, 3 и 4 вместе, чтобы они выполнялись последовательно, единственный доступный способ, которые обратные вызовы дают нам, — это жестко закодировать шаг 2 в шаг 1, шаг 3 в шаг 2, шаг 4 в шаг 3, и так далее. Жесткое кодирование не обязательно плохо, если это действительно фиксированное условие, что шаг 2 всегда должен вести к шагу 3. Но жесткое кодирование определенно делает код немного более хрупким, поскольку оно не учитывает никаких ошибок, которые могут вызвать отклонение в последовательности шагов. Например, если шаг 2 терпит неудачу, шаг 3 никогда не достигается, и шаг 2 не повторяется, или выполняется переход к альтернативному потоку обработки ошибок и т.д. Все эти проблемы вы *можете* вручную жестко прописать в каждом шаге, но этот код часто очень повторяющийся и не может быть повторно использован на других шагах или в других асинхронных потоках в вашей программе. Несмотря на то, что наш мозг может планировать ряд задач в последовательном порядке (это, затем это, затем это), событийный характер работы нашего мозга делает восстановление/повторение/разветвление управления потоком практически без усилий. Если вы бегаете по делам и понимаете, что оставили дома список покупок, день не заканчивается, потому что вы не спланировали это заранее. Ваш мозг легко обходит эту заминку: вы идете домой, берете список и сразу же направляетесь обратно в магазин. Но хрупкая природа жестко закодированных вручную обратных вызовов (даже с жестко запрограммированной обработкой ошибок) часто гораздо менее изящна. Как только вы в конечном итоге укажете (также известное как предварительное планирование) все различные возможности/пути, код станет настолько запутанным, что его трудно будет поддерживать или обновлять. **Вот это** и есть "ад обратных вызовов"! Вложение/отступы - это, по сути, второстепенное шоу, отвлекающий маневр. И как будто всего этого недостаточно, мы даже не коснулись того, что происходит, когда две или более цепочек этих продолжений обратного вызова происходят *одновременно*, или когда третий шаг разветвляется на «параллельные» обратные вызовы с воротами или защелками, или... ОМГ, у меня мозг болит, а у тебя?! Улавливаете ли вы здесь, что наше последовательное, блокирующее поведение планирования мозга просто не очень хорошо отображается в асинхронном коде, ориентированном на обратных вызовах? Это первый существенный недостаток обратных вызовов, о котором нужно сказать: они выражают асинхронность в коде таким образом, что нашему мозгу приходится бороться только за то, чтобы синхронизироваться с ним (каламбур!). ## Проблемы с доверием Несоответствие между последовательным мозговым планированием и асинхронным кодом JS, управляемым обратными вызовами, — это только часть проблемы с обратными вызовами. Есть кое-что гораздо более глубокое, о чем следует беспокоиться. Давайте еще раз вернемся к понятию callback-функции как продолжения (она же вторая половина) нашей программы: ```js // A ajax( "..", function(..){ // C } ); // B ``` `// A` и `// B` происходят *сейчас* под непосредственным контролем основной JS-программы. Но `// C` откладывается, чтобы произойти *позже*, и находится под контролем другой стороны – в данном случае, функции `ajax(..)`. В общем смысле такая передача управления обычно не вызывает много проблем для программ. Но пусть вас не вводит в заблуждение его редкость, что этот переключатель управления не имеет большого значения. На самом деле, это одна из самых серьезных (и в то же время наиболее тонких) проблем проектирования, основанного на обратных вызовах. Он вращается вокруг идеи, что иногда `ajax(..)` (т.е. "сторона", которой вы передаете продолжение обратного вызова) не является функцией, которую вы написали или которой вы непосредственно управляете. Часто это утилита, предоставляемая третьей стороной. Мы называем это «инверсией управления (IoC)», когда вы берете часть своей программы и передаете контроль над ее выполнением другому третьему лицу. Между вашим кодом и сторонней утилитой существует негласный «контракт» — набор вещей, которые вы ожидаете поддерживать. ### Сказка о пяти обратных вызовах Может быть не совсем очевидно, почему это так важно. Позвольте мне построить преувеличенный сценарий, чтобы проиллюстрировать опасности доверия в игре. Представьте, что вы разработчик, которому поручено создать систему оплаты электронной торговли для сайта, продающего дорогие телевизоры. У вас уже есть все различные страницы системы оформления заказов, созданные просто отлично. На последней странице, когда пользователь нажимает «подтвердить», чтобы купить телевизор, вам нужно вызвать стороннюю функцию (предоставленную, скажем, какой-то аналитической компанией), чтобы можно было отследить продажу. Вы заметили, что они предоставили то, что выглядит как утилита асинхронного отслеживания, вероятно, ради лучших практик производительности, что означает, что вам нужно передать функцию обратного вызова. В этом продолжении, которое вы передаете, у вас будет окончательный код, который снимает средства с кредитной карты клиента и отображает страницу благодарности. Этот код может выглядеть так: ```js analytics.trackPurchase( purchaseData, function(){ chargeCreditCard(); displayThankyouPage(); } ); ``` Достаточно легко, верно? Вы пишете код, тестируете его, все работает, и вы запускаете его в производство. Все счастливы! Прошло полгода и никаких проблем. Вы почти забыли, что даже написали этот код. Однажды утром вы сидите в кофейне перед работой, небрежно наслаждаясь латте, когда вам звонит в панике начальник и настаивает, чтобы вы бросили кофе и сразу же бросились на работу. Когда вы приедете, вы узнаете, что с кредитной карты высокопоставленного клиента пять раз списали средства за один и тот же телевизор, и он, по понятным причинам, расстроен. Служба поддержки клиентов уже принесла извинения и обработала возврат средств. Но ваш босс требует знать, как это могло произойти. «Разве у нас нет тестов на подобные вещи!?» Вы даже не помните код, который вы написали. Но вы копаетесь и начинаете пытаться выяснить, что могло пойти не так. Покопавшись в некоторых логах, вы приходите к выводу, что единственное объяснение в том, что утилита аналитики каким-то образом по какой-то причине вызвала ваш обратный вызов пять раз вместо одного. Ничто в их документации не упоминает ничего об этом. Разочарованный, вы связываетесь со службой поддержки, которая, конечно, так же удивлена, как и вы. Они соглашаются сообщить об этом своим разработчикам и обещают вернуться к вам. На следующий день вы получаете длинное электронное письмо с объяснением того, что они нашли, и сразу же пересылаете его своему боссу. Судя по всему, разработчики аналитической компании работали над некоторым экспериментальным кодом, который при определенных условиях будет повторять предоставленный обратный вызов один раз в секунду в течение пяти секунд, прежде чем произойдет сбой с тайм-аутом. Они никогда не собирались продвигать это в производство, но каким-то образом они это сделали, и они полностью смущены и извиняются. Они подробно рассказывают о том, как они определили сбой и что они сделают, чтобы это никогда не повторилось. Что дальше? Вы обсуждаете это со своим боссом, но он не чувствует себя особенно довольным положением вещей. Он настаивает, и вы неохотно соглашаетесь, что вы не можете больше доверять *им* (это то, что вас задело), и что вам нужно снова придумать, как защитить код проверки от такой уязвимости. После некоторой доработки вы реализуете простой специальный код, подобный приведенному ниже, который, похоже, устраивает команду: ```js var tracked = false; analytics.trackPurchase( purchaseData, function(){ if (!tracked) { tracked = true; chargeCreditCard(); displayThankyouPage(); } } ); ``` **Примечание:** Это должно показаться вам знакомым из главы 1, потому что мы, по сути, создаем защелку для обработки, если произойдет несколько одновременных вызовов нашего обратного вызова. Но затем один из ваших QA-инженеров спрашивает: «Что произойдет, если они никогда не вызовут обратный вызов?» Упс. Никто из вас не думал об этом. Вы начинаете искать кроличью нору и думаете обо всех возможных вещах, которые могут пойти не так, если они перезвонят вам. Вот примерный список возможных сбоев в работе аналитической утилиты: * Вызывать обратный вызов слишком рано (до того, как он будет отслежен) * Вызов обратного вызова слишком поздно (или никогда) * Вызовите обратный вызов слишком мало или слишком много раз (например, проблема, с которой вы столкнулись!) * Не удалось передать любую необходимую среду/параметры вашему обратному вызову * Проглотить любые ошибки/исключения, которые могут произойти * ... Это должно показаться тревожным списком, потому что так оно и есть. Вы, вероятно, постепенно начинаете понимать, что вам придется изобретать очень много специальной логики **в каждом обратном вызове**, которая передается утилите, которой вы не уверены, что можете доверять. Теперь вы немного более полно осознаете, насколько адским является «ад обратных вызовов». ### Не только чужой код Некоторые из вас могут сейчас скептически отнестись к тому, настолько ли это важно, как я это преподношу. Возможно, вы мало взаимодействуете с действительно сторонними утилитами. Возможно, вы используете версионные API или самостоятельно размещаете такие библиотеки, чтобы их поведение нельзя было изменить без вас. Итак, подумайте над этим: можете ли вы вообще *действительно* доверять утилитам, которыми вы теоретически управляете (в своей собственной кодовой базе)? Подумайте об этом так: большинство из нас согласны с тем, что, по крайней мере, в некоторой степени мы должны создавать свои собственные внутренние функции с некоторыми защитными проверками входных параметров, чтобы уменьшить/предотвратить непредвиденные проблемы. Чрезмерное доверие входным данным: ```js function addNumbers(x,y) { // + перегружен приведением, // чтобы также быть конкатенацией строк, // поэтому эта операция не является строго безопасной // в зависимости от того, что передано. return x + y; } addNumbers( 21, 21 ); // 42 addNumbers( 21, "21" ); // "2121" ``` Защита от ненадежного ввода: ```js function addNumbers(x,y) { // обеспечиваем числовой ввод if (typeof x != "number" || typeof y != "number") { throw Error( "Bad parameters" ); } // если мы доберемся сюда, + безопасно выполнит числовое сложение return x + y; } addNumbers( 21, 21 ); // 42 addNumbers( 21, "21" ); // Error: "Bad parameters" ``` Или, возможно, все еще безопасно, но дружелюбнее: ```js function addNumbers(x,y) { // обеспечиваем числовой ввод x = Number( x ); y = Number( y ); // + будет безопасно делать числовое сложение return x + y; } addNumbers( 21, 21 ); // 42 addNumbers( 21, "21" ); // 42 ``` Как бы вы это ни делали, такого рода проверки/нормализации довольно распространены на входных данных функций, даже с кодом, которому мы теоретически полностью доверяем. Грубо говоря, это похоже на программный эквивалент геополитического принципа «Доверяй, но проверяй». Итак, не разумно ли, что мы должны делать то же самое с композицией обратных вызовов асинхронных функций, не только с действительно внешним кодом, но даже с кодом, который, как мы знаем, обычно «находится под нашим собственным контролем»? **Конечно, должны.** Но обратные вызовы на самом деле не предлагают ничего, чтобы помочь нам. Мы должны создавать весь этот механизм сами, и часто это приводит к большому количеству шаблонов/накладных расходов, которые мы повторяем для каждого отдельного асинхронного обратного вызова. Самая неприятная проблема с обратными вызовами — это *инверсия управления*, ведущая к полному разрыву всех этих линий доверия. Если у вас есть код, который использует обратные вызовы, особенно, но не исключительно, со сторонними утилитами, и вы еще не применяете какую-то логику смягчения для всех этих *инверсии управления* проблем с доверием, ваш код *имеет* ошибки в нем прямо сейчас, хотя они, возможно, еще не укусили вас. Скрытые ошибки остаются ошибками. Действительно ад. ## Попытка сохранить обратные вызовы Существует несколько вариантов дизайна обратного вызова, которые пытались решить некоторые (не все!) проблемы доверия, которые мы только что рассмотрели. Это доблестная, но обреченная попытка спасти шаблон обратного вызова от разрушения самого себя. Например, что касается более изящной обработки ошибок, некоторые проекты API предусматривают раздельные обратные вызовы (один для уведомления об успехе, другой для уведомления об ошибке: ```js function success(data) { console.log( data ); } function failure(err) { console.error( err ); } ajax( "http://some.url.1", success, failure ); ``` В API такого дизайна часто обработчик ошибок `failure()` является необязательным, и если он не указан, предполагается, что вы хотите проглотить ошибки. Фу. **Примечание.** Этот дизайн с разделенным обратным вызовом используется API ES6 Promise. Мы рассмотрим обещания ES6 более подробно в следующей главе. Другой распространенный шаблон обратного вызова называется «стиль с ошибкой в первую очередь» (иногда его также называют «стилем узла», поскольку это также соглашение, используемое почти во всех API-интерфейсах Node.js), где первый аргумент одного обратного вызова зарезервирован для объекта ошибки (если есть). В случае успеха этот аргумент будет пустым/ложным (и любые последующие аргументы будут данными об успехе), но если сигнализируется результат ошибки, первый аргумент устанавливается/истинный (и обычно больше ничего не передается): ```js function response(err,data) { // ошибка? if (err) { console.error( err ); } // в иных случаях успех else { console.log( data ); } } ajax( "http://some.url.1", response ); ``` В обоих этих случаях следует соблюдать несколько вещей. Во-первых, это на самом деле не решило большинство проблем с доверием, как может показаться. В обратном вызове нет ничего, что предотвращало бы или отфильтровывало нежелательные повторные вызовы. Более того, сейчас дела обстоят еще хуже, потому что вы можете получить как сигналы об успехе, так и ошибки, или ни одного из них, и вам все равно придется кодировать любое из этих условий. Кроме того, не упустите тот факт, что, хотя это стандартный шаблон, который вы можете использовать, он определенно более многословен и похож на шаблон без особого повторного использования, поэтому вы устанете вводить все это для каждого обратного вызова в вашем приложении. А как насчет проблемы доверия никогда не быть вызванным? Если это вызывает беспокойство (и, вероятно, должно быть!), вам, вероятно, потребуется установить тайм-аут, который отменяет событие. Вы можете сделать утилиту (показано только доказательство концепции), которая поможет вам в этом: ```js function timeoutify(fn,delay) { var intv = setTimeout( function(){ intv = null; fn( new Error( "Timeout!" ) ); }, delay ) ; return function() { // timeout уже произошёл? if (intv) { clearTimeout( intv ); fn.apply( this, [ null ].concat( [].slice.call( arguments ) ) ); } }; } ``` Вот как можно его использовать: ```js // использование обратного вызова в стиле «сначала ошибка» function foo(err,data) { if (err) { console.error( err ); } else { console.log( data ); } } ajax( "http://some.url.1", timeoutify( foo, 500 ) ); ``` Еще одна проблема доверия называется «слишком рано». С точки зрения конкретного приложения это может фактически включать вызов до завершения какой-либо критической задачи. Но в более общем плане проблема проявляется в утилитах, которые могут либо вызывать предоставленный вами обратный вызов *сейчас* (синхронно) или *позже* (асинхронно). Этот недетерминизм в отношении синхронного или асинхронного поведения почти всегда приводит к тому, что очень трудно отследить ошибки. В некоторых кругах вымышленный вызывающий безумие монстр по имени Залго используется для описания синхронно-асинхронных кошмаров. «Не выпускайте Залго!» — это распространенный крик, и он приводит к очень здравому совету: всегда вызывайте обратные вызовы асинхронно, даже если это «сразу же» на следующем этапе цикла событий, чтобы все обратные вызовы предсказуемо были асинхронными. **Примечание.** Дополнительную информацию о Zalgo см. в статье Орена Голана «Не выпускайте Zalgo!» (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) и «Разработка API-интерфейсов для асинхронности» Исаака З. Шлютера (http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony). Рассмотрим: ```js function result(data) { console.log( a ); } var a = 0; ajax( "..pre-cached-url..", result ); a++; ``` Будет ли этот код печатать «0» (вызов обратного вызова синхронизации) или «1» (вызов асинхронного обратного вызова)? Зависит... от условий. Вы видите, как быстро непредсказуемость Zalgo может угрожать любой JS-программе. Таким образом, глупо звучащее «никогда не выпускайте Zalgo» на самом деле является невероятно распространенным и надежным советом. Всегда быть асинхронным. Что, если вы не знаете, всегда ли рассматриваемый API будет выполняться асинхронно? Вы можете изобрести утилиту, подобную этой `asyncify(..)`, для доказательства концепции: ```js function asyncify(fn) { var orig_fn = fn, intv = setTimeout( function(){ intv = null; if (fn) fn(); }, 0 ) ; fn = null; return function() { // срабатывает слишком быстро, прежде чем сработает таймер // `intv`, чтобы показать, что асинхронный ход прошел? if (intv) { fn = orig_fn.bind.apply( orig_fn, // добавляем `this` обёртки к `bind(..)` // параметры вызова, а также каррирование любых // передается в параметрах [this].concat( [].slice.call( arguments ) ) ); } // уже асинхронно else { // вызвать исходную функцию orig_fn.apply( this, arguments ); } }; } ``` Можно использовать `asyncify(..)` следующим образом: ```js function result(data) { console.log( a ); } var a = 0; ajax( "..pre-cached-url..", asyncify( result ) ); a++; ``` Независимо от того, находится ли запрос Ajax в кеше и разрешает попытку немедленного вызова обратного вызова, или его нужно получить по сети и, таким образом, выполнить позже асинхронно, этот код всегда будет выводить `1` вместо `0` -- `result(..)` не может не вызываться асинхронно, что означает, что `a++` имеет шанс запуститься раньше, чем `result(..)`. Ура, еще одна проблема доверия решена! Но это неэффективно и еще больше раздувает шаблон, чтобы отягощать ваш проект. Это просто история, снова и снова, с обратными вызовами. Они могут делать почти все, что вы хотите, но вы должны быть готовы много работать, чтобы получить это, и часто эти усилия намного больше, чем вы можете или должны потратить на такие рассуждения о коде. Возможно, вам понадобятся встроенные API или другие языковые механизмы для решения этих проблем. Наконец-то появился ES6 с отличными ответами, так что продолжайте читать! ## Обзор Обратные вызовы — это фундаментальная единица асинхронности в JS. Но их недостаточно для меняющегося ландшафта асинхронного программирования по мере взросления JS. Во-первых, наш мозг планирует вещи последовательным, блокирующим, однопоточным семантическим способом, но обратные вызовы выражают асинхронный поток довольно нелинейным, непоследовательным образом, что значительно усложняет правильное рассмотрение такого кода. Плохие рассуждения о коде — это плохой код, который приводит к серьезным ошибкам. Нам нужен способ выразить асинхронность в более синхронной, последовательной, блокирующей манере, как это делает наш мозг. Во-вторых, что более важно, обратные вызовы страдают от *инверсии управления*, поскольку они неявно передают управление другой стороне (часто сторонней утилите, не находящейся под вашим контролем!) для вызова *продолжения* вашей программы. Эта передача управления приводит нас к тревожному списку проблем с доверием, например, вызывается ли обратный вызов чаще, чем мы ожидаем. Изобретение специальной логики для решения этих проблем с доверием возможно, но это сложнее, чем должно быть, и в результате получается более громоздкий и трудный для сопровождения код, а также код, который, вероятно, недостаточно защищен от этих опасностей, пока вы явно не укусите его ошибки. Нам нужно обобщенное решение **всех проблем с доверием**, которое можно будет повторно использовать для любого количества обратных вызовов, которое мы создадим, без дополнительных шаблонных накладных расходов. Нам нужно что-то лучше, чем обратные вызовы. До сих пор они хорошо служили нам, но «будущее» JavaScript требует более сложных и эффективных асинхронных шаблонов. В последующих главах этой книги мы углубимся в эти зарождающиеся эволюции. ================================================ FILE: async & performance/ch3.md ================================================ # Вы не знаете JS: Асинхронность и производительность # Глава 3: Промисы В главе 2 мы определили две главные категории недостатков в использовании колбеков для того, чтобы выразить асинхронность программы и управлять параллельной обработкой: отсутствие последовательного кода и надежности. Теперь, когда мы разобрали эти проблемы досконально, настало время обратить внимание на шаблоны, которые позволят их решить. Проблема, с которой мы хотим начать - *инверсия управления (IoC)*, это то доверие, которое так тяжело сохранять и так легко потерять. Вспомните как мы обработали *продолжение (continuation)* нашей программы в колбек-функции и передали этот колбек в другую часть кода (потенциально даже во внешний код) и просто скрестили пальцы на удачу, что вызов этого колбэка произойдет корректно. Мы сделали это поскольку мы хотели этим сказать, "вот то, что должно выполниться *позже*, после того, как завершится текущий шаг программы." Но что если бы мы смогли разинверсировать эту *инверсию управления*? Что если вместо передачи продолжения программы в другой код мы могли бы ожидать от него дать нам возможность узнать когда его задача завершится и затем наш код мог бы решить, что делать дальше? Эта концепция называется **Промисы**. Промисы начинают быстро покорять мир JS, поскольку разработчики и создатели спецификаций в равной мере отчаянно ищут возможность избавиться от безумия ада колбеков в своим коде/дизайне. На самом деле, многие новые асинхронные API добавляются в платформу JS/DOM будучи построенным на промисах. Так что, возможно, это неплохая идея уйти с головой и изучить их, Вы так не думаете!? **Примечание:** Слово "сразу" будет часто использоваться в этой главе, в основном указывая на какое-либо действие по разрешению промиса. Однако, в фактически всех случаях, "сразу" в терминах поведения очереди заданий означает (см. главу 1) не строго синхронное значение *сейчас*. ## Что такое промис? Когда разработчики решают изучить новую технологию или шаблон, обычно их первым шагом будет "Покажите мне код!". Это вполне естественно для нас просто броситься в омут с головой и изучать по ходу. Однако бывает так, что одних только API недостаточно, чтобы понять определённые абстракции. Промисы — это как раз тот инструмент, где по тому, как их используют, сразу видно: понимает ли человек, для чего они нужны и как работают, или просто изучил и использует API. Так что перед тем как я покажу код с промисами, я хочу объяснить целиком и полностью что такое промисы концептуально. Я надеюсь, что это быстрее направит вас на истинный путь по мере того как вы будете постигать теорию интеграции промисов в свой асинхронный поток. Помня об этом, давайте взглянем на две различные аналогии того что *есть* промисы. ### Будущее значение Представьте такой сценарий: Я подхожу к стойке в ресторане быстрого питания и делаю заказ на чизбургер. Я даю кассиру $1.47. Разместив и оплатив свой заказ, я сделал запрос возврата *значения* (чизбургера). Я открыл транзакцию. Но частенько, чизбургер не сразу мне доступен. Кассир даем мне что-то взамен моего чизбургера: чек с номером заказа в нем. Этот номер заказа - это IOU-*обещание* (промис) (Я должен вам ("I owe you")) которое гарантирует, что в итоге я должен получить свой чизбургер. Так что я храню мой чек и номер заказа. Я знаю, что оно представляет собой мой *будущий чизбургер*, поэтому мне не надо о этом больше волноваться кроме ощущения голода! Пока я жду Я могу заниматься другими делами, например отправить текстовое сообщение другу, говорящее: "Эй, как насчет присоединиться ко мне за обедом? Я собираюсь съесть чизбургер." Я уже рассуждаю о своем *будущем чизбургере*, несмотря на то, что у меня еще его нет. Мой мозг способен так делать потому что он воспринимает номер заказа как заменитель чизбургера. Такой заменитель фактически делает значение *независимым от времени*. Это и есть **будущее значение**. В итоге я слышу "Заказ 113!" и радостно иду обратно к стойке с чеком в руках. Я передаю чек кассиру и взамен беру свой чизбургер. Другими словами, как только мое *будущее значение* было готово, я обменял промис значения на само значение. Но есть и еще один возможный исход. Они называют мой номер заказа, но когда я подхожу, чтобы забрать свой чизбургер кассир с сожалению сообщает мне: "Мне жаль, но мы судя по всему остались без чизбургеров." Оставив в стороне разочарование покупателя в таком сценарии на секундочку, мы можем заметить важную характеристику *будущих значений*: они могут сигнализировать либо о завершении, либо об отказе. Каждый раз, когда я заказываю чизбургер, я знаю что либо я рано или поздно получу чизбургер, либо получу печальные новости о нехватке чизбургеров и я должен буду придумать что-то другое на обед. **Примечание:** В реальном коде процесс не такой простой, потому что метафорически номер заказа может никогда не быть назван, и в таком случае мы остаемся в неразрешим состоянии вечно. Мы еще вернемся к этому случаю позже. #### Значения Сейчас и Позже Всё это может прозвучать слишком абстрактно для применения в вашем коде. Так давайте внесем больше конкретики. Однако, перед тем, как мы представим как работают промисы подобным образом, мы собираемся покопаться в коде, который мы уже умеем понимать -- колбеки! -- чтобы понять как обрабатывать эти *будущие значения*. Когда вы пишете код, чтобы обработать каким-либо образом некое значение, например выполнив математические вычисления над `числом`, то осознанно или нет, вы предполагаете кое-что очень фундаментальное об этом значении, а именно, что это - уже конкретное значение *сейчас*: ```js var x, y = 2; console.log( x + y ); // NaN <-- потому что в `x` еще не установлено значение ``` Операция `x + y` предполагает, что оба `x` и `y` уже заданы. В терминах, которые мы вскоре разъясним, мы полагаем что значения `x` и `y` уже *разрешены* (т.е. с уж определенными значениями). Будет абсурдом ожидать, что оператор `+` сам по себе каким-то магическим образом сможет определить и ждать до тех пор, пока оба `x` и `y` разрешатся (т.е. будут готовы), и только затем выполнит операцию. Это может привести к хаосу в программе, если одни выражения закончатся *сейчас*, а другие закончатся *позже*, не так ли? Как вы сможете потенциально рассуждать о связях между двумя выражениями, если одно из них (или оба) могут быть еще не завершены? Если выражение 2 полагается на то, что выражение 1 будет завершено, то возможны два исхода: либо выражение 1 закончится прямо *сейчас* и всё благополучно продолжится, либо statement 1 еще не завершено, и в итоге выражение 2 приведет к ошибке. Если такие вещи звучат знакомо после главы 1, хорошо! Давайте вернемся к нашей математической операции `x + y`. Представьте, что был бы путь сказать, "Сложи `x` и `y`, но если кто-то из них еще не подготовлен, просто подожди пока это не произойдет. Сложи их как можно скорее." Ваш мозг возможно сразу переключился на колбеки. Хорошо, итак... ```js function add(getX,getY,cb) { var x, y; getX( function(xVal){ x = xVal; // оба готовы? if (y != undefined) { cb( x + y ); // отправить сумму } } ); getY( function(yVal){ y = yVal; // оба готовы? if (x != undefined) { cb( x + y ); // отправить сумму } } ); } // `fetchX()` and `fetchY()` синхронные или асинхронные // функции add( fetchX, fetchY, function(sum){ console.log( sum ); // это было легко, ага? } ); ``` Подождите минутку, чтобы позволить красоте (или отсутствию таковой) этого кусочка кода уложиться в голове (терпеливо насвистываю). Хотя это уродство и несомненное, тут есть кое-что очень важное на заметку об этом асинхронном шаблоне. В этом кусочке кода, мы трактовали `x` и `y` как будущие значения и мы выразили операцию `add(..)` так, что она (снаружи) не заботится о том, доступен ли `x` или `y` прямо сейчас или нет. Другими словами, он нормализует *сейчас* и *потом* таким образом, что мы можем положиться на предсказуемый результат операции `add(..)`. При использовании `add(..)`, которая временно консистентна, она ведет себя одинаково *сейчас* и *потом* - такой асинхронный код легче себе представлять. Выражаясь более просто: чтобы обработать согласованно оба *сейчас* и *потом*, мы оба их превращаем в *потом*: все операции становятся асинхронными. Конечно, этот грубый подход, основанный на колбеках, оставляет желать намного лучшего. Это всего-лишь первый крошечный шаг к пониманию преимуществ представления *будущих значений* без беспокойств о временном аспекте того, доступны они или нет. #### Промис как значение Мы определенно углубимся в детали промисов позже в этой главе, поэтому не волнуйтесь если что-то тут покажется запутанным, а просто мельком взгляните на то, как мы выразим пример `x + y` через `Promise`ы: ```js function add(xPromise,yPromise) { // `Promise.all([ .. ])` принимает массив промисов // и возвращает новый промис, который ожидает завершения всех переданных return Promise.all( [xPromise, yPromise] ) // когда промис разрешен, давайте возьмем // полученные значения `X` и `Y` и сложим их. .then( function(values){ // `values` - массив сообщений от // ранее разрешенных промисов return values[0] + values[1]; } ); } // `fetchX()` и `fetchY()` возвращают промисы для // своих соответствующих значений, которые могут быть готовы // *сейчас* или *позже*. add( fetchX(), fetchY() ) // мы получаем обратно промис с суммой этих // двух чисел. // теперь мы выполняем в цепочке вызов `then(..)`, чтобы дождаться разрешения // этого возвращенного промиса. .then( function(sum){ console.log( sum ); // это намного легче! } ); ``` В этом кусочке кода есть два слоя промисов. `fetchX()` и `fetchY()` вызываются напрямую и возвращаемые или значения (промисы!) передаются в `add(..)`. Внутренние значения, которые представляют эти промисы, могут быть готовы *сейчас* или *позже*, но каждый промис приводит свое поведение к тому, чтобы вести себя одинаково вне зависимости ни о чего. Мы рассуждаем о значениях `X` и `Y` во время-независимой манере. Они - *будущие значения*. Второй уровень - это промис, который создается в `add(..)` (через `Promise.all([ .. ])`) и возвращается, и который мы ожидаем вызвав `then(..)`. Когда операция `add(..)` завершена, наше *будущее значение* `sum` готово и можем вывести его на экран. Внутри `add(..)` мы скрываем всю логику ожидания *будущих значений* `X` и `Y`. **Примечание** Внутри `add(..)`, вызов `Promise.all([ .. ])` создает промис (который ждем разрешения `promiseX` и `promiseY`). Цепочечный Вызов `.then(..)` создает еще один промис, который сразу же разрешает строку `return values[0] + values[1]` (с результатом сложения). Таким образом, вызов `then(..)`, который мы поместили в конец цепочки вызова `add(..)` в конце фрагмента кода, в сущности работает с этим вторым возвращенным промисом, а не с первым, созданным `Promise.all([ .. ])`. Также, хотя мы и не добавили ничего в конец цепочки второго `then(..)`, он также создает еще один промис, невзирая на то, хотим мы его использовать или нет. Эту штуку с цепочками промисов мы поясним в деталях позже в этой главе. Прямо как с заказами чизбургеров, есть такая же вероятность того, что промис разрешится отказом вместо исполнения. В отличие от исполненного промиса, где значение всегда программно задано, значение при отказе, обычно называемое "причиной отказа", может быть установлено либо напрямую в логике программы, либо может явиться результатом исключения времени выполнения. С использованием промисов, вызов `then(..)` фактически может принимать две функции: первую - для завершения (как показано ранее), а вторую - для отказа: ```js add( fetchX(), fetchY() ) .then( // обработчик завершения function(sum) { console.log( sum ); }, // обработчик отказа function(err) { console.error( err ); // облом! } ); ``` Если что-то пошло не так при получении `X` или `Y`, или что-то каким-либо образом привело к сбою во время сложения, промис, который возвращается из `add(..)` - отвергается (завершается отказом) и второй колбек-обработчик ошибок, переданный в `then(..)` получит значение отказа из промиса. Поскольку промисы инкапсулируют в себе состояние, не зависящее от времени, с ожиданием завершения или отказа получения значения операции снаружи, промис сам по себе является независимым от времени и потому промисы можно компоновать (составлять) предсказуемым образом независимо от времени или внутреннего результата. Более того, как только промис разрешен, он остается таковым навсегда, он становится *неизменяемым значением* в этот момент и может потом быть *обследован* столько раз, сколько нужно. **Примечание** Поскольку промис является неизменяемым внешне как только он разрешен, то теперь можно безопасно передавать его куда угодно зная, что он не может быть изменен случайно или злонамеренно. Это особенно верно в связи с тем, что наблюдать за разрешением одного и того же промиса могут разные стороны. Невозможно повлиять на возможность одной стороны наблюдать за разрешением промиса другой стороной. Неизменяемость может прозвучать как какая-то научная тема, но на самом деле это один из самых фундаментальных и важных аспектов дизайна промисов и не должен быть рассмотрен походя мимоходом. Это один из самых мощных и важных ключей к пониманию промисов. Проделав достаточно большую работу, вы могли бы специально добиться того же эффекта используя только композицию из уродливых колбеков, но это не особенно эффективная стратегия, особенно потому, что вы вынуждены делать это снова и снова. Промисы - это легко повторяемый механизм инкапсуляции и совмещения *будущих значений*. ### Событие завершения Как мы только что видели, одиночный промис ведет себя как *будущее значение*. Но есть и другой путь представлять разрешение промиса: как механизм потокового управления, временнОе "это-затем-то" для двух и более шагов в асинхронной задаче. Давайте представим вызов функции `foo(..)` для выполнения некой задачи. Мы либо не знаем ничего о ее внутренней реализации, или просто не беспокоимся об этом. Она может завершить задачу сразу или может занять некоторое время. Нам просто нужно знать когда завершится `foo(..)`, чтобы мы могли двигаться к нашей следующей задаче. Другими словами, нам нужна возможность получить оповещение о завершении `foo(..)`, чтобы мы могли *продолжить* выполнение. В обычном случае в JavaScript если вам нужно получить оповещение, вы вероятно подумаете об этом с точки зрения событий. Таким образом мы можем переформулировать нашу потребность в оповещении как необходимость получить событие *завершения* (или *продолжения*), инициированное функцией `foo(..)`. **Примечание** Назовете ли вы это "событием завершения" или "событием продолжения" зависит от вашей точки зрения. На чем больше смещен фокус: на том что случится в `foo(..)` или на том что произойдет *после* завершения `foo(..)`? Обе точки зрения точны и полезны. Уведомление о событии сообщает нам, что `foo(..)` *завершилась*, но также и то, что можно *продолжить* выполнение следующего шага. Безусловно, тот колбек, который вы передаете, чтобы он был вызван для уведомления о событии, сам по себе то, что мы ранее назвали *продолжение*. Потому что *событие завершения* немного более сфокусировано на `foo(..)`, что больше привлекает наше внимание в настоящий момент, мы все же чуть больше отдаем предпочтение *событию завершения* до конца этого текста. С использованием колбеков, "уведомлением" будет наш колбек, вызванный задачей (`foo(..)`). Но с промисами, мы переворачиваем отношения и ожидаем, что можем ждать событие от `foo(..)` и как только получим его может действовать соответственно. Сперва, обратите внимание на этот псевдокод: ```js foo(x) { // начинаем выполнять что-то, что требует времени } foo( 42 ) on (foo "completion") { // теперь мы можем выполнить следующий шаг! } on (foo "error") { // ой, что-то пошло не так в `foo(..)` } ``` Мы вызываем `foo(..)`, а затем настраиваем два обработчика событий, один для `"completion"` (завершение), а другой для `"error"` (сбоя)-- двух возможных *окончательных* исхода вызова `foo(..)`. По сути, не похоже, что `foo(..)` вообще в курсе о том, что вызывающий код подписался на эти события, что ведет к очень хорошему *разделению обязанностей*. К сожалению, такой код потребовал бы некоторой "магии" окружения JS, которое не существует (и которое бы весьма вероятно было бы немного непрактичным). Вот более естественный путь, которым мы может это выразить в JS: ```js function foo(x) { // начнем выполнять что-нибудь, требующее времени // создадим обработчик оповещения о событии `listener`, // чтобы его можно было вернуть из функции return listener; } var evt = foo( 42 ); evt.on( "completion", function(){ // теперь мы можем выполнить следующий шаг } ); evt.on( "failure", function(err){ // ой, что-то пошло не так в `foo(..)` } ); ``` `foo(..)` специально создает возможность подписки на события, которую можно вернуть из функции и вызывающий код получает и регистрирует два обработчик событий для нее. Инверсия обычного колбек-ориентированного кода должно быть очевидна и это намеренно. Вместо передачи колбеков `foo(..)`, она возвращает возможность получения событий, которую мы назвали `evt`, которая получает колбеки. Но если вы вспомните главу 2, колбеки сами по себе являются *инверсией управления* (IoC). Таким образом инвертируя шаблон колбека, в действительности получаем *инверсия инверсии* или *разинверсия управления*, возвращая управление обратно вызывающему коду, туда где оно должно было быть изначально. Одно важное преимущество - это то, что многим отдельным частях кода можно дать возможность получать события и они все смогут быть независимо уведомлены о том, когда завершится `foo(..)`, чтобы выполнить последующий код после ее завершения: ```js var evt = foo( 42 ); // пусть `bar(..)` получает уведомление о завершении `foo(..)` bar( evt ); // пусть `baz(..)` также получает уведомление о завершении `foo(..)` baz( evt ); ``` *Разинверсия управления* открывает возможность лучшего *разделения обязанностей*, где функциям `bar(..)` и `baz(..)` не нужно быть вовлеченными в то, как вызывается `foo(..)`. Аналогично, функции `foo(..)` не нужно ни знать, ни беспокоиться о том, что `bar(..)` и `baz(..)` существуют или ждут уведомления о завершении `foo(..)`. Фактически, это объект `evt` - это нейтральный сторонний посредник между отдельными функциональными обязанностями. #### "События" промиса Как вы можете теперь предположить, возможность получения события `evt` - это аналогия для промиса. В промисо-ориентированном подходе предыдущий блок кода мог бы содержать `foo(..)`, создающую и возвращающую экземпляр `Promise`, и этот промис был бы передан в `bar(..)` и `baz(..)`. **Примечание** "События" разрешения промиса, который мы ждем, не являются событиями в строгом смысле (хотя они определенно ведут себя как события в этом сценарии), и они не просто вызывают `"completion"` или `"error"`. Вместо этого, мы используем `then(..)`, чтобы зарегистрировать событие `"then"`. Или чуть более точно, `then(..)` регистрирует событие(я) `"fulfillment"` (выполнения) и/или `"rejection"` (отказа), хотя мы и не видим эти термины в явном виде в коде. Взгляните: ```js function foo(x) { // начнем выполнять что-нибудь, требующее времени // создает и возвращаем промис return new Promise( function(resolve,reject){ // в итоге вызвать `resolve(..)` или `reject(..)`, // которые являются колбеками разрешения для промиса. } ); } var p = foo( 42 ); bar( p ); baz( p ); ``` **Примечание** Шаблон, показанный с `new Promise( function(..){ .. } )` - обычно называется["открытый конструктор (revealing constructor)"](http://domenic.me/2014/02/13/the-revealing-constructor-pattern/). Переданная функция выполняется сразу (а не асинхронно отложенным вызовом, как колбеки в `then(..)`) и туда передаются два параметра, которые в этом случае называются `resolve` и `reject`. Это функции разрешения промиса. `resolve(..)` обычно сигнализирует о выполнении, а `reject(..)` - об отказе. Вы вероятно сможете угадать, как выглядят внутри `bar(..)` и `baz(..)`: ```js function bar(fooPromise) { // ждать завершения `foo(..)` fooPromise.then( function(){ // `foo(..)` теперь закончена, так что // выполняем задачу `bar(..)` }, function(){ // ой, что-то пошло не так в `foo(..)` } ); } // то же самое для `baz(..)` ``` Разрешение промиса не обязательно требует отправки сообщения, как это было когда мы исследовали промисы как *будущие значения*. Это может быть просто сигнал управления потоком, как это было в предыдущем блоке кода. Еще один путь добиться этого: ```js function bar() { // `foo(..)` определенно завершилась, поэтому // выполняем задачу `bar(..)` } function oopsBar() { // ой, что-то пошло не так в `foo(..)`, // поэтому `bar(..)` не был запущен } // то же самое для `baz()` и `oopsBaz()` var p = foo( 42 ); p.then( bar, oopsBar ); p.then( baz, oopsBaz ); ``` **Примечание** Если вы уже видели раньше промис-ориентированный код, у вас может возникнуть соблазн поверить, что две последних строки этого кода можно записать как `p.then( .. ).then( .. )`, используя цепочку вызовов вместо `p.then(..); p.then(..)`. Это было бы совершенно другое поведение, будьте осторожны! Прямо сейчас разница может быть неочевидна, но это на самом деле совершенно другой асинхронный шаблон, нежели мы видели до сих пор: разделение/разветвление. Не волнуйтесь! Мы вернемся к этому позже в этой главе. Вместо передачи промиса `p` в `bar(..)` и `baz(..)`, мы используем промис, чтобы управлять когда `bar(..)` и `baz(..)` будут выполнены, если вообще будут. Главное отличие - в обработке ошибок. В подходе, использованном в первом примере кода, `bar(..)` вызывается независимо от того, `foo(..)` или нет, и в нем выполняется своя собственная логика возврата, если получается сообщение о том, что в `foo(..)` произошел сбой. Очевидно, что то же самое верно и для `baz(..)`. Во втором примере кода, `bar(..)` вызывается только если `foo(..)` завершена, а иначе вызывается `oopsBar(..)`. То же самое для `baz(..)`. Ни один из этих подходов не является *корректным* сам по себе. Будут случаи, когда один будет предпочитаем другому. В любом случае, промис `p`, который возвращается из `foo(..)`, используется для контроля того, что произойдет дальше. Более того, факт того, что оба примера кода заканчиваются вызовом `then(..)` дважды для одного и того же промиса `p` иллюстрирует рассказанное ранее, то что промисы (если уже разрешены) остаются в том же состоянии разрешения (завершение или отказ) навсегда и могут быть позже исследованы столько раз, сколько нужно. Всякий раз когда `p` разрешается, следующий шаг будет одним и тем же, и *сейчас*, и *позже*. ## Утиная типизация и наличие then В краю промисов важной деталью является то, как узнать наверняка, что какое-то значение - подлинный промис или нет. Или более прямо, ведет ли себя это значение как промис? Учитывая, что промисы создаются используя синтаксис `new Promise(..)`, вы можете подумать, что `p instanceof Promise` будет приемлемой проверкой. Но к сожалению есть ряд причин, указывающих что этого совершенно недостаточно. Преимущественно, вы можете получить значение промиса от другого окна браузера (iframe и т.д.), у которого есть свой собственный промис, отличный от того, который в текущем окне/фрейме, и такая проверка на определение промиса была бы неудачной. Более того, библиотека или фреймворк могут избрать путь распространения своих собственных промисов и не использовать нативную реализацию ES6 `Promise`. По сути, вы можете вполне успешно пользоваться промисами из библиотек в старых браузерах, у которых совсем нет промисов. Когда мы будем обсуждать процессы разрешения промисов позже в этой главе, станет более очевидным почему ненативное, но выглядящее как промис значение будет все еще очень важно опознавать и употреблять. Но на текущий момент, просто поверьте на слово, что это важная часть головоломки. Поэтому, было определено, что путем определения промиса (или чего-то, что ведет себя как промис) будет определение чего-либо, называемого "then-содержащим", как любой объект или функция, у которой есть метод `then(..)`. Предполагается, что любое такое значение является промисо-совместимым then-содержащим. Общее термин для "проверок типа", которые делают предположения о "типе" значения на основании его формы (какие в нем есть свойства), называется "утиная типизация": "Если это выглядит как утка и крякает как утка, значит это должно быть утка" (см. раздел *Типы и грамматика* в этой серии книг). Таким образом утиная проверка на наличие then условно будет такой: ```js if ( p !== null && ( typeof p === "object" || typeof p === "function" ) && typeof p.then === "function" ) { // предположим, что это содержит then! } else { // не содержит then } ``` Тьфу! Оставляя в стороне тот факт, что эта логика немного уродливая для использования в различных местах, тут происходит кое-что поглубже и более проблематичное. Если вы попробуете завершить промис из любого значения объекта/функции, у которых оказалась функция `then(..)`, но вы не хотели интерпретировать его как промис/then-содержащее значение, вам не повезло, потому что он будет автоматически распознан как then-содержащее значение и обработан по специальным правилам (см. позднее в этой главе). Это верно даже если вы не осознавали, что у этого значения есть `then(..)`. Например: ```js var o = { then: function(){} }; // сделать чтобы у `v` в качестве `[[Prototype]]` было `o` var v = Object.create( o ); v.someStuff = "cool"; v.otherStuff = "not so cool"; v.hasOwnProperty( "then" ); // false ``` `v` совсем не выглядит как промис или then-содержащее значение. Это просто обычный объект с некоторыми свойствами. Возможно вы просто хотите передавать это значение везде как любой другой объект. Но скрытно от вас, `v` также связано `[[Prototype]]` (см. *this и прототипы объектов* книгу в серии книг) с другим объектом `o`, в котором как оказалось есть `then(..)`. Таким образом проверка утиной типизации на then-содержащее значение подумает и предположит, что `v` - это then-содержащее значение. Ой-ей. Тут даже всё может быть не так явно намеренным: ```js Object.prototype.then = function(){}; Array.prototype.then = function(){}; var v1 = { hello: "world" }; var v2 = [ "Hello", "World" ]; ``` Оба `v1` и `v2` будут определены как then-содержащие значения. Вы не можете контролировать или предсказать добавит ли какой-либо код случайно или злонамеренно `then(..)` в `Object.prototype`, `Array.prototype` или любой из других встроенных прототипов. И если то, что указано является функцией, которая не вызывает ни один из своих параметров как колбеки, то любой промис, разрешенный с таким значением просто незаметно повиснет навсегда! Безумие. Звучит неправдоподобно или невероятно? Возможно. Но не забывайте, что есть несколько хорошо известных не-промис библиотек, существовавших в сообществе до ES6, в кторым случайно оказался метод, названный `then(..)`. Некоторые из этих библиотек решили переименовать свои собственные методы, чтобы избежать коллизий (которые удручают!). Другие просто были отнесены к несчастливому статусу "несовместим с кодом, использующим промисы" в награду за их неспособность измениться, чтобы убраться с дороги. По стандартам решили украсть ранее незарезервированное и совершенно универсально звучащее имя свойства `then`, что означает что ни одно значение (или любой из его делегатoв), прошлое, настоящее или будущее, не может иметь функцию `then(..)` намеренно или случайно, в противном случае это значение будут путать с then-содержащим в промис-системах, что вероятно повлечет за собой создание ошибок, которые будет действительно трудно отловить. **Предупреждение:** Мне не нравится как мы закончили материал об утиной типизации then-содержащих значений для определения промисов. Были и другие варианты, такие как "брэндинг" или даже "анти-брэндинг"; то, что у нас было, казалось наихудшим компромиссом. Но это совсем не конец света. Then-содержащая утиная типизация может быть и полезной как мы увидим позже. Просто будьте осторожны, так как такая утиная типизация по then может быть опасна если она некорректно определяет что-то как промис, которое таковым не является. ## Доверие к промису Сейчас мы увидели две сильные аналогии, которые объясняют различные аспекты того, что могут делать промисы для нашего асинхронного кода. Но если мы тут и остановимся, мы возможно упустим единственную важнейшую характеристику, которую предоставляет шаблон промисов: доверие. В то время как аналогии *будущие значения* и *события завершения* в явном виде происходят в тех шаблонах кода которые мы изучили, будет не совсем очевидно почему или как промисы разработаны, чтобы решить все проблемы доверия *инверсии управления*, которые мы изложили в секции "Проблемы с доверием" главы 2. Но слегка покопавшись мы можем вскрыть некоторые важные гарантии, которые восстановят уверенность в асинхронном кодировании, которую разрушила глава 2! Давайте начнем с рассмотрения проблем доверия при разработке в стиле одних только колбеков. Когда вы передаете колбек в функцию `foo(..)`, она может: * Вызвать колбек слишком рано * Вызвать колбек слишком поздно (или никогда) * Вызвать колбек слишком мало раз или слишком много раз * Провалить передачу в колбек любых необходимых окружения/параметров * Проглотить любые ошибки/исключения, которые могут произойти Характеристики промисов намеренно разработаны, чтобы обеспечить полезные, воспроизводимые ответы на все эти проблемы. ### Вызывая слишком рано В первую очередь, эта проблема в том, могут ли проявиться в коде Залго-подобные эффекты (см. главу 2), где иногда задача завершается синхронно, а иногда - асинхронно, что может приводить к состоянию гонки. Промисы по определению не могут быть подвержены этой проблеме, потому что даже сразу завершенный промис (типа `new Promise(function(resolve){ resolve(42); })`) нельзя *исследовать* синхронно. То есть, когда вы вызываете `then(..)` у промиса, даже если промис уже был разрешен, колбек, который вы передаете в `then(..)` **всегда** будет вызван асинхронно (детальнее об этом см. "Задачи" в главе 1). Больше не нужно вставлять свои собственные костыли с `setTimeout(..,0)`. Промисы не допускают Залго автоматически. ### Вызывая слишком поздно Аналогично предыдущему пункту, колбеки наблюдения, зарегистрированные в `then(..)` промиса автоматически планируются к вызову когда вызван либо `resolve(..)`, либо `reject(..)` посредством кода создания промиса. Эти запланированные колбеки будут предсказуемо вызваны в следующий асинхронный момент (см. "Задачи" в главе 1). Синхронное наблюдение тут невозможно, следовательно невозможно запустить синхронную цепочку задач таким образом, чтобы на практике "отложить" вызов другого колбека ожидаемым образом. То есть, когда промис разрешен, все зарегистрированные для`then(..)` колбеки будут по порядку вызваны, сразу же при следующей асинхронной возможности (снова, см. "Задачи" в главе 1) и ничто, что происходит внутри одного из этих колбеков не может повлиять или задержать вызов остальных колбеков. Например: ```js p.then( function(){ p.then( function(){ console.log( "C" ); } ); console.log( "A" ); } ); p.then( function(){ console.log( "B" ); } ); // A B C ``` Здесь, `"C"` не может прервать и предшествовать `"B"`, в силу того как промисам было определено работать. #### Хитрости планировщика промисов Важно отметить, впрочем, что есть масса нюансов планировщика, когда относительный порядок между колбеками, выстроенными в цепочки двух отдельных промисов, не является надежно предсказуемым. Если два промиса `p1` и `p2` оба уже разрешены, то будет истиной, что `p1.then(..); p2.then(..)` закончится вызовов колбека(ов) для `p1` до колбеков для `p2`. Но есть некоторые хитрые случаи, когда это может быть и не так, такие как следующий: ```js var p3 = new Promise( function(resolve,reject){ resolve( "B" ); } ); var p1 = new Promise( function(resolve,reject){ resolve( p3 ); } ); var p2 = new Promise( function(resolve,reject){ resolve( "A" ); } ); p1.then( function(v){ console.log( v ); } ); p2.then( function(v){ console.log( v ); } ); // A B <-- не B A как вы могли бы ожидать ``` Мы расскажем об этом позже, но как вы видите `p1` разрешен не с непосредственным значением, а с еще одним промисом `p3`, который сам разрешен со значением `"B"`. Указанное поведение - это *распаковать* `p3` внутри `p1`, но асинхронно, таким образом колбек(и) `p1` *позади* колбека(ов) `p2` в очереди асинхронных задач (см. главу 1). Чтобы избежать таких хитрых кошмаров, вам никогда не следует полагаться на что-либо связанное с порядком/шедулингом колбеков в промисах. На самом деле, хорошей практикой будет не писать код таким образом, чтобы порядок многочисленных колбеков имел хоть какие-то значение. Избегайте этого если сможете. ### Колбек, который никогда не был вызван Это - очень распространенная проблема. Она решаема несколькими путями с промисами. Во-первых, ничто (ни даже JS-ошибка) не может повлиять на уведомление вас от промиса о своем разрешении (если он разрешен). Если вы указываете оба колбека как для завершения, так и для сбоя при создании промиса и промис разрешается,то один из этих двух колбеков всегда будет вызван. Конечно, если ваши колбеки сами содержат JS-ошибки, вы можете не получить ожидаемый результат, но колбек и в самом деле будет вызван. Мы расскажем позже как получить уведомление об ошибке в своем колбеке, потому что даже эти ошибки не проглатываются. Но что если сам промис никогда не будет разрешен тем или иным путем? Даже для такой ситуации у промисов есть ответ используя абстракцию более высокого порядка, называемую "гонка": ```js // функция с промисом по тайм-ауту function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( "Тайм-аут!" ); }, delay ); } ); } // настройка тайм-аута для `foo()` Promise.race( [ foo(), // попробовать вызвать `foo()` timeoutPromise( 3000 ) // дать на это 3 секунды ] ) .then( function(){ // `foo(..)` выполнился успешно и вовремя! }, function(err){ // либо `foo()` завершилась неудачно, либо она // не завершилась вовремя, так что проверьте // `err` чтобы определить причину } ); ``` Есть еще много нюансов, которые можно учесть при рассмотрении этого шаблона промисов с тайм-аутом, но мы вернемся к этом позже. Важно, что мы можем гарантированно просигнализировать о результате `foo()`, чтобы предотвратить зависание нашей программы бесконечно. ### Вызывая слишком мало или много раз По определению, *один* - это подходящее число раз, которое колбек должен быть вызван. Случай "слишком мало" будет означать ноль вызовов, что то же самое что и случай "никогда", который мы только что рассмотрели. Случай "слишком много" легко объяснить. Промисы определены таким образом, что могут быть разрешены только один раз. Если по каким-либо причинам код создания промиса попытается вызвать `resolve(..)` или `reject(..)` несколько раз или попытается вызвать их обоих, то промис примет во внимание только первый вызов и молча проигнорирует любые последующие попытки. Поскольку промис можно разрешить лишь раз, любые зарегистрированные колбеки `then(..)` будут вызваны только по разу (каждый). Конечно, если зарегистрируете один и ото же колбек более одного раза, (т.е., `p.then(f); p.then(f);`), он будет вызван столько раз, сколько был зарегистрирован. Гарантия того, что функция ответа будет вызвана только раз не препятствует вам выстрелить себе в ногу. ### Сбой при передаче параметров/окружения У промисов может быть не больше одного значения разрешения (завершение или отказ). Если вы не разрешаете промис явно с конкретным значением, значение будет `undefined`, как и обычно в таких случаях в JS. Но если ли значение или нет, оно всегда будет передавно во все зарегистрированные (и корректные: завершение или отказ) колбеки, либо *сейчас*, или в будущем. Что-то, о чем нужно знать: если вы вызываете `resolve(..)` или `reject(..)` с несколькими параметрами, все параметры, которые следуют за первым, будут молча проигнорированы. Хотя это и может выглядеть как нарушение гарантии, которую мы только что описали, это не совсем так потому что it это представляет собой недопустимое использование механизма промисов. Другие недопустимые случаи использования API (такие как вызов `resolve(..)` несколько раз) *защищены* похожим образом, таким образом поведение промисов тут будет консистентным (если не немного расстраивающим). Если вы хотите передать несколько значений, вы должны обернуть их в еще одно одиночное значение, которое вы и передадите, такое как `array` (массив) или `object` (объект). Что касается окружения, функции в JS всегда сохраняют свое замыкание области видимости, в которой они определены (см. *Область видимости и замыкания* в этой серии книг), так что у них, конечно, остается доступ к тому окружающему состоянию, которое вы предоставляете. Конечно, то же самое справедливо и для подхода с одними колбеками, поэтому это не какое-то особенная выгодная добавка от промисов, но это гарантия, на которую мы можем тем ни менее положиться. ### Проглатывание любых ошибок/исключений В основе, это просто переформулирование предыдущего пункта. Если вы отклоняете промис с определенной *причиной* (т.е. сообщением об ошибке), то это значение будете передано в колбек(и) отказа. Но здесь есть нечто гораздо большее. Если в любой точке создания промиса или при исследовании его разрешения произойдет исключение JS, такое как `TypeError` или `ReferenceError`, такое исключение будет захвачено и это заставит завершиться с отказом рассматриваемый промис. Например: ```js var p = new Promise( function(resolve,reject){ foo.bar(); // `foo` не определена поэтому ошибка! resolve( 42 ); // никогда не достигнет этой точки :( } ); p.then( function fulfilled(){ // никогда не достигнет этой точки :( }, function rejected(err){ // `err` будет объектом исключения `TypeError` // произошедшим на строке `foo.bar()`. } ); ``` JS-исключение, которое возникает от `foo.bar()`, становится отказом промиса, который вы можете захватить и обработать. Это - важная деталь, потому что она успешно решает еще один потенциальный Залго-момент, который заключается в том, что эти ошибки повлечь синхронную реакцию тогда как не-ошибки будут асинхронными. Даже JS-исключения промисы превращают в асинхронное поведение, тем самым значительно уменьшая шансы появления состояния гонки. Но что произойдет если промис завершен, а произошло JS-исключение во время обработки результата (в зарегистрированном колбеке `then(..)`)? Даже такие исключения не будут потеряны, но вы можете немного удивиться тому, как они обрабатываются, пока не вникните немного глубже в это: ```js var p = new Promise( function(resolve,reject){ resolve( 42 ); } ); p.then( function fulfilled(msg){ foo.bar(); console.log( msg ); // никогда не достигнет этой точки :( }, function rejected(err){ // никогда не достигнет и этой точки :( } ); ``` Подождите-ка, тут похоже, что исключение от `foo.bar()` и в самом деле проглочено. Без паники, оно не пропало. Но кое-что внутри не так, а именно то, что мы не смогли получить уведомление об этом. Вызов `p.then(..)` сам по себе возвращает другой промис и это *тот самый* промис, который будет отклонен с исключением `TypeError`. Так почему же он не может просто вызвать обработчик ошибок, который мы там объявили? Похоже, что логичное объяснение лежит на поверхности. Но оно нарушило бы основополагающий принцип промисов - **неизменность** после разрешения. `p` уже была завершена со значением `42`, так что она не может быть позднее изменена на отказ только потому, что возникла ошибка в наблюдающей функции разрешения `p`. Кроме нарушения принципа, такое поведение может нанести ущерб, если скажем было несколько зарегистрированных колбеков `then(..)` для промиса `p`, поскольку тогда некоторые будут вызваны, а другие - нет, и это было очень непрозрачно в плане причины почему так произошло. ### Надежный промис? Есть одна последняя деталь, чтобы понять как установить доверие, основанное на шаблоне промисов. Без сомнения вы заметили, что промисы не избавились от колбеков полностью. Они просто поменяли место, куда передается колбек. Вместо передачи колбека в `foo(..)`, мы получаем *нечто* (предположительно настоящий промис) обратно из `foo(..)`, и мы взамен передаем колбек в это *нечто*. Но почему это было бы надежнее, чем просто колбеки сами по себе? Как мы можем быть уверены в точ, что *нечто*, что мы получаем в ответ на самом деле надежный промис? Не является ли всё это в сущности просто карточным домиком, где мы можем доверять только потому, что мы уже доверяем? Одна из самых важных, но часто незаслуженно обойденных вниманием деталей промисов - это то, что у них есть решение также и этой проблемы. Включенное в нативную реализацию ES6 `Promise` - `Promise.resolve(..)`. Если вы передаете непосредственное, не являющееся ни промисом, ни then-содержащим, значение в `Promise.resolve(..)`, вы получили промис, который завершен с этим значением. Другими словами, эти два промиса `p1` и `p2` будут вести себя практически идентично: ```js var p1 = new Promise( function(resolve,reject){ resolve( 42 ); } ); var p2 = Promise.resolve( 42 ); ``` Но если вы передадите настоящий промис в `Promise.resolve(..)`, вы просто получите тот же промис обратно: ```js var p1 = Promise.resolve( 42 ); var p2 = Promise.resolve( p1 ); p1 === p2; // true ``` Что еще более важно, если вы передаете не-промис then-содержащее значение в `Promise.resolve(..)`, оно попытается распаковать это значение, и распаковка будет продолжаться до тех пор, пока не будет извлечено конкретное окончательное не-промис значение. Помните наше прошедшее обсуждение then-содержащих? Рассмотрите: ```js var p = { then: function(cb) { cb( 42 ); } }; // это сработает, но только благодаря удаче p .then( function fulfilled(val){ console.log( val ); // 42 }, function rejected(err){ // никогда не достигнет этой точки } ); ``` Это `p` - then-содержащее, но это не настоящий промис. К счастью, оно разумное, как и большинство какие бывают. Но что если вы получите в ответ что-то, что выглядит как-то так: ```js var p = { then: function(cb,errcb) { cb( 42 ); errcb( "злобный смех" ); } }; p .then( function fulfilled(val){ console.log( val ); // 42 }, function rejected(err){ // ой, не должно было сработать console.log( err ); // злобный смех } ); ``` Это `p` - then-содержащее, но оно не ведет себя как хороший промис. Вредоносное ли оно? Или просто игнорирует то, как должны работать промисы? Это не важно, если честно. В любом случае, оно не надежно в том виде, как есть. Тем ни менее, мы может передать любую их этих версий `p` в `Promise.resolve(..)`, и мы получим нормализованный, безопасный результат, который и ожидаем: ```js Promise.resolve( p ) .then( function fulfilled(val){ console.log( val ); // 42 }, function rejected(err){ // никогда не достигнет этой точки } ); ``` `Promise.resolve(..)` примет любые then-содержащие аргументы, и распакует их в их не-then-содержащее значение. Но вы получите обратно из `Promise.resolve(..)` настоящий, подлинный промис, **тот, которому вы можете доверять**. Если то, что вы передали, уже является настоящим промисом, вы просто получите его же обратно, так что нет никаких недостатков в том, что фильтровать через `Promise.resolve(..)`, чтобы получить надежность. Допустим мы вызываем `foo(..)` и не уверены, что можем доверять его возвращаемому значению в том, что оно является правильным промисом, но мы знаем, что оно как минимум then-содержащее. `Promise.resolve(..)` даст нам надежную обертку в виде промиса, которую можно использовать в цепочке: ```js // не делайте так: foo( 42 ) .then( function(v){ console.log( v ); } ); // вместо этого, делайте так: Promise.resolve( foo( 42 ) ) .then( function(v){ console.log( v ); } ); ``` **Примечание** Еще один выгодный побочный эффект оборачивания `Promise.resolve(..)` вокруг любого возвращаемого из функции значения (then-содержащей или нет) - это то, что это легкий путь к приведению этого вызова функции к правильно ведущей себя асинхронной задаче. Если `foo(42)` иногда возвращает непосредственное значение, а иногда промис, `Promise.resolve( foo(42) )` следит за тем, чтобы это всегда был в результате промис. И избегая Залго приводит к намного лучшему коду. ### Построенное доверие Надеюсь, что предыдущая дискуссия теперь полностью "разрешает" (каламбур) в вашим мозгу факт, почему промис надежен, и более важно, почему это доверие так критично для построения надежного, поддерживаемого ПО. Можете ли вы писать асинхронный код в JS без какой-либо надежности? Конечно можете. Мы, JS-разработчики, пишем асинхронный код не имея ничего кроме колбеков уже почти два десятилетия. Но как только вы начинаете задавать вопросы просто о том, насколько вы можете доверять механизмам, на которые вы опираетесь, в самом деле быть предсказуемыми и надежными, вы начинаете осознавать, что у колбеков довольно шаткий фундамент доверия. Промисы - это шаблон, который дополняет колбеки надежной семантикой, так что поведение становится более разумным и надежным. Сделав разинверсию *инверсии управления* колбеками, мы передаем управление надежной системе (промисам), которая была специально разработана, чтобы привнести здравый смысл в нашу асинхронность. ## Цепочечный поток Мы уже намекали на это пару раз, но промисы - это не только механизм для одношаговой операции типа *это-затем-то*. Это строительный блок, конечно, но оказывается мы можем связать немного промисов вместе для представления последовательности асинхронных шагов. Ключ к тому, чтобы это сработало, построен на двух видах поведения, присущих промисам: * Каждый раз как вы вызываете `then(..)` у промиса, он создает и возвращает новый промис, с которым мы можем составить *цепочку*. * Какое бы значение вы ни вернули из колбека завершения `then(..)` (первый параметр) - оно автоматически устанавливается как set как результата нормального завершения промиса *в цепочке* (из первого пункта). Давайте сперва проиллюстрируем что это значит, а *затем* (then, туту игра слов) вы выведем как это помодет нам создавать асинхронные последовательности управления потоком. Рассмотрим следующее: ```js var p = Promise.resolve( 21 ); var p2 = p.then( function(v){ console.log( v ); // 21 // завершить `p2` со значением `42` return v * 2; } ); // составляем цепочку с `p2` p2.then( function(v){ console.log( v ); // 42 } ); ``` Возвращая `v * 2` (i.e., `42`), мы завершаем промис `p2` с успешным результатом, который создал и вернул первый вызов `then(..)`. Когда происходит вызов `then(..)` у `p2`, он получает результат из выражения `return v * 2`. Конечно, `p2.then(..)` создает еще один промис, который мы можем сохранить в переменной `p3`. Но немного раздражает необходимость создавать промежуточную переменную `p2` (или `p3`, и т.д.). К счастью, мы легко можем объединить их в цепочку: ```js var p = Promise.resolve( 21 ); p .then( function(v){ console.log( v ); // 21 // вернуть результат промиса в цепочке со значением `42` return v * 2; } ) // вот и промис в цепочке .then( function(v){ console.log( v ); // 42 } ); ``` Таким образом теперь первый `then(..)` - это первый шаг в асинхронной последовательности, а второй `then(..)` - это второй шаг. Это может продолжаться столь долго, сколь вам надо это расширять. Просто продолжайте цепочку от предыдущего `then(..)` автоматически созданным промисом. Но здесь нет кое-чего. Что если мы хотим, чтобы шаг 2 ждал пока шаг 1 выполнит что-то асинхронное? Мы используем выражение `return` для возврата значения сразу, которое немедленно и завершает промис в цепочке. Ключ к тому, чтобы заставить последовательность промисов быть истинно асинхронной на каждом шаге - это вспомнить как работает `Promise.resolve(..)` когда то, что вы передаете ему - это промис или then-содержащее вместо конечного значения. `Promise.resolve(..)` прямо возвращает полученный настоящий промис или распаковывает значение полученного then-содержащего и движется рекурсивно пока может распаковывать then-содержащие. Такой же вид распаковки происходит если вы используете в `return` then-содержащее или промис из обработчика завершения (или отказа). Рассмотрим: ```js var p = Promise.resolve( 21 ); p.then( function(v){ console.log( v ); // 21 // создаем промис и возвращаем его return new Promise( function(resolve,reject){ // завершение со значением `42` resolve( v * 2 ); } ); } ) .then( function(v){ console.log( v ); // 42 } ); ``` Несмотря на то, что мы обернули `42` в промис, который вернули, оно все еще распаковано и оказалось в качестве разрешения промиса в цепочке, такого как второй `then(..)`, который тем ни менее получил `42`. Если мы добавим асинхронность для этого оборачивающего промиса, все всё еще будет работать по-прежнему одинаково: ```js var p = Promise.resolve( 21 ); p.then( function(v){ console.log( v ); // 21 // создаем промис и возвращаем его return new Promise( function(resolve,reject){ // добавляем асинхронность! setTimeout( function(){ // завершение со значением `42` resolve( v * 2 ); }, 100 ); } ); } ) .then( function(v){ // запускается после задержки 100мс на предыдущем шаге console.log( v ); // 42 } ); ``` Это невероятно мощно! Теперь мы можем строить последовательность со сколь угодно большим количеством асинхронных шагов и каждый шаг может задержать следующий шаг (или не задержать!), если необходимо. Конечно, значение, передаваемое из шага в шаг в этих примерах, не обязательное. Если вы не вернете явное значение, будет предполагаться неявное значение `undefined` и промисы все еще будут в цепочке таким же образом. Разрешение каждого промиса, таким образом, является просто сигналом перейти к следующему шагу. Для дальней иллюстрации цепочки, давайте обобщим создание промиса с задержкой (без вывода сообщений о завершении) в функцию, которую мы можем переиспользовать для нескольких шагов: ```js function delay(time) { return new Promise( function(resolve,reject){ setTimeout( resolve, time ); } ); } delay( 100 ) // шаг 1 .then( function STEP2(){ console.log( "шаг 2 (после 100мс)" ); return delay( 200 ); } ) .then( function STEP3(){ console.log( "шаг 3 (после еще 200мс)" ); } ) .then( function STEP4(){ console.log( "шаг 4 (следующая задача)" ); return delay( 50 ); } ) .then( function STEP5(){ console.log( "шаг 5 (после еще 50мс)" ); } ) ... ``` Вызов `delay(200)` создает промис, который завершится через 200мс, а затем вы вернем его из первого колбека завершения `then(..)`, который приведет ко второму промису от `then(..)`, чтобы подождать этот 200мс-промис. **Примечание** Как было описано, технически есть два промиса в этом обмене: промис с 200мс-задержкой и промис в цепочке, к которому присоединяется второй `then(..)`. Но возможно вам будет проще мысленно объединить эти два промиса вместе, потому что механизм промисов автоматически объединим их состояния для вас. В этом отношении, вы можете думать о `return delay(200)` как о создании промиса, который заменяет ранее возвращенный промис в цепочке. Хотя, если честно, последовательности задержек без передачи полезной нагрузки не очень полезный пример управления потоком промисов. Давайте взглянем на сценарий, который выглядит чуточку более практично. Вместо таймеров, давайте рассмотрим возможность создания Ajax-запросов: ```js // предположим есть функция `ajax( {url}, {callback} )` // Ajax с учетом промисов function request(url) { return new Promise( function(resolve,reject){ // колбек `ajax(..)` должен стать нашей // функцией `resolve(..)` промиса ajax( url, resolve ); } ); } ``` Сначала мы определяем функцию `request(..)`, которая создает промис для представления завершения вызова `ajax(..)`: ```js request( "http://some.url.1/" ) .then( function(response1){ return request( "http://some.url.2/?v=" + response1 ); } ) .then( function(response2){ console.log( response2 ); } ); ``` **Примечание** Разработчики часто сталкиваются с ситуациями, когда они хотят получить асинхронное управление потоком промисов для функций, которые сами по себе не поддерживают промисы (как `ajax(..)` здесь, который ожидает на входе колбек). Хотя нативный механизм ES6 `Promise` не решает автоматически этот шаблон за нас, практически все промис-библиотеки *решают*. Они обычно называют этот процесс "lifting" (поднятие) или "promisifying" (промисифицирование) или вариации на тему. Мы вернемся к этой технике позже. Используя `request(..)`, умеющий возвращать промис, мы неявно создаем первый шаг в нашей цепочке вызывая его с первым URL, и объединяем в цепочку этот возвращенный промис с первым `then(..)`. Как только возвращается `response1`, мы используем это значение чтобы создать второй URL и выполнить второй вызов `request(..)`. Этот второй промис из `request(..)` - возвращен `return`ом таким образом, что третий шаг в нашем асинхронном контроле потока ждет завершения этого Ajax-вызова. Наконец, мы выводим `response2` как только он возвращен. Цепочка промисов, которую мы создали - не только управление потоком, которое отражает многошаговую асинхронную последовательность, но и также действует как канал сообщений для передачи их от шага к шагу. Что если что-то пойдет не так в одном из шагов цепочки промисов? Ошибка/исключение соединены со своим промисом, что означает, что есть возможность поймать такую ошибку в любом месте цепочки и такой захват действует как бы как "сброс" цепочки обратно к нормальному функционированию в этой точке: ```js // шаг 1: request( "http://some.url.1/" ) // шаг 2: .then( function(response1){ foo.bar(); // undefined, ошибка! // никогда не достигнет этой точки return request( "http://some.url.2/?v=" + response1 ); } ) // шаг 3: .then( function fulfilled(response2){ // никогда не достигнет этой точки }, // обработчик отказа, чтобы поймать ошибку function rejected(err){ console.log( err ); // `TypeError` из-за ошибки на `foo.bar()` return 42; } ) // шаг 4: .then( function(msg){ console.log( msg ); // 42 } ); ``` Когда происходит ошибка на шаге 2, обработчик отказа в шаге 3 ловит ее. Возвращаемое значение (`42` в этом примере кода), если таковое есть, из обработчика отказа завершает промис для следующего шага (4), так, чтобы цепочка вернулось обратно в состояние завершения. **Примечание** Как мы уже говорили ранее, при возврате промиса из обработчика завершения, этот промис не обернут и может задержать следующий шаг. Это также верно при возврате промисов из обработчиков отказа, так что если `return 42` на шаге 3 вернет вместо этого промис, этот промис может задержать шаг 4. Выброшенное исключение внутри либо обработчика завершения, либо отказа в вызове `then(..)` приведет к тому, что следующий (в цепочке) промис будет немедленно отвергнут с этим же исключением. Если вы вызовете `then(..)` у промиса и передадите только обработчик завершения в него, будет подставлен неявный обработчик отказов: ```js var p = new Promise( function(resolve,reject){ reject( "Ой" ); } ); var p2 = p.then( function fulfilled(){ // никогда не достигнет этой точки } // неявный обработчик отказа, если явно не указан или // передано любое другое значение - не-функция // function(err) { // throw err; // } ); ``` Как видите, неявный обработчик отказа просто повторно бросает ту же ошибку, что в итоге вынуждает `p2` (промис в цепочке) завершиться отказом с той же самой причиной в виде ошибки. По сути, это позволяет ошибке продолжить путешествовать по цепочке промисов до тех пор, пока не встретится явно заданный обработчик отказа. **Примечание** Мы расскажем подробнее об обработке ошибок в промисах немного позже, потому что есть и другие нюансы, о которым стоит побеспокоиться. Если в `then(..)` не передана валидная функция в качестве параметра обработчика завершения, также будет подставлен неявный обработчик: ```js var p = Promise.resolve( 42 ); p.then( // неявный обработчик завершения, если явно не указан или // передано любое другое значение - не-функция // function(v) { // return v; // } null, function rejected(err){ // никогда не достигнет этой точки } ); ``` Как видите, обработчик завершения по умолчанию просто передает полученное значение на следующий шаг (промис). **Примечание** Шаблон `then(null,function(err){ .. })`, обрабатывающий только отказы (если есть), но позволяющий пропускать далее завершения, имеет сокращенную форму в API: `catch(function(err){ .. })`. Мы рассмотрим `catch(..)` более полно в следующем разделе. Давайте вкратце рассмотрим присущие промисам типы поведения, которые позволяют организовать цепочечное управление потоком: * Вызов `then(..)` с одним промисом автоматически создает новый промис в качестве возвращаемого значения вызова. * Внутри обработчиков завершения/отказа, если вы возвращаете значение или бросается исключение, новый возвращенный промис (который можно присоединить к цепочке) разрешается соответственно с тем же результатом. * Если обработчик завершения или отказа возвращают промис, он не обернут, таким образом как бы он не разрешился, это станет разрешением промиса в цепочке, возвращенного из текущего `then(..)`. Хотя цепочечное управление потоком полезное, возможно будет более точным представлять его как побочный эффект того, как промисы объединяются (составляются) вместе, нежели как основной функционал. Как мы подробно обсуждали уже несколько раз, промисы нормализуют асинхронность и скрывают состояние значения, зависимого от времени, и это *то*, что позволяет нам объединять их в цепочки таким удобным способом. Определенно, последовательная выразительность цепочки (это-then-это-then-это...) - это большое улучшение по сравнению с запутанным клубком колбеков, как мы уже выяснили в главе 2. Но все еще есть изрядный объем шаблона (`then(..)` и `function(){ .. }`), через который нужно продираться. В следующей главе мы увидим значительно более приятный шаблон для выразительной организации последовательного управления потоком с помощью генераторов. ### Терминология: Разрешить (Resolve), Завершить (Fulfill) и Отвергнуть (Reject) Существует небольшая путаница в терминах "разрешить (resolve)", "Завершить (fulfill)" и "отвергнуть (reject)", которую нам необходимо прояснить до того, как вы погрузитесь слишком глубоко в изучении промисов. Давайте сначала рассмотрим конструктор `Promise(..)`: ```js var p = new Promise( function(X,Y){ // X() для завершения // Y() для отказа } ); ``` Как видите, переданы два колбека (здесь помечены как `X` и `Y`). Первый *обычно* используется для отметки того, что промис завершен, а второй *всегда* помечает промис как отвергнутый. Но о чем это "обычно" и что это означает для точного именования этих параметров? В конечном счете, это только ваш код и имена идентификаторов не интерпретируются JS-движком как что-то значимое, так что *технически* это не имеет значения; `foo(..)` и `bar(..)` одинаково функциональны. Но слова, которыми вы пользуетесь, могут затронуть не только как вы думаете о коде, но и как другие разработчики в вашей команде будут думать о нем. Думая неправильно о тщательно организованном асинхронном коде - это почти наверняка будет хуже, чем альтернативы из спагетти-колбеков. Так что на самом деле имеет значение, как вы их называете. Со вторым параметром легко определиться. Почти вся литература использует `reject(..)` (отвергнуть) как его имя и поскольку это в точности (и только это!) то, что он делает, это и есть очень хороший выбор для этого имени. Я бы настоятельно рекомендовал вам всегда использовать `reject(..)`. Но вокруг первого параметра чуть больше неясностей, который в литературе о промисах часто обозначается `resolve(..)` (разрешить). Это слово очевидно связано с "resolution" (разрешение), которое и используется во всей литературе (включая эту книгу), чтобы описать установку конечного значения/состояния в промисе. Мы уже использовать "разрешить промис" несколько раз, чтобы обозначить либо завершение, или отвергнутый промис. Но если этот параметр, по-видимому, используется для конкретно завершения промиса, почему бы не назвать его `fulfill(..)` (завершить) вместо `resolve(..)` (разрешить), чтобы быть более точным? Чтобы ответить на этот вопрос, давайте также взглянет на два метода `Promise` API: ```js var fulfilledPr = Promise.resolve( 42 ); var rejectedPr = Promise.reject( "Ой" ); ``` `Promise.resolve(..)` создает промис, который разрешен со переданным значением. В этом примере, `42` - это обычное, не-промис, не-then-содержащее значение, поэтому завершенный промис `fulfilledPr` создан для значения `42`. `Promise.reject("Ой")` создает отвергнутый промис `rejectedPr` для причины `"Ой"`. Давайте теперь проиллюстрируем, почему слово "resolve" (разрешить) (такое как в `Promise.resolve(..)`) - является однозначным и более точным, если используется явно в контексте, который должен либо завершиться, либо закончиться отказом: ```js var rejectedTh = { then: function(resolved,rejected) { rejected( "Ой" ); } }; var rejectedPr = Promise.resolve( rejectedTh ); ``` Как мы уже говорили ранее в этой главе, `Promise.resolve(..)` вернет полученный настоящий промис напрямую или распакует полученное then-содержащее. Если распаковка этого then-содержащего покажет отвергнутое состояние, то промис, который был возвращен из `Promise.resolve(..)` - по факту в том же самом отвергнутом состоянии. Таким образом `Promise.resolve(..)` - это хорошее, точное название для метода API, потому что он может либо завершиться, либо будет отвергнутым. Первый колбек-параметр конструктора `Promise(..)` распакует либо then-содержащее (идентично `Promise.resolve(..)`), либо настоящий промис: ```js var rejectedPr = new Promise( function(resolve,reject){ // разрешить этот промис отвергнутым промисом resolve( Promise.reject( "Ой" ) ); } ); rejectedPr.then( function fulfilled(){ // никогда не достигнет этой точки }, function rejected(err){ console.log( err ); // "Ой" } ); ``` Теперь должно быть ясно, что `resolve(..)` - подходящее название для для первого колбек-параметра конструктора `Promise(..)`. **Предупреждение:** Ранее упомянутый `reject(..)` **не** выполняет распаковку, как это делает `resolve(..)`. Если вы передадите промис или then-содержащее значение в `reject(..)`, то именно это значение нетронутым будет установлено как причина отказа. Последующий обработчик отказа получит настоящий промис/then-содержащее, которое вы передали в `reject(..)`, а не его внутреннее непосредственное значение. Но теперь давайте обратим наше внимание на колбеки, переданные в `then(..)`. Как их следует назвывать (и в литературе, и в коде)? Я бы предложил `fulfilled(..)` (завершенный) и `rejected(..)` (отвергнутый): ```js function fulfilled(msg) { console.log( msg ); } function rejected(err) { console.error( err ); } p.then( fulfilled, rejected ); ``` В случае первого параметра в `then(..)` - это однозначно всегда случай завершения, поэтому нет нужны для двойственной терминологии "resolve". В качестве примечания, спецификация ES6 использует `onFulfilled(..)` и `onRejected(..)`, чтобы обозначить эти два колбека, поэтому они являются точными терминами. ## Обработка ошибок Мы уже видели несколько примеров того, как отказ промисов, либо намеренно через вызов `reject(..)`, либо случайно через исключение JS, позволяет более разумно обрабатывать ошибки при асинхронной разработке. Давайте вернемся назад и четко сформулируем некоторые детали, которые мы пропустили. Самая естественная форма обработки ошибок для большинства разработчиков - это синхронная конструкция `try..catch`. К сожалению, она есть только в синхронной форме, поэтому она не поможет в шаблонах асинхронного кода: ```js function foo() { setTimeout( function(){ baz.bar(); }, 100 ); } try { foo(); // позднее выбросит глобальную ошибку из `baz.bar()` } catch (err) { // никогда не достигнет этой точки } ``` Было бы неплохо иметь в арсенале `try..catch`, но он не работает для асинхронных операций. То есть, если только нет какой-то дополнительной поддержки среды, к которой мы вернемся вместе с генераторами в главе 4. В колбеках, появились некоторые стандарты для шаблонной обработки ошибок, особенно стиль "колбек ошибки идет первым": ```js function foo(cb) { setTimeout( function(){ try { var x = baz.bar(); cb( null, x ); // успех! } catch (err) { cb( err ); } }, 100 ); } foo( function(err,val){ if (err) { console.error( err ); // облом :( } else { console.log( val ); } } ); ``` **Примечание** `try..catch` тут работает только с той точки зрения, что вызов `baz.bar()` немедленно завершится или прервется со сбоем и при этом синхронно. Если `baz.bar()` сам являлся своей же асинхронной функцией продолжения, то любые асинхронные ошибки внутри него нельзя будет поймать. Колбек, который мы передаем в `foo(..)`, ожидает получить сигнал об ошибке с помощью зарезервированного первого параметра `err`. Если присутствует, предполагается ошибка. Если нет, то предполагается завершение. Такой тип обработки ошибок технически *поддерживает асинхронность*, но он совсем не способен к композиции. Множественные уровни колбеков в стиле "ошибка первая" сплетенные вместе с этими вездесущими операторами проверок `if` неизбежно приведет вас к опасностям, связанным с адом колбеков (см. главу 2). Таким образом вы возвращаемся к обработке ошибок с помощью промисов, с передачей обработчика отказов в `then(..)`. Промисы не используют популярный стиль дизайна "колбек с первым параметром-ошибкой", а вместо этого используют стиль "раздельных колбеков", есть один колбек для завершения и другой - для отказа: ```js var p = Promise.reject( "Ой" ); p.then( function fulfilled(){ // никогда не достигнет этой точки }, function rejected(err){ console.log( err ); // "Ой" } ); ``` Несмотря на то, что этот шаблон обработки ошибок, на первый взгляд, имеет смысл, нюансы обработки ошибок в промисах зачастую гораздо сложнее полностью осознать. Представьте: ```js var p = Promise.resolve( 42 ); p.then( function fulfilled(msg){ // числа не содержат функцией как у строк, // поэтому тут возникнет ошибка console.log( msg.toLowerCase() ); }, function rejected(err){ // никогда не достигнет этой точки } ); ``` Если `msg.toLowerCase()` законно выдает ошибку (и это действительно так!), почему же наш обработчик ошибок не вызван? Как мы объясняли ранее, так происходит потому, что *этот* обработчик ошибок - для промиса `p`, который уже был разрешен со значением `42`. Промис `p` - неизменяемый, поэтому единственный промис, который может получить ошибку, тот, что возвращается из `p.then(..)`, который мы в этом случае никак не ловим. Это должно дать четкое представление о том, почему обработка ошибок в промисах подвержена ошибкам (каламбур). Слишком легко допустить, чтобы ошибки были "проглочены", так как это очень редко то, что вы задумывали изначально. **Предупреждение:** Если вы используете API промисов неправильным путем и происходит ошибка, то это препятсвует правильному созданию промиса, в результате будет полученное сращу же исключение, но **не отвергнутый промис**. Некоторые примеры некорректного использования, который ломают создание промиса: `new Promise(null)`, `Promise.all()`, `Promise.race(42)`, и т.д.. Вы не сможете получить отвергнутый промис, если вы не используете API промисов достаточно корректно, чтобы на само деле создать в первую очередь сам промис! ### Яма отчаяния Джефф Этвуд заметил несколько лет назад: языки программирования часто настроены настоены по умолчанию таким образом, что разработчки попадают в "яму отчаяния" (http://blog.codinghorror.com/falling-into-the-pit-of-success/), где за инциденты наказывают и что нужно больше стараться, чтобы все получилось. Он призвал нас вместо этого создавать "яму успеха," когда по умолчанию вы попадаете в ожидаемое (успешное) действие, и, следовательно, должны сильно постараться, чтобы потерпеть неудачу. Обработка ошибок в промисах - несомненно, является дизайном "ямы отчаяния". По умолчанию, он предполагает, что вы хотите, чтобы любые ошибки были поглощены состоянием промиса и если вы забудете исследовать этот состояние, ошибка тихо томится/умирает в безвестности, обычно отчаяния. Чтобы избежать ошибки из-за молчания забытого/заброшенного промиса, некоторые разработчики заявили, что "лучшей практикой" для цепочек промисов является всегда завершать цепочку заключительным `catch(..)`, например: ```js var p = Promise.resolve( 42 ); p.then( function fulfilled(msg){ // числа не содержат функцией как у строк, // поэтому тут возникнет ошибка console.log( msg.toLowerCase() ); } ) .catch( handleErrors ); ``` Поскольку мы не передали обработчик отказов в `then(..)`, был подставлен обработчик по умолчанию, который просто передает ошибку следующему промису в цепочке. Таким образом, обе ошибки, идущие в `p`, и ошибки, которые появляются *после* `p` в его разрешении (как в `msg.toLowerCase()`) будут отфильтрованы до конечного `handleErrors(..)`. Проблема решена, не так ли? Не так быстро! Что случится, если `handleErrors(..)` сам содержит ошибку? Кто отловит ее? Остался еще один невыполненный промис: тот, который возвращает `catch(..)`, который мы не ловим и не регистрируем обработчик отказа для него. Вы не можете просто приклеить другой `catch(..)` в конец этой цепочки, потому что он тоже может завершиться ошибкой. Последний шаг в любой цепочек промисов, какой бы он ни был, всегда будет содержать возможность, хоть и в меньшей степени, зависнуть с непойманной ошибкой, застрявшей внутри неотслеживаемого промиса. Все еще звучит как невыполнимая головоломка? ### Обработка непойманного Эту проблему нелегко решить полностью. Есть и другие способы достичь этого, которые, по мнению многих, являются *лучшими*. Некоторые промис-библиотеки добавили методы для регистрации чего-то подобного "глобальному обработчику необработанных отказов", который бы вызывался вместо глобального выброса ошибки. Но их решение о том, как определить ошибку как "непойманную" - это иметь таймер произвольной длительности, скажем 3 секунды, запускаемый от момента отказа. Если промис отвергнут, но не было зарегистрировано ни одного обработчика ошибок до того, как будет запушен таймер, то предполагается, что вы будто и не регистрировали ни одного обработчика, поэтому ошибка "не поймана". На практике, это хорошо работает для многих библиотек, поскольку большинство использованных подходов как правило не требуют значительной задержки между отказом промиса и началом наблюдения за ним. Но этот шаблон вызывает проблемы, потому что 3 секунды это очень произвольное время (пусть даже эмпирическое), а также потому, что действительно есть случаи когда вы хотите придержать отказ промиса на некоторый неопределенный период времени и вы на самом деле не хотите, чтобы ваш обработчик "непойманного" вызывался для всех этих ложных срабатываний (еще не обработанные "непойманные ошибки"). Другое более распространенное предложение заключается в том, что к промисам нужно добавить `done(..)`, что, по сути, отмечает цепочку промисов как "выполненную" (done). `done(..)` не создает и не возвращает промис, таким образом колбеки, переданные в `done(..)`, очевидно не подключены для извещения о проблемах в промис в цепочке, который не существует. Итак, что происходит вместо этого? Она обрабатывается так, как вы обычно ожидаете в условиях не пойманной ошибки: любое исключение внутри обработчика отказа `done(..)` будет выброшено как глобальная непойманная ошибка (в консоль разработчика, по сути): ```js var p = Promise.resolve( 42 ); p.then( function fulfilled(msg){ // у чисел нет функций как у строк, // поэтому будет выброшена ошибка console.log( msg.toLowerCase() ); } ) .done( null, handleErrors ); // если `handleErrors(..)` породила свое собственное исключение, оно // будет выброшено тут как глобальное ``` Это может показаться более привлекательным, чем бесконечная цепочка или произвольные тайм-ауты. Но самая большая проблема заключается в том, что это не является частью стандарта ES6, поэтому, как бы хорошо это ни звучало, в лучшем случае это еще долго не будет надежным и повсеместным решением. Неужели мы просто застряли? Не совсем. Браузеры имеют уникальную возможность, которой нет у нашего кода: они могут отслеживать и точно знать, когда любой объект становится ненужным и собирается сборщиком мусора. Таким образом, браузеры могут отслеживать объекты промисов и всякий раз, когда их собирает сборщик мусора, если они содержат отказ, браузер точно знает, что это была допустимая "непойманная ошибка", и поэтому может с уверенностью утверждать, что должен сообщить о ней в консоль разработчика. **Примечание** На момент написания этой статьи, в обоих Chrome и Firefox есть ранние ранние попытки такого рода возможности "непойманный отказ", хотя поддержка в лучшем случае неполная. Однако, если промис не был собран сборщиком мусора, что очень легко сделать случайно с помощью множества различных шаблонов разработки, осведомленность сборщика мусора браузера не поможет вам узнать и диагностировать, что у вас есть молча отвергнутый промис, находящийся рядом. Есть ли другие альтернативы? Да. ### Яма успеха Нижеследующее является лишь теоретическим, как промисы *могли бы* однажды изменить свое поведение. Я верю, что Я уверен, что это будет намного лучше, чем то, что мы имеем сейчас. И я думаю, что это изменение будет возможно даже в пост-ES6, потому что я не думаю, что это нарушит совместимость с ES6 промисами. Более того, это можно превратить в полифил, если вы будете осторожны. Давайте взглянем: * Промисы могут по умолчанию сообщать (в консоль разработчика) о любом отказе в следующей задаче или тике цикла событий, если в этот момент для промиса не был зарегистрирован обработчик ошибок. * Для случаев, когда вы хотите удерживать отвергнутый промис в таком состоянии на бесконечное количество времени до начала наблюдения за ним, вы могли бы вызвать `defer()`, который подавляет автоматическое уведомление об ошибках в этом промисе. Если промис отвергнут, по умолчанию он шумно сообщает об этом факте в консоль разработчика (вместо тишины по умолчанию). Вы можете отказаться от такой информации, либо неявно (зарегистрировав обработчик ошибок до отказа), или явно (с помощью `defer()`). В любом случае, *вы* управляете ложными срабатываниями. Рассмотрим: ```js var p = Promise.reject( "Ой" ).defer(); // `foo(..)` промисоподобная foo( 42 ) .then( function fulfilled(){ return p; }, function rejected(err){ // обработка ошибок в `foo(..)` } ); ... ``` Когда мы создаем `p`, мы знаем, что мы собираемся подождать некоторое время, чтобы использовать/наблюдать за его отказом, поэтому мы вызываем `defer()` - таким образом, отсутствует глобальная отчетность. `defer()` просто возвращает тот же промис для возможности выстраивания цепочки. Промис, возвращенный из `foo(..)`, получает обработчик ошибок, привязанный *сразу же*, поэтому она выходит "из игры" и никакой глобальной отчетности по этому поводу также не ведется. А вот у промиса, возвращенного из вызова `then(..)`, нет ни `defer()`, ни присоединенного обработчика ошибок, поэтому если он завершается отказом (изнутри любого обработчика разрешения), то *он* будет сообщен в консоль разработчика как не пойманная ошибка. **"Этот подход" - яма успеха.** По умолчанию, все ошибки либо обрабатываются, либо о них получаются уведомления, то, что почти все разработчики в почти всех случаях ожидали бы. Вы либо должны зарегистрировать обработчик, либо вы должны намеренно отказаться и указать, что вы намерены отложить обработку ошибок на *попозже*, вы берете дополнительную ответственность только в этом конкретном случае. Единственная реальная опасность в этом подходе - если вы отложите (`defer()`) промис, а затем не сможете на деле вообще наблюдать/обработать его отказ. Но вы должны были намеренно вызвать `defer()`, чтобы опуститься в эту яму отчаяния, изначально была яма успеха, поэтому мы мало что можем сделать, чтобы спасти вас от ваших собственных ошибок. Я думаю, что все еще есть надежда на обработку ошибок промисов (пост-ES6). Я надеюсь, что власть предержащие переосмыслят ситуацию и рассмотрят эту альтернативу. Тем временем, вы можете реализовать это сами (непростое упражнение для читателя!) или использовать *более умную* библиотеку промисов, которая сделает это за вас! **Примечание** Эта конкретная модель обработки ошибок/сообщений реализована в моей библиотеке абстракций над промисами *asynquence*, которую обсудим в приложении A этой книги. ## Шаблоны промисов Мы уже увидено неявно шаблон последовательности в цепочках промисов (управление потоком это-затем-это-затем-то), но существует множество вариаций асинхронных шаблонов, которые мы можем построить как абстракции над промисами. Этим шаблоны служат для упрощения выражения асинхронного управления потоком, который помогает сделать наш код более более разумным и более поддерживаемым, даже в самых сложных частях наших программ. Два таких шаблона кодируются непосредственно в нативную реализацию ES6 `Promise`, так что мы получаем их бесплатно, чтобы использовать как строительные блоки для других шаблонов. ### Promise.all([ .. ]) В асинхронной последовательности (цепочке промисов) только одна асинхронная задача координируется в любой момент времени, шаг 2 строго следует за шагом 1, а шаг 3 строго следует за шагом 2. А как насчет выполнения двух и более шагов одновременно (т.е. "параллельно")? В классической терминологии программирования, "шлюз" - это механизм, который ожидает завершения двух или более параллельных/ одновременных задач, прежде чем продолжить работу. Не важно в каком порядке они завершатся, а важно только, что все они должны завершиться чтобы открыть шлюз и позволить потоку управлению потоком идти дальше. В API промисов, мы называем этот шаблон `all([ .. ])`. Скажем вы хотели сделать два Ajax-запроса в одно и то же время и дождаться окончания обоих, независимо от их порядка, до выполнения третьего Ajax-запроса. Рассмотрим: ```js // `request(..)` - промис-совместимая Ajax-функция, // примерно как та, что мы определяли ранее в главе var p1 = request( "http://some.url.1/" ); var p2 = request( "http://some.url.2/" ); Promise.all( [p1,p2] ) .then( function(msgs){ // оба `p1` and `p2` завершатся успешно и передадут // свои сообщения сюда return request( "http://some.url.3/?v=" + msgs.join(",") ); } ) .then( function(msg){ console.log( msg ); } ); ``` `Promise.all([ .. ])` ожидает один аргумент, `массив`, состоящий состоящий в целом из экземпляров промисов. Промис, возвращенный из вызова `Promise.all([ .. ])`, получит сообщение о завершении (`msgs` в этом примере кода), которое является `массивом` всех сообщений о завершении от переданных промисов, в том же порядке как они были переданы (независимо от порядка завершения). **Примечание** Технически, `массив` значений, переданный в `Promise.all([ .. ])`, может содержать промисы, then-содержащие или даже непосредственные значения. Каждое значение в списке по сути, проходит через `Promise.resolve(..)`, чтобы убедиться, что ожидается настоящий промис, таким образом непосредственное значение будет просто приведено в промис для этого значения. Если `массив` пустой, основной промис немедленно завершается. Основной промис, возвращенный из `Promise.all([ .. ])`, будет завершен только тогда и если все входящие в него промисы будут завершены. Если любой из этих промисов вместо этого отвергается, основной промис `Promise.all([ .. ])` сразу же отвергается, отбрасывая все результаты любых других промисов. Помните о том, чтобы всегда присоединять обработчик отказа/ошибки к каждому промису, даже и особенно к тому, который возвращается из `Promise.all([ .. ])`. ### Promise.race([ .. ]) В то время как `Promise.all([ .. ])` координирует несколько обещаний одновременно и предполагает, что все они нужны для завершения, иногда вы хотите всего лишь получить ответ от "первого же промиса, пересекшего финишную линию", позволяя других промисам отпасть за ненадобностью. Этот шаблон классически называют "задвижка" (latch), но в промисах он называется "гонка" (race). **Предупреждение:** В то время как метафора "только первый, пересёкший финишную черту, выигрывает" хорошо соответствует поведению, к сожалению "гонка" - это своего рода нагруженный термин, потому что "состояния гонки" - обычно воспринимаются как ошибки в программах (см. главу 1). Не путайте `Promise.race([ .. ])` с "состоянием гонки" (race condition). `Promise.race([ .. ])` также ожидает единственный аргумент в виде `массива`, содержащий один или более промисов, then-содержащих или непосредственных значений. Не имеет большого практического смысла иметь гонку с непосредственными значениями, потому что первое перечисленное значение очевидно выиграет, как в беге, где один бегун стартует с финиша! Аналогично `Promise.all([ .. ])`, `Promise.race([ .. ])` завершится если и тогда, когда любое из разрешений промисов - успешное, и завершится отказом если и тогда, когда любое из разрешений промисов - это отказ. **Предупреждение:** "гонка" требует по меньшей мере одного "бегуна", поэтому если вы передадите пустой `массив`, вместо немедленно разрешения, основной промис `race([..])` никогда не будет разрешен. Это программные "грабли"! ES6 должен был указать, что он либо выполняет, либо отклоняет, либо просто выбрасывает какую-то синхронную ошибку. К сожалению, из-за прецедента в библиотеках промисов, предшествующих ES6 `Promise`, им пришлось оставить эту недоработку, поэтому будьте осторожны и никогда не отправляйте пустой `массив`. Давайте вернемся к нашему предыдущему примеру с параллельным Ajax, но в контексте гонки между `p1` и `p2`: ```js // `request(..)` - это промис-совместимая функция, // подобно той, что мы ранее определили в этой главе var p1 = request( "http://some.url.1/" ); var p2 = request( "http://some.url.2/" ); Promise.race( [p1,p2] ) .then( function(msg){ // либо `p1`, либо `p2` выиграет гонку return request( "http://some.url.3/?v=" + msg ); } ) .then( function(msg){ console.log( msg ); } ); ``` Поскольку побеждает только один промис, значение завершения - это одно сообщение, а не `массив`, как это было в `Promise.all([ .. ])`. #### Гонка тайм-аутов Мы видели этот пример ранее, иллюстрирующий как `Promise.race([ .. ])` может использоваться для выражения шаблона "тайм-аут промиса": ```js // `foo()` - функция, поддерживающая промисы // `timeoutPromise(..)`, определенный ранее, аозвращает // промис, который завершается отказом rejects после указанной задержки // настроить тайм-аут для `foo()` Promise.race( [ foo(), // попробовать вызвать `foo()` timeoutPromise( 3000 ) // дать ему 3 секунды ] ) .then( function(){ // `foo(..)` завершилась успешно и вовремя! }, function(err){ // либо `foo()` завершился отказом, либо просто // не успеет завершиться вовремя, поэтому загляните в // `err`, чтобы узнать причину } ); ``` Этот шаблон тайм-аута работает в большинстве случаев. Но есть некоторые нюансы, которые необходимо учитывать, и, честно говоря, они применимы к обоим `Promise.race([ .. ])` и `Promise.all([ .. ])` в равной степени. #### "Finally" Ключевой вопрос, который необходимо задать: "Что происходит с промисами, который который отбрасываются/игнорируются?" Мы задаем этот вопрос не с точки зрения производительности, они, как правило, попадают в сборку мусора как подходящие кандидаты для этого, а с поведенческий аспекта (побочные эффекты и т.д.). Промисы нельзя отменить, и не должны бы, поскольку это разрушит доверие к внешней неизменяемости, обсуждаемой в секции "Промис неотменяемый" позже в этой главе, поэтому их можно только молча игнорировать. Но что если `foo()` из предыдущего примера резервирует какой-то ресурс для использования, но первым срабатывает тайм-аут и приводит к тому, что этот промис игнорируется? Есть ли в этом шаблоне что-нибудь, что с упреждением освобождает зарезервированный ресурс после истечения тайм-аута или иным образом отменяет любые побочные эффекты, которые он мог иметь? Что если всё, что вы хотели - это зафиксировать факт того, что `foo()` завершился по тайм-ауту? Некоторые разработчики предлагают, что промису нужна регистрация колбека `finally(..)`, который вызывается всегда, когда промис разрешен, и позволяет вам и позволяет вам указать любую очистку, которая может потребоваться. На текущий момент такого нет в спецификации, но может появиться в ES7+. Подождем и посмотрим. Это может выглядеть так: ```js var p = Promise.resolve( 42 ); p.then( something ) .finally( cleanup ) .then( another ) .finally( cleanup ); ``` **Примечание** В различных промис-библиотеках `finally(..)` все еще создает и возвращает новый промис (чтобы продолжать цепочку). Если бы функция `cleanup(..)` возвращала промис, его можно было бы соединить в цепочку, что означает, что у вас все еще могли бы быть проблемы с неразрешенными отказами, которые мы ранее обсуждали. Тем временем, мы могли бы создать статическую вспомогательную функцию, которая позволит нам наблюдать (без вмешательства) за разрешением промиса: ```js // защитная проверка в стиле безопасного полифила if (!Promise.observe) { Promise.observe = function(pr,cb) { // стороннее наблюдение за разрешением `pr` pr.then( function fulfilled(msg){ // запланировать колбек асинхронно (в виде задачи) Promise.resolve( msg ).then( cb ); }, function rejected(err){ // запланировать колбек асинхронно (в виде задачи) Promise.resolve( err ).then( cb ); } ); // вернуть оригинальный промис return pr; }; } ``` Вот как мы используем его в предыдущем примере с тайм-аутом: ```js Promise.race( [ Promise.observe( foo(), // попытка вызова `foo()` function cleanup(msg){ // почистить за `foo()`, даже если она // не завершилась после тайм-аута } ), timeoutPromise( 3000 ) // дать функции тайм-аут в 3 секунды ] ) ``` Это вспомогательная функция `Promise.observe(..)` - просто иллюстрация того, как вы могли бы наблюдать за завершениями промисов без вмешательства в них. В других библиотеках промисов есть свои собственные решения. Независимо от того, как вы это сделаете, скорее всего, у вас будут места, где вы захотите убедиться, что ваши промисы не будут *просто* молча проигнорированы случайно. ### Вариации на тему all([ .. ]) и race([ .. ]) В то время как нативные ES6 промисы идут со встроенными `Promise.all([ .. ])` и `Promise.race([ .. ])`, есть несколько других часто используемых паттернов с вариациями этой семантики: * `none([ .. ])` похож на `all([ .. ])`, но завершения и отказы меняются местами. Все промисы должны быть отвергнуты, отказы становятся значениями завершения, а значения завершения - наоборот. * `any([ .. ])` похож на `all([ .. ])`, но она игнорирует любые отказы, поэтому нужно выполнить только один, а не *все*. * `first([ .. ])` похож на гонку в сочетании с `any([ .. ])`, которая заключается в том, что она игнорирует любые отказы и завершается, как только завершается первый промис. * `last([ .. ])` похож на `first([ .. ])`, но только побеждает самое последнее завершение. Некоторые библиотеки абстракций над промисами обеспечивают такие функции, но вы также можете определить из сами использую механизмы промисов, `race([ .. ])` и `all([ .. ])`. Например, вот как мы могли бы определить `first([ .. ])`: ```js // защитная проверка в стиле безопасного полифила if (!Promise.first) { Promise.first = function(prs) { return new Promise( function(resolve,reject){ // цикл по всем промисам prs.forEach( function(pr){ // нормализовать значение Promise.resolve( pr ) // кто завершится первым, тот и победил, и // приводит к разрешению основного промиса .then( resolve ); } ); } ); }; } ``` **Примечание** Такая реализация `first(..)` не завершается отказом если все ее промисы завершаются отказом; она просто зависает, подобно тому, как работает `Promise.race([])`. При необходимости, вы могли бы добавить дополнительную логику для отслеживания каждого отказа промисов и если все они отвергнуты, вызвать `reject()` для основного промиса. Оставим это как упражнение для читателя. ### Одновременные итерации Иногда вы хотите проходить по списку промисов и выполнить некоторую задачу для них всех, так же, как это можно сделать с синхронными `array`s (например, `forEach(..)`, `map(..)`, `some(..)`, and `every(..)`). Если задача, которую нужно выполнить по отношению к каждому промису, является принципиально синхронной, это отлично работает, точно так же, как мы использовали `forEach(..)` в предыдущем отрывке кода. Но если задачи принципиально асинхронные или могут/должны в противном случае выполняться одновременно, вы можете воспользоваться асинхронными версиями этих функций, предоставляемых многими библиотеками. Например, давайте рассмотрим асинхронную функцию `map(..)`, которая принимает `массив` значений (могут быть промисами или чем-то еще), плюс функцию (задачу), для выполнения над каждым значением. `map(..)` сам по себе возвращает промис, чье значение завершения - `массив`, который хранит (в том же порядке) асинхронное значение завершения из каждой задачи: ```js if (!Promise.map) { Promise.map = function(vals,cb) { // новый промис, который ждет все сопоставленные промисы return Promise.all( // примечание: обычная функция массива `map(..)`, превращает // массив значений в массив промисов vals.map( function(val){ // заменить `val` новым промисом, который // разрешается после того, как `val` асинхронно отмаплена return new Promise( function(resolve){ cb( val, resolve ); } ); } ) ); }; } ``` **Примечание** В этой реализации `map(..)` вы не можете сигнализировать об асинхронном отказе, но если происходит синхронное исключение/ошибка внутри колбека маппинга (`cb(..)`), основной промис, возвращающийся из the main `Promise.map(..)` будет отвергнут. Давайте проиллюстрируем использование `map(..)` со списком промисов (вместо простых значений): ```js var p1 = Promise.resolve( 21 ); var p2 = Promise.resolve( 42 ); var p3 = Promise.reject( "Ой" ); // удвоить значения в списке, даже если они в промисах Promise.map( [p1,p2,p3], function(pr,done){ // убедиться, что само значение - это промис Promise.resolve( pr ) .then( // извлечь значение как `v` function(v){ // отмапить значение завершения `v` в новое значение done( v * 2 ); }, // или отмапить в сообщение отказа промиса done ); } ) .then( function(vals){ console.log( vals ); // [42,84,"Ой"] } ); ``` ## Обзор API промисов Давайте проведем обзор ES6 `Promise` API, которые мы уже в какой-то степени наблюдали в этой главе. **Примечание** Следующий API является нативным только в ES6, но есть полифилы, совместимые со спецификацией (а не просто расширенные библиотеки промисов), которые могут определить `Promise` и все связанное с ним поведение, так что вы можете использовать нативные промисы даже в до-ES6 браузерах. Один такой полифил - это "Native Promise Only" (http://github.com/getify/native-promise-only), который написал я! ### Конструктор new Promise(..) *Доступный конструктор* `Promise(..)` должен использоваться с `new` и в него нужно передать колбек-функцию, которая вызывается синхронно/немедленно. Это функция передается в в два колбека, которые действуют как возможности для разрешения промиса. Мы обычно называем их `resolve(..)` и `reject(..)`: ```js var p = new Promise( function(resolve,reject){ // `resolve(..)` чтобы разрешить/завершить промис // `reject(..)` чтобы отвергнуть промис } ); ``` `reject(..)` просто отвергает промис, а `resolve(..)` может либо завершить промис или отвергнуть его, в зависимости от того, что передано на вход. Если в `resolve(..)` передано непосредственное, не-промис, не-then-содержащее значение, то промис завершается с этим значением. Но если в `resolve(..)` передается настоящий промис или then-содержащее значение, то это значение будет рекурсивно распаковано, и какое бы ни было его окончательное разрешение/состояние - оно будет принято промисом. ### Promise.resolve(..) и Promise.reject(..) Краткий вариант для создания уже отвергнутого промиса - `Promise.reject(..)`, таким образом эти два промиса равнозначны: ```js var p1 = new Promise( function(resolve,reject){ reject( "Ой" ); } ); var p2 = Promise.reject( "Ой" ); ``` `Promise.resolve(..)` обычно используется для создания уже завершенного промиса примерно также как `Promise.reject(..)`. Однако, `Promise.resolve(..)` также распаковывает then-содержащие значения (как уже неоднократно обсуждалось). В этом случае, возвращенный промис принимает окончательное разрешение then-содержащего, которое вы передали, которое может быть либо завершением, либо отказом: ```js var fulfilledTh = { then: function(cb) { cb( 42 ); } }; var rejectedTh = { then: function(cb,errCb) { errCb( "Ой" ); } }; var p1 = Promise.resolve( fulfilledTh ); var p2 = Promise.resolve( rejectedTh ); // `p1` станет завершенным промисом // `p2` станет отвергнутым промисом ``` И помните, `Promise.resolve(..)` ничего не дает, если то, что вы передаете ему, является настоящим промисом, она просто непосредственно вернет это значение. Поэтому нет никаких затрат на вызов `Promise.resolve(..)` со значениями чью природу вы не знаете, если они уже оказались настоящими промисами. ### then(..) и catch(..) Каждый экземпляр промиса (но **не** пространства имен `Promise` API) содержит методы `then(..)` и `catch(..)`, которые позволяют зарегистрировать обработчики завершения и отказа для этого промиса. Как только промис разрешен, будет вызван один из этих обработчиков, но не оба, и он всегда будет вызваться асинхронно (см. "Задачи" в главе 1). `then(..)` принимает один или два параметра, первый - для колбека завершения, а второй - для колбека отказа. Если какой-либо из параметров будет опущен или будет передано значение не-функция, то подставляется колбек по умолчанию соответственно. Колбек завершения по умолчанию просто передает сообщение дальше, в то время как колбек отказа по умолчанию просто заново выбрасывает (распространяет дальше) полученную причину ошибки (propagates). `catch(..)` принимает только колбека отказа как параметр и автоматически подставляет обработчик завершения по умолчанию, как только что упоминалось. Другими словами, это - эквивалент `then(null,..)`: ```js p.then( fulfilled ); p.then( fulfilled, rejected ); p.catch( rejected ); // или `p.then( null, rejected )` ``` `then(..)` и `catch(..)` также создают и возвращают новый промис, который можно использовать для выражения управления потоком цепочки промисов. Если у колбеков завершения или отказа уже есть выброшенное исключение, то возвращаемый промис будет отвергнут. Если какой-либо из колбеков вернет непосредственное, не-промис, не-then-содержащее значение, то это значение будет установлено как завершение возвращенного промиса. Если обработчик завершения специально возвращает промис или then-содержащее значение, то это значение распаковывается и становится разрешением возвращенного промиса. ### Promise.all([ .. ]) и Promise.race([ .. ]) Статические вспомогательные функции `Promise.all([ .. ])` и `Promise.race([ .. ])` в ES6 `Promise` API обе создают промис как свое возвращаемое значение. Разрешение этого промиса целиком управляется массивом промисов, который вы передаете на вход. Для `Promise.all([ .. ])` все промисы, которые вы передаете, должны завершиться, чтобы возвращаемый промис также завершился. Если какой-либо из промисов будет отвергнут, главный возвращаемый промис будет также немедленно отвергнут (отбрасывая результаты любых других промисов). При завершении вы получаете `массив` всех переданных в промисы значений завершения. При отказе, вы получаете только значение причины отказа первого промиса. Этот шаблон классически называется "шлюз (gate)": все должны прибыть до того, как откроется шлюз. Для `Promise.race([ .. ])`, только первый промис с разрешением (завершение или отказ) "выигрывает", и какое бы ни было его разрешение, оно и становится разрешением возвращаемого промиса. Этот шаблон классически называется "задвижка (latch)": первый, кто откроет задвижку - проходит. Представьте: ```js var p1 = Promise.resolve( 42 ); var p2 = Promise.resolve( "Привет, мир" ); var p3 = Promise.reject( "Ой" ); Promise.race( [p1,p2,p3] ) .then( function(msg){ console.log( msg ); // 42 } ); Promise.all( [p1,p2,p3] ) .catch( function(err){ console.error( err ); // "Ой" } ); Promise.all( [p1,p2] ) .then( function(msgs){ console.log( msgs ); // [42,"Привет, мир"] } ); ``` **Предупреждение:** Будьте осторожны! Если в `Promise.all([ .. ])` будет передает пустой массив, то функция завершится немедленно, а вот `Promise.race([ .. ])` повиснет навсегда и никогда не разрешится. ES6 `Promise` API довольно простое и понятное. Оно, по крайней мере, достаточно хорош, чтобы обслуживать самые простые случаи асинхронной работы и это хорошее место для начала перестройки вашего кода из ада колбеков в нечто лучшее. Но существует целый ряд асинхронных сложностей, которые часто требуются приложениям и для которых промисы сами по себе будут ограничены в решении. В следующем разделе мы погрузимся в эти ограничения как мотивацию для создания библиотек промисов. ## Ограничения промисов Многие детали, которые мы обсудим в этом разделе, уже упоминались в этой главе, но мы просто рассмотрим эти ограничения отдельно. ### Обработка ошибок последовательностей Мы подробно рассмотрели обработку ошибок с помощью промисов в начале этой главы. Ограничения в том, как разработаны промисы, в частности, как они связываются в цепочки, создают очень легкую ловушку, когда ошибка в цепочке промисов может быть молча проигнорирована случайно. Но есть еще кое-что, что следует учитывать с ошибками промисов. Поскольку цепочка промисов - это не что иное, как входящие в нее промисы, соединенные вместе, нет никакой сущности, чтобы ссылаться на всю цепочку как на единое *нечто*, что означает, что нет внешнего способа наблюдения за возможными ошибками. Если вы конструируете цепочку промисов, в которой нет обработки ошибок, любая ошибка где бы то ни было в цепочке будет распространяться бесконечно вниз по цепочке, пока не будет замечена (регистрацией обработчика отказов на каком-либо шаге). Так что, в этом конкретном случае, иметь ссылку на *последний* промис в цепочке будет достаточно (`p` в последующем примере кода), потому что вы можете зарегистрировать там обработчик отказов, и он будет уведомлен о любых распространяемых ошибках: ```js // `foo(..)`, `STEP2(..)` и `STEP3(..)` - // обе функции поддерживают промисы var p = foo( 42 ) .then( STEP2 ) .then( STEP3 ); ``` Хотя это может показаться хитрым и запутанным, `p` здесь не указывает на первый промис в цепочке (тот, что получен из вызова `foo(42)`), а вместо этого из последнего промиса, того, который возвращается из вызова `then(STEP3)`. Кроме того, ни один шаг в цепочке промисов не выполняет собственную обработку ошибок. Это означает, что вы могли бы зарегистрировать обработчик ошибок отказа для `p` и он получит уведомление если возникнет какая-нибудь ошибка в цепочке: ``` p.catch( handleErrors ); ``` Но если любой этап цепочки фактически выполняет собственную обработку ошибок (возможно скрыто/абстрагировано от того, что вы можете увидеть), ваш `handleErrors(..)` не получит уведомлений. Возможно это и будет то, что вы хотите, это же был, в конце концов, "обработанный отказ", но это также может и *не* быть тем, что вы хотели. Полное отсутствие возможности получать уведомления (об "уже обработанных" ошибках отказа) является ограничением, которое сужает возможности в некоторых сценариях использования. По сути, это то же самое ограничение, которое существует для блока `try..catch`, который может поймать исключение и просто проглотить его. Таким образом, это не является ограничением **специфичным для промисов**, но это *то*, для чего мы могли бы захотеть наличия обходного пути. К сожалению, во многих случаях не сохраняются ссылки на промежуточные шаги в последовательности цепочки промисов, поэтому без таких ссылок вы не сможете подключить обработчики ошибок для надежного наблюдения за ошибками. ### Единственное значение У промисов по определению есть только одно значение завершения или одна причина отказа. В простых примерах, это не так уж и важно, но в более сложных сценариях вы можете счесть это ограничением. Типичный совет - создать обертку для значений (такую как `объект` или `массив`), чтобы хранить эти многочисленные сообщения. Это решение работает, но это может быть довольно неудобно и утомительно - упаковывать и распаковывать ваши сообщения на каждом шаге вашей цепочки промисов. #### Разделение значений Иногда вы можете воспринять это как сигнал о том, что вы можете/должны разложить проблему на два или более промисов. Представьте, что у вас есть функция `foo(..)`, которая обеспечивает два значения (`x` и `y`) асинхронно: ```js function getY(x) { return new Promise( function(resolve,reject){ setTimeout( function(){ resolve( (3 * x) - 1 ); }, 100 ); } ); } function foo(bar,baz) { var x = bar * baz; return getY( x ) .then( function(y){ // упаковать оба значения в контейнер return [x,y]; } ); } foo( 10, 20 ) .then( function(msgs){ var x = msgs[0]; var y = msgs[1]; console.log( x, y ); // 200 599 } ); ``` Сперва, давайте переставим то. что возвращает `foo(..)` чтобы нам не пришлось упаковывать `x` и `y` в единственное значение `массива`, чтобы для передачи через один промис. Вместо этого мы можем обернуть кажое значение в свой собственный промис: ```js function foo(bar,baz) { var x = bar * baz; // вернуть оба промиса return [ Promise.resolve( x ), getY( x ) ]; } Promise.all( foo( 10, 20 ) ) .then( function(msgs){ var x = msgs[0]; var y = msgs[1]; console.log( x, y ); } ); ``` Действительно ли `массив` промисов лучше, чем `массив` значений, переданных через один промис? Синтаксически это не является большим улучшением. Но этот подход больше отвечает теории дизайна промисов. Теперь легче в будущем рефакторить, чтобы разделить вычисление `x` и `y` на отдельные функции. Это Гораздо чище и гибче позволить вызывающему коду решать, как согласовать эти два промиса используя тут `Promise.all([ .. ])`, но, конечно, не единственный вариант, а не абстрагироваться от таких деталей внутри `foo(..)`. #### Распаковка/разбиение аргументов Присвоения `var x = ..` и `var y = ..` - все еще неудобные накладные расходы. Мы можем использовать некоторые функциональные хитрости (благодарим за предоставленную информацию Реджинальда Брейтуэйта (Reginald Braithwaite), @raganwald в твиттере) во вспомогательной функции: ```js function spread(fn) { return Function.apply.bind( fn, null ); } Promise.all( foo( 10, 20 ) ) .then( spread( function(x,y){ console.log( x, y ); // 200 599 } ) ) ``` Это немного лучше! Конечно, вы можете встроить функциональную магию, чтобы избежать дополнительную функцию: ```js Promise.all( foo( 10, 20 ) ) .then( Function.apply.bind( function(x,y){ console.log( x, y ); // 200 599 }, null ) ); ``` Эти трюки могут быть простыми, но у ES6 есть еще более лучший ответ для нас: деструктуризация. Форма присвоения с деструктуризацией массива выглядит как-то так: ```js Promise.all( foo( 10, 20 ) ) .then( function(msgs){ var [x,y] = msgs; console.log( x, y ); // 200 599 } ); ``` Но лучше всего, ES6 предлагает формат деструктуризации параметра-массива: ```js Promise.all( foo( 10, 20 ) ) .then( function([x,y]){ console.log( x, y ); // 200 599 } ); ``` Теперь мы придерживаемся мантры "одно значение на промис", но сократили до минимума наш вспомогательный шаблон! **Примечание** Для получения дополнительной информации о форматах деструктуризации в ES6 см. книгу *За пределами ES6* этой серии. ### Единственное разрешение Одним из наиболее характерных свойств промисов является то, что промис может быть разрешен только один раз (завершение или отказ). Для многих случаев использования асинхронного кода, вы получаете значение только один раз, поэтому это работает прекрасно. Но есть также множество асинхронных случаев, которые вписываются в другую модель - те, которые больше похожи на события и/или потоки данных. На первый взгляд неясно, насколько хорошо промисы могут вписаться в такие сценарии использования, если это вообще возможно. Без значительной абстракции поверх промисов, они будут совершенно не подходить для обработки множественного разрешения значений. Представьте себе сценарий, в котором вы хотите запустить последовательность асинхронных шагов в ответ на стимул (например, событие), который может произойти несколько раз, например, нажатие кнопки. Это, вероятно, не будет работать так, как вы хотите: ```js // `click(..)` привязывает событие `"click"` к элементу DOM // `request(..)` это ранее определенный Ajax-запрос с поддержкой промисов var p = new Promise( function(resolve,reject){ click( "#mybtn", resolve ); } ); p.then( function(evt){ var btnID = evt.currentTarget.id; return request( "http://some.url.1/?id=" + btnID ); } ) .then( function(text){ console.log( text ); } ); ``` Приведенное здесь поведение работает только в том случае, если ваше приложение требует, чтобы кнопка была нажата только один раз. Если кнопка нажата второй раз, промис `p` уже был разрешен, поэтому второй вызов `resolve(..)` будет проигнорирован. Вместо этого вам, вероятно, придется изменить парадигму, создавая совершенно новую цепочку промисов для каждого срабатывания события: ```js click( "#mybtn", function(evt){ var btnID = evt.currentTarget.id; request( "http://some.url.1/?id=" + btnID ) .then( function(text){ console.log( text ); } ); } ); ``` Этот подход *работает* в том смысле, что для каждого события `"click"` на кнопке будет запускаться совершенно новая последовательность промисов. Но помимо безобразия, связанного с необходимостью определять всю цепочку промисов внутри обработчика события, эта конструкция в некоторых отношениях нарушает идею разделения обязанностей/возможностей (SoC). Вполне возможно, что вы захотите определить обработчик события в другом месте вашего кода, а не там, где вы определяете *ответ* на событие (цепочка промисов). Это довольно неудобно делать в этом шаблоне без вспомогательных механизмов.. **Примечание** Другой способ сформулировать это ограничение заключается в том, что было бы неплохо, если бы мы могли создать некую "наблюдаемую штуку", на которую мы могли бы подписать цепочку промисов. Существуют библиотеки, которые создали такие абстракции (такие как RxJS -- http://rxjs.codeplex.com/), но абстракции могут показаться настолько тяжелыми, что вы больше не можете видеть природу промисов. Такая тяжелая абстракция заставляет задуматься о таких важных вопросах, как: являются ли (вне промисов) эти механизмы настолько *надежными*, насколько сами промисы были разработаны для этого. Мы вернемся к шаблону "Наблюдаемый" в Приложении B. ### Инерция Одним из конкретных препятствий для начала использования промисов в вашем собственном коде является весь существующий код, который еще не поддерживает промисы. Если у вас много кода, основанного на колбеках, гораздо проще просто продолжать кодировать в том же стиле. "Кодовая база в движении (с колбеками) будет оставаться в движении (с колбеками), если не будут приняты меры со стороны умного, знающего промисы разработчика." Промисы предлагают другую парадигму, и поэтому подход к коду может быть разным - от просто немного другого до, в некоторых случаях, радикально другого. Вы должны быть целенаправленными в этом, потому что промисы не будут просто естественным образом вытекать из тех же старых способов выполнения кода, которые до сих пор хорошо вам служили. Рассмотрим следующий сценарий, основанный на колбеке: ```js function foo(x,y,cb) { ajax( "http://some.url.1/?x=" + x + "&y=" + y, cb ); } foo( 11, 31, function(err,text) { if (err) { console.error( err ); } else { console.log( text ); } } ); ``` Очевидно ли сразу, каковы первые шаги по преобразованию этого кода, основанного на колбеках, в код, ориентированный на промисы? Зависит от вашего опыта. Чем больше у вас будет практики, тем естественнее оно будет ощущаться. Но, безусловно, Промисы не только в том, что на этикетке написано, как именно это сделать - универсального ответа не существует, так что ответственность лежит на вас. Как мы уже писали ранее, нам определенно требуется Ajax-функция, которая поддерживает промисы вместо колбеков, которую мы могли бы назвать `request(..)`. Вы можете сделать свою собственную, как мы уже делали. Но накладные расходы, связанные с необходимостью вручную определять промис-совместимые обертки для каждой функции, основанной на колбеках, снижают вероятность того, что вы вообще решите перейти на промис-совместимое кодирование. Промисы не дают прямого ответа на это ограничение. Однако, большинство библиотек промисов предлагают вспомогательные средства. Но даже без библиотеки, представьте вспомогательный код подобный этому: ```js // защитная проверка в стиле безопасного полифила if (!Promise.wrap) { Promise.wrap = function(fn) { return function() { var args = [].slice.call( arguments ); return new Promise( function(resolve,reject){ fn.apply( null, args.concat( function(err,v){ if (err) { reject( err ); } else { resolve( v ); } } ) ); } ); }; }; } ``` Хорошо, это больше, чем просто маленькая тривиальная утилита. Однако, хотя это может выглядеть немного пугающе, все не так плохо, как вы думаете. Она принимает функцию, которая ожидает колбек в стиле ошибка-первым-аргументом, как свой последний параметр и возвращает новый, который автоматически создает промис в качестве возвращаемого значения и подставляет колбек за вас, присоединяя к завершению/отказу промиса. Вместо того, чтобы тратить много времени на обсуждение того, *как* эта вспомогательная функция `Promise.wrap(..)` работает, давайте просто взглянем на то, как мы ее используем: ```js var request = Promise.wrap( ajax ); request( "http://some.url.1/" ) .then( .. ) .. ``` Ого, это было довольно просто! `Promise.wrap(..)` **не** создает промис. Она создает функцию, которая создаст промисы. В каком-то смысле, промисо-генерирующая функция может рассматриваться как "фабрика промисов". Я предлагаю "promisory" в качестве названия для такой вещи ("Promise" + "factory" (промис + фабрика)). Процесс обертывания функции, ожидающей колбек, в функцию с поддержкой промисов иногда называют "поднятием" (lifting) или "промисификацией" (promisifying). Но, похоже, нет стандартного термина для того, как следует называть результирующую функцию, кроме как "поднятая функция", поэтому мне больше нравится "промисофабрика (promisory)", поскольку я считаю его более описательным. **Примечание** Promisory - это не выдуманный термин. Это реальное слово, и его определение означает "содержать или передать промис". Именно это и делают эти функции, так что получается довольно идеальное терминологическое соответствие! Таким образом, `Promise.wrap(ajax)` создает промисофабрику для `ajax(..)`, который мы назвали `request(..)` и эта промисофабрика создает промисы для Ajax-ответов. Если бы все функции уже были бы промисофабриками, нам бы не пришлось создавать из самим, так что лишний шаг - это немного досадно. Но, по крайней мере, шаблон упаковки (обычно) повторяемый, поэтому мы можем поместить его во вспомогательную функцию `Promise.wrap(...)`, как показано на примере, чтобы облегчить кодирование промисов. Итак, возвращаясь к нашему предыдущему примеру, нам нужна промисофабрика для обоих `ajax(..)` и `foo(..)`: ```js // создать промисофабрику для `ajax(..)` var request = Promise.wrap( ajax ); // отрефакторить `foo(..)`, но оставить его снаружи // на основе колбека для совместимости с другими // частями кода пока что, и использовать промис из // `request(..)` только внутри. function foo(x,y,cb) { request( "http://some.url.1/?x=" + x + "&y=" + y ) .then( function fulfilled(text){ cb( null, text ); }, cb ); } // теперь, для целей данного кода, сделаем // промисофабрику для `foo(..)` var betterFoo = Promise.wrap( foo ); // и используем эту промисофабрику betterFoo( 11, 31 ) .then( function fulfilled(text){ console.log( text ); }, function rejected(err){ console.error( err ); } ); ``` Конечно, в то время как мы рефакторим `foo(..)`, чтобы использовать нашу новую промисофабрику `request(..)`, мы могли бы просто превратить саму `foo(..)` в промисофабрику, вместо того, чтобы оставлять основу из колбеков и необходимость создания и использования последующей промисофабрики `betterFoo(..)`. Это решение зависит только от того. надо ли оставлять `foo(..)` колбеко-совместимой с другими частями кода или нет. Представим: ```js // `foo(..)` теперь является промисофабрикой, поскольку она // делегирует работу промисофабрике `request(..)` function foo(x,y) { return request( "http://some.url.1/?x=" + x + "&y=" + y ); } foo( 11, 31 ) .then( .. ) .. ``` В то время как ES6 промисы не оснащены нативно вспомогательными функциями для такого обертывания с помощью промисофабрик, многие библиотеки предоставляют их, либо вы можете сами сделать свои собственные. В любом случае, это конкретное ограничение промисов можно устранить без особых проблем. (конечно, по сравнению с муками ада обратных вызовов!). ### Неотменяемый промис Как только вы создадите промис и зарегистрируете для него обработчик завершения и/или отказа, не будет ничего снаружи, что вы могли бы сделать, чтобы остановить это движение, если произойдет что-то еще, что сделает эту задачу неактуальной. **Примечание** Многие библиотеки абстракций над промисами предоставляют возможности для отмены промисов, но это ужасная идея! Многие разработчики хотели бы, чтобы промисы были изначально спроектированы с возможностью внешней отмены, но проблема в том, что это позволило бы одному потребителю/наблюдателю промиса влиять на способность другого потребителя наблюдать тот же промис. Это нарушает достоверность (внешнюю неизменяемость) будущего значения и, более того, является воплощением анти-паттерна "действие на расстоянии" ("action at a distance" (http://en.wikipedia.org/wiki/Action_at_a_distance_%28computer_programming%29)). Каким бы полезным он ни казался, на самом деле он приведет вас к тому же кошмару, что и колбеки. Рассмотрим наш сценарий с тайм-аутом промиса, описанный ранее: ```js var p = foo( 42 ); Promise.race( [ p, timeoutPromise( 3000 ) ] ) .then( doSomething, handleError ); p.then( function(){ // все равно происходит даже в случае тайм-аута :( } ); ``` "Тайм-аут" был внешним по отношению к `p`, поэтому само `p` выполняется дальше, чего мы, вероятно, не хотим. Одним из вариантов может быть инвазивное определение колбеков разрешения: ```js var OK = true; var p = foo( 42 ); Promise.race( [ p, timeoutPromise( 3000 ) .catch( function(err){ OK = false; throw err; } ) ] ) .then( doSomething, handleError ); p.then( function(){ if (OK) { // выполняется только если не было тайм-аута! :) } } ); ``` Это ужасно. Это работает, но далеко от идеала. В общем случае следует стараться избегать таких сценариев. Но если не можете этого избежать, Уродливость этого решения должна быть подсказкой, что *отмена* - это функциональность, которая находится на более высоком уровне абстракции, поверх промисов. Я бы рекомендовал обращаться за помощью к библиотекам абстракций над промисами, а не разрабатывать их самостоятельно. **Примечание** Моя библиотека абстракций над промисами *asynquence* предоставляет именно такую абстракцию и возможность сделать `abort()` для последовательности, все они будут рассмотрены в Приложении А. Одиночный промис на самом деле не является механизмом управления потоком (по крайней мере, в сильно значимом смысле), а это именно то, к чему относится *отмена*; поэтому отмена промиса будет выглядеть неловко. В отличие от этого, цепочка промисов, взятых в совокупности (то, что я люблю называть "последовательность") - *является* выражением управления потоком, и поэтому уместно, чтобы отмена была определена на этом уровне абстракции. Ни один отдельный промис не должен быть отменяемым, но разумно, чтобы отменяемой была *последовательность*, поскольку вы не передаете последовательность как единое неизменяемое значение, как вы делаете это с обещанием. ### Производительность промисов Данное ограничение является одновременно и простым, и сложным. Сравнивая, сколько частей двигается в базовой цепочке асинхронных задач, основанных на колбеках, с цепочкой промисов, можно заметить, что в промисах происходит гораздо больше операций, а значит, они, естественно, по крайней мере немного медленнее. Вспомните простой список гарантий доверия, которые предоставляют промисы, по сравнению со специальным кодом решения, который пришлось бы накладывать поверх колбеков для достижения того же уровня защиты. Больше работы, больше защитных мер, что означает, что обещания *медленнее* по сравнению с чистыми, ненадежными колбеками. Это очевидно и, вероятно, просто для восприятия. Но насколько медленнее? Ну... На этот вопрос, на самом деле, невероятно сложно ответить всесторонне. Честно говоря, это своего рода сравнение "яблок с апельсинами", так что, вероятно, это неправильный вопрос. На самом деле следует сравнить, насколько специализированная система колбеков со всеми теми же защитами, внедренными вручную, быстрее, чем реализация промисов. Если у промисов и есть обоснованное ограничение производительности, то оно заключается в том, что они не дают возможности выбирать, какие средства защиты надежности вам нужны или не нужны - вы получаете их все и всегда. Тем не менее, если мы признаем, что промис в целом *немного медленнее*, чем его эквивалент в виде не-промис и ненадежного колбека, предполагая, что есть места, где вы можете оправдать отсутствие надежности - означает ли это, что промиса следует избегать повсеместно, как будто все ваше приложение управляется только самым быстрым кодом, какой только может быть? Если ваш код действительно такой, то **подходит ли JavaScript для таких задач?** JavaScript может быть оптимизирован для выполнения приложений с очень высокой производительностью (см. главу 5 и главу 6). Но действительно ли уместно зацикливаться на крошечных компромиссах производительности с промисами в свете всех преимуществ, которые они дают? Еще одна небольшая проблема заключается в том, что Promises делают *все* асинхронным, а это значит, что некоторые немедленно (синхронно) завершенные шаги все равно откладывают выполнение следующего шага до Задачи (см. главу 1). Это означает, что последовательность задач промиса может выполняться чуть медленнее, чем та же самая последовательность с колбеками. Конечно, вопрос заключается в следующем: стоят ли эти потенциальные снижения крошечных долей производительности всех остальных преимуществ промисов, которые мы изложили в этой главе? Я считаю, что практически во всех случаях, когда производительность промисов кажется вам достаточно низкой, чтобы начать беспокоиться, в действительности это антипаттерн - оптимизировать преимущества надежности и комбинируемости промисов, избегая их полностью. Вместо этого вам следует по умолчанию использовать их во всей кодовой базе, а затем профилировать и анализировать ключевые (критические) места вашего приложения. Являются ли промисы *действительно* узким местом, или они просто теоретически замедляют работу? Только *после этого*, вооружившись реальными контрольными показателями (см. главу 6), можно ответственно и разумно исключить промисы именно в тех критических областях, которые были выявлены. Промисы немного медленнее, но в обмен на это вы получаете много надежности, не-Залго предсказуемости, и встроенную способность к композиции. Может быть, ограничением является не их производительность, а ваше недостаточное восприятие их преимуществ? ## Обзор Обещания - это круто. Используйте их. Они решают проблемы *инверсии управления*, которые мучают нас в коде, основанном только на колбеках. Они не избавляются от колбеков, а просто перепоручают их организацию надежному механизму-посреднику, который находится между нами и другой утилитой. Цепочки промисов также начинают решать (хотя, конечно, не идеально) проблему лучшего способа последовательного выражения потока асинхронного кода, что помогает нашему мозгу лучше планировать и поддерживать асинхронный JS-код. Мы увидим еще более удачное решение *этой* проблемы в следующей главе! ================================================ FILE: async & performance/ch4.md ================================================ # You Don't Know JS: Async & Performance # Chapter 4: Generators In Chapter 2, we identified two key drawbacks to expressing async flow control with callbacks: * Callback-based async doesn't fit how our brain plans out steps of a task. * Callbacks aren't trustable or composable because of *inversion of control*. In Chapter 3, we detailed how Promises uninvert the *inversion of control* of callbacks, restoring trustability/composability. Now we turn our attention to expressing async flow control in a sequential, synchronous-looking fashion. The "magic" that makes it possible is ES6 **generators**. ## Breaking Run-to-Completion In Chapter 1, we explained an expectation that JS developers almost universally rely on in their code: once a function starts executing, it runs until it completes, and no other code can interrupt and run in between. As bizarre as it may seem, ES6 introduces a new type of function that does not behave with the run-to-completion behavior. This new type of function is called a "generator." To understand the implications, let's consider this example: ```js var x = 1; function foo() { x++; bar(); // <-- what about this line? console.log( "x:", x ); } function bar() { x++; } foo(); // x: 3 ``` In this example, we know for sure that `bar()` runs in between `x++` and `console.log(x)`. But what if `bar()` wasn't there? Obviously the result would be `2` instead of `3`. Now let's twist your brain. What if `bar()` wasn't present, but it could still somehow run between the `x++` and `console.log(x)` statements? How would that be possible? In **preemptive** multithreaded languages, it would essentially be possible for `bar()` to "interrupt" and run at exactly the right moment between those two statements. But JS is not preemptive, nor is it (currently) multithreaded. And yet, a **cooperative** form of this "interruption" (concurrency) is possible, if `foo()` itself could somehow indicate a "pause" at that part in the code. **Note:** I use the word "cooperative" not only because of the connection to classical concurrency terminology (see Chapter 1), but because as you'll see in the next snippet, the ES6 syntax for indicating a pause point in code is `yield` -- suggesting a politely *cooperative* yielding of control. Here's the ES6 code to accomplish such cooperative concurrency: ```js var x = 1; function *foo() { x++; yield; // pause! console.log( "x:", x ); } function bar() { x++; } ``` **Note:** You will likely see most other JS documentation/code that will format a generator declaration as `function* foo() { .. }` instead of as I've done here with `function *foo() { .. }` -- the only difference being the stylistic positioning of the `*`. The two forms are functionally/syntactically identical, as is a third `function*foo() { .. }` (no space) form. There are arguments for both styles, but I basically prefer `function *foo..` because it then matches when I reference a generator in writing with `*foo()`. If I said only `foo()`, you wouldn't know as clearly if I was talking about a generator or a regular function. It's purely a stylistic preference. Now, how can we run the code in that previous snippet such that `bar()` executes at the point of the `yield` inside of `*foo()`? ```js // construct an iterator `it` to control the generator var it = foo(); // start `foo()` here! it.next(); x; // 2 bar(); x; // 3 it.next(); // x: 3 ``` OK, there's quite a bit of new and potentially confusing stuff in those two code snippets, so we've got plenty to wade through. But before we explain the different mechanics/syntax with ES6 generators, let's walk through the behavior flow: 1. The `it = foo()` operation does *not* execute the `*foo()` generator yet, but it merely constructs an *iterator* that will control its execution. More on *iterators* in a bit. 2. The first `it.next()` starts the `*foo()` generator, and runs the `x++` on the first line of `*foo()`. 3. `*foo()` pauses at the `yield` statement, at which point that first `it.next()` call finishes. At the moment, `*foo()` is still running and active, but it's in a paused state. 4. We inspect the value of `x`, and it's now `2`. 5. We call `bar()`, which increments `x` again with `x++`. 6. We inspect the value of `x` again, and it's now `3`. 7. The final `it.next()` call resumes the `*foo()` generator from where it was paused, and runs the `console.log(..)` statement, which uses the current value of `x` of `3`. Clearly, `*foo()` started, but did *not* run-to-completion -- it paused at the `yield`. We resumed `*foo()` later, and let it finish, but that wasn't even required. So, a generator is a special kind of function that can start and stop one or more times, and doesn't necessarily ever have to finish. While it won't be terribly obvious yet why that's so powerful, as we go throughout the rest of this chapter, that will be one of the fundamental building blocks we use to construct generators-as-async-flow-control as a pattern for our code. ### Input and Output A generator function is a special function with the new processing model we just alluded to. But it's still a function, which means it still has some basic tenets that haven't changed -- namely, that it still accepts arguments (aka "input"), and that it can still return a value (aka "output"): ```js function *foo(x,y) { return x * y; } var it = foo( 6, 7 ); var res = it.next(); res.value; // 42 ``` We pass in the arguments `6` and `7` to `*foo(..)` as the parameters `x` and `y`, respectively. And `*foo(..)` returns the value `42` back to the calling code. We now see a difference with how the generator is invoked compared to a normal function. `foo(6,7)` obviously looks familiar. But subtly, the `*foo(..)` generator hasn't actually run yet as it would have with a function. Instead, we're just creating an *iterator* object, which we assign to the variable `it`, to control the `*foo(..)` generator. Then we call `it.next()`, which instructs the `*foo(..)` generator to advance from its current location, stopping either at the next `yield` or end of the generator. The result of that `next(..)` call is an object with a `value` property on it holding whatever value (if anything) was returned from `*foo(..)`. In other words, `yield` caused a value to be sent out from the generator during the middle of its execution, kind of like an intermediate `return`. Again, it won't be obvious yet why we need this whole indirect *iterator* object to control the generator. We'll get there, I *promise*. #### Iteration Messaging In addition to generators accepting arguments and having return values, there's even more powerful and compelling input/output messaging capability built into them, via `yield` and `next(..)`. Consider: ```js function *foo(x) { var y = x * (yield); return y; } var it = foo( 6 ); // start `foo(..)` it.next(); var res = it.next( 7 ); res.value; // 42 ``` First, we pass in `6` as the parameter `x`. Then we call `it.next()`, and it starts up `*foo(..)`. Inside `*foo(..)`, the `var y = x ..` statement starts to be processed, but then it runs across a `yield` expression. At that point, it pauses `*foo(..)` (in the middle of the assignment statement!), and essentially requests the calling code to provide a result value for the `yield` expression. Next, we call `it.next( 7 )`, which is passing the `7` value back in to *be* that result of the paused `yield` expression. So, at this point, the assignment statement is essentially `var y = 6 * 7`. Now, `return y` returns that `42` value back as the result of the `it.next( 7 )` call. Notice something very important but also easily confusing, even to seasoned JS developers: depending on your perspective, there's a mismatch between the `yield` and the `next(..)` call. In general, you're going to have one more `next(..)` call than you have `yield` statements -- the preceding snippet has one `yield` and two `next(..)` calls. Why the mismatch? Because the first `next(..)` always starts a generator, and runs to the first `yield`. But it's the second `next(..)` call that fulfills the first paused `yield` expression, and the third `next(..)` would fulfill the second `yield`, and so on. ##### Tale of Two Questions Actually, which code you're thinking about primarily will affect whether there's a perceived mismatch or not. Consider only the generator code: ```js var y = x * (yield); return y; ``` This **first** `yield` is basically *asking a question*: "What value should I insert here?" Who's going to answer that question? Well, the **first** `next()` has already run to get the generator up to this point, so obviously *it* can't answer the question. So, the **second** `next(..)` call must answer the question *posed* by the **first** `yield`. See the mismatch -- second-to-first? But let's flip our perspective. Let's look at it not from the generator's point of view, but from the iterator's point of view. To properly illustrate this perspective, we also need to explain that messages can go in both directions -- `yield ..` as an expression can send out messages in response to `next(..)` calls, and `next(..)` can send values to a paused `yield` expression. Consider this slightly adjusted code: ```js function *foo(x) { var y = x * (yield "Hello"); // <-- yield a value! return y; } var it = foo( 6 ); var res = it.next(); // first `next()`, don't pass anything res.value; // "Hello" res = it.next( 7 ); // pass `7` to waiting `yield` res.value; // 42 ``` `yield ..` and `next(..)` pair together as a two-way message passing system **during the execution of the generator**. So, looking only at the *iterator* code: ```js var res = it.next(); // first `next()`, don't pass anything res.value; // "Hello" res = it.next( 7 ); // pass `7` to waiting `yield` res.value; // 42 ``` **Note:** We don't pass a value to the first `next()` call, and that's on purpose. Only a paused `yield` could accept such a value passed by a `next(..)`, and at the beginning of the generator when we call the first `next()`, there **is no paused `yield`** to accept such a value. The specification and all compliant browsers just silently **discard** anything passed to the first `next()`. It's still a bad idea to pass a value, as you're just creating silently "failing" code that's confusing. So, always start a generator with an argument-free `next()`. The first `next()` call (with nothing passed to it) is basically *asking a question*: "What *next* value does the `*foo(..)` generator have to give me?" And who answers this question? The first `yield "hello"` expression. See? No mismatch there. Depending on *who* you think about asking the question, there is either a mismatch between the `yield` and `next(..)` calls, or not. But wait! There's still an extra `next()` compared to the number of `yield` statements. So, that final `it.next(7)` call is again asking the question about what *next* value the generator will produce. But there's no more `yield` statements left to answer, is there? So who answers? The `return` statement answers the question! And if there **is no `return`** in your generator -- `return` is certainly not any more required in generators than in regular functions -- there's always an assumed/implicit `return;` (aka `return undefined;`), which serves the purpose of default answering the question *posed* by the final `it.next(7)` call. These questions and answers -- the two-way message passing with `yield` and `next(..)` -- are quite powerful, but it's not obvious at all how these mechanisms are connected to async flow control. We're getting there! ### Multiple Iterators It may appear from the syntactic usage that when you use an *iterator* to control a generator, you're controlling the declared generator function itself. But there's a subtlety that's easy to miss: each time you construct an *iterator*, you are implicitly constructing an instance of the generator which that *iterator* will control. You can have multiple instances of the same generator running at the same time, and they can even interact: ```js function *foo() { var x = yield 2; z++; var y = yield (x * z); console.log( x, y, z ); } var z = 1; var it1 = foo(); var it2 = foo(); var val1 = it1.next().value; // 2 <-- yield 2 var val2 = it2.next().value; // 2 <-- yield 2 val1 = it1.next( val2 * 10 ).value; // 40 <-- x:20, z:2 val2 = it2.next( val1 * 5 ).value; // 600 <-- x:200, z:3 it1.next( val2 / 2 ); // y:300 // 20 300 3 it2.next( val1 / 4 ); // y:10 // 200 10 3 ``` **Warning:** The most common usage of multiple instances of the same generator running concurrently is not such interactions, but when the generator is producing its own values without input, perhaps from some independently connected resource. We'll talk more about value production in the next section. Let's briefly walk through the processing: 1. Both instances of `*foo()` are started at the same time, and both `next()` calls reveal a `value` of `2` from the `yield 2` statements, respectively. 2. `val2 * 10` is `2 * 10`, which is sent into the first generator instance `it1`, so that `x` gets value `20`. `z` is incremented from `1` to `2`, and then `20 * 2` is `yield`ed out, setting `val1` to `40`. 3. `val1 * 5` is `40 * 5`, which is sent into the second generator instance `it2`, so that `x` gets value `200`. `z` is incremented again, from `2` to `3`, and then `200 * 3` is `yield`ed out, setting `val2` to `600`. 4. `val2 / 2` is `600 / 2`, which is sent into the first generator instance `it1`, so that `y` gets value `300`, then printing out `20 300 3` for its `x y z` values, respectively. 5. `val1 / 4` is `40 / 4`, which is sent into the second generator instance `it2`, so that `y` gets value `10`, then printing out `200 10 3` for its `x y z` values, respectively. That's a "fun" example to run through in your mind. Did you keep it straight? #### Interleaving Recall this scenario from the "Run-to-completion" section of Chapter 1: ```js var a = 1; var b = 2; function foo() { a++; b = b * a; a = b + 3; } function bar() { b--; a = 8 + b; b = a * 2; } ``` With normal JS functions, of course either `foo()` can run completely first, or `bar()` can run completely first, but `foo()` cannot interleave its individual statements with `bar()`. So, there are only two possible outcomes to the preceding program. However, with generators, clearly interleaving (even in the middle of statements!) is possible: ```js var a = 1; var b = 2; function *foo() { a++; yield; b = b * a; a = (yield b) + 3; } function *bar() { b--; yield; a = (yield 8) + b; b = a * (yield 2); } ``` Depending on what respective order the *iterators* controlling `*foo()` and `*bar()` are called, the preceding program could produce several different results. In other words, we can actually illustrate (in a sort of fake-ish way) the theoretical "threaded race conditions" circumstances discussed in Chapter 1, by interleaving the two generator interations over the same shared variables. First, let's make a helper called `step(..)` that controls an *iterator*: ```js function step(gen) { var it = gen(); var last; return function() { // whatever is `yield`ed out, just // send it right back in the next time! last = it.next( last ).value; }; } ``` `step(..)` initializes a generator to create its `it` *iterator*, then returns a function which, when called, advances the *iterator* by one step. Additionally, the previously `yield`ed out value is sent right back in at the *next* step. So, `yield 8` will just become `8` and `yield b` will just be `b` (whatever it was at the time of `yield`). Now, just for fun, let's experiment to see the effects of interleaving these different chunks of `*foo()` and `*bar()`. We'll start with the boring base case, making sure `*foo()` totally finishes before `*bar()` (just like we did in Chapter 1): ```js // make sure to reset `a` and `b` a = 1; b = 2; var s1 = step( foo ); var s2 = step( bar ); // run `*foo()` completely first s1(); s1(); s1(); // now run `*bar()` s2(); s2(); s2(); s2(); console.log( a, b ); // 11 22 ``` The end result is `11` and `22`, just as it was in the Chapter 1 version. Now let's mix up the interleaving ordering and see how it changes the final values of `a` and `b`: ```js // make sure to reset `a` and `b` a = 1; b = 2; var s1 = step( foo ); var s2 = step( bar ); s2(); // b--; s2(); // yield 8 s1(); // a++; s2(); // a = 8 + b; // yield 2 s1(); // b = b * a; // yield b s1(); // a = b + 3; s2(); // b = a * 2; ``` Before I tell you the results, can you figure out what `a` and `b` are after the preceding program? No cheating! ```js console.log( a, b ); // 12 18 ``` **Note:** As an exercise for the reader, try to see how many other combinations of results you can get back rearranging the order of the `s1()` and `s2()` calls. Don't forget you'll always need three `s1()` calls and four `s2()` calls. Recall the discussion earlier about matching `next()` with `yield` for the reasons why. You almost certainly won't want to intentionally create *this* level of interleaving confusion, as it creates incredibly difficult to understand code. But the exercise is interesting and instructive to understand more about how multiple generators can run concurrently in the same shared scope, because there will be places where this capability is quite useful. We'll discuss generator concurrency in more detail at the end of this chapter. ## Generator'ing Values In the previous section, we mentioned an interesting use for generators, as a way to produce values. This is **not** the main focus in this chapter, but we'd be remiss if we didn't cover the basics, especially because this use case is essentially the origin of the name: generators. We're going to take a slight diversion into the topic of *iterators* for a bit, but we'll circle back to how they relate to generators and using a generator to *generate* values. ### Producers and Iterators Imagine you're producing a series of values where each value has a definable relationship to the previous value. To do this, you're going to need a stateful producer that remembers the last value it gave out. You can implement something like that straightforwardly using a function closure (see the *Scope & Closures* title of this series): ```js var gimmeSomething = (function(){ var nextVal; return function(){ if (nextVal === undefined) { nextVal = 1; } else { nextVal = (3 * nextVal) + 6; } return nextVal; }; })(); gimmeSomething(); // 1 gimmeSomething(); // 9 gimmeSomething(); // 33 gimmeSomething(); // 105 ``` **Note:** The `nextVal` computation logic here could have been simplified, but conceptually, we don't want to calculate the *next value* (aka `nextVal`) until the *next* `gimmeSomething()` call happens, because in general that could be a resource-leaky design for producers of more persistent or resource-limited values than simple `number`s. Generating an arbitrary number series isn't a terribly realistic example. But what if you were generating records from a data source? You could imagine much the same code. In fact, this task is a very common design pattern, usually solved by iterators. An *iterator* is a well-defined interface for stepping through a series of values from a producer. The JS interface for iterators, as it is in most languages, is to call `next()` each time you want the next value from the producer. We could implement the standard *iterator* interface for our number series producer: ```js var something = (function(){ var nextVal; return { // needed for `for..of` loops [Symbol.iterator]: function(){ return this; }, // standard iterator interface method next: function(){ if (nextVal === undefined) { nextVal = 1; } else { nextVal = (3 * nextVal) + 6; } return { done:false, value:nextVal }; } }; })(); something.next().value; // 1 something.next().value; // 9 something.next().value; // 33 something.next().value; // 105 ``` **Note:** We'll explain why we need the `[Symbol.iterator]: ..` part of this code snippet in the "Iterables" section. Syntactically though, two ES6 features are at play. First, the `[ .. ]` syntax is called a *computed property name* (see the *this & Object Prototypes* title of this series). It's a way in an object literal definition to specify an expression and use the result of that expression as the name for the property. Next, `Symbol.iterator` is one of ES6's predefined special `Symbol` values (see the *ES6 & Beyond* title of this book series). The `next()` call returns an object with two properties: `done` is a `boolean` value signaling the *iterator's* complete status; `value` holds the iteration value. ES6 also adds the `for..of` loop, which means that a standard *iterator* can automatically be consumed with native loop syntax: ```js for (var v of something) { console.log( v ); // don't let the loop run forever! if (v > 500) { break; } } // 1 9 33 105 321 969 ``` **Note:** Because our `something` *iterator* always returns `done:false`, this `for..of` loop would run forever, which is why we put the `break` conditional in. It's totally OK for iterators to be never-ending, but there are also cases where the *iterator* will run over a finite set of values and eventually return a `done:true`. The `for..of` loop automatically calls `next()` for each iteration -- it doesn't pass any values in to the `next()` -- and it will automatically terminate on receiving a `done:true`. It's quite handy for looping over a set of data. Of course, you could manually loop over iterators, calling `next()` and checking for the `done:true` condition to know when to stop: ```js for ( var ret; (ret = something.next()) && !ret.done; ) { console.log( ret.value ); // don't let the loop run forever! if (ret.value > 500) { break; } } // 1 9 33 105 321 969 ``` **Note:** This manual `for` approach is certainly uglier than the ES6 `for..of` loop syntax, but its advantage is that it affords you the opportunity to pass in values to the `next(..)` calls if necessary. In addition to making your own *iterators*, many built-in data structures in JS (as of ES6), like `array`s, also have default *iterators*: ```js var a = [1,3,5,7,9]; for (var v of a) { console.log( v ); } // 1 3 5 7 9 ``` The `for..of` loop asks `a` for its *iterator*, and automatically uses it to iterate over `a`'s values. **Note:** It may seem a strange omission by ES6, but regular `object`s intentionally do not come with a default *iterator* the way `array`s do. The reasons go deeper than we will cover here. If all you want is to iterate over the properties of an object (with no particular guarantee of ordering), `Object.keys(..)` returns an `array`, which can then be used like `for (var k of Object.keys(obj)) { ..`. Such a `for..of` loop over an object's keys would be similar to a `for..in` loop, except that `Object.keys(..)` does not include properties from the `[[Prototype]]` chain while `for..in` does (see the *this & Object Prototypes* title of this series). ### Iterables The `something` object in our running example is called an *iterator*, as it has the `next()` method on its interface. But a closely related term is *iterable*, which is an `object` that **contains** an *iterator* that can iterate over its values. As of ES6, the way to retrieve an *iterator* from an *iterable* is that the *iterable* must have a function on it, with the name being the special ES6 symbol value `Symbol.iterator`. When this function is called, it returns an *iterator*. Though not required, generally each call should return a fresh new *iterator*. `a` in the previous snippet is an *iterable*. The `for..of` loop automatically calls its `Symbol.iterator` function to construct an *iterator*. But we could of course call the function manually, and use the *iterator* it returns: ```js var a = [1,3,5,7,9]; var it = a[Symbol.iterator](); it.next().value; // 1 it.next().value; // 3 it.next().value; // 5 .. ``` In the previous code listing that defined `something`, you may have noticed this line: ```js [Symbol.iterator]: function(){ return this; } ``` That little bit of confusing code is making the `something` value -- the interface of the `something` *iterator* -- also an *iterable*; it's now both an *iterable* and an *iterator*. Then, we pass `something` to the `for..of` loop: ```js for (var v of something) { .. } ``` The `for..of` loop expects `something` to be an *iterable*, so it looks for and calls its `Symbol.iterator` function. We defined that function to simply `return this`, so it just gives itself back, and the `for..of` loop is none the wiser. ### Generator Iterator Let's turn our attention back to generators, in the context of *iterators*. A generator can be treated as a producer of values that we extract one at a time through an *iterator* interface's `next()` calls. So, a generator itself is not technically an *iterable*, though it's very similar -- when you execute the generator, you get an *iterator* back: ```js function *foo(){ .. } var it = foo(); ``` We can implement the `something` infinite number series producer from earlier with a generator, like this: ```js function *something() { var nextVal; while (true) { if (nextVal === undefined) { nextVal = 1; } else { nextVal = (3 * nextVal) + 6; } yield nextVal; } } ``` **Note:** A `while..true` loop would normally be a very bad thing to include in a real JS program, at least if it doesn't have a `break` or `return` in it, as it would likely run forever, synchronously, and block/lock-up the browser UI. However, in a generator, such a loop is generally totally OK if it has a `yield` in it, as the generator will pause at each iteration, `yield`ing back to the main program and/or to the event loop queue. To put it glibly, "generators put the `while..true` back in JS programming!" That's a fair bit cleaner and simpler, right? Because the generator pauses at each `yield`, the state (scope) of the function `*something()` is kept around, meaning there's no need for the closure boilerplate to preserve variable state across calls. Not only is it simpler code -- we don't have to make our own *iterator* interface -- it actually is more reason-able code, because it more clearly expresses the intent. For example, the `while..true` loop tells us the generator is intended to run forever -- to keep *generating* values as long as we keep asking for them. And now we can use our shiny new `*something()` generator with a `for..of` loop, and you'll see it works basically identically: ```js for (var v of something()) { console.log( v ); // don't let the loop run forever! if (v > 500) { break; } } // 1 9 33 105 321 969 ``` But don't skip over `for (var v of something()) ..`! We didn't just reference `something` as a value like in earlier examples, but instead called the `*something()` generator to get its *iterator* for the `for..of` loop to use. If you're paying close attention, two questions may arise from this interaction between the generator and the loop: * Why couldn't we say `for (var v of something) ..`? Because `something` here is a generator, which is not an *iterable*. We have to call `something()` to construct a producer for the `for..of` loop to iterate over. * The `something()` call produces an *iterator*, but the `for..of` loop wants an *iterable*, right? Yep. The generator's *iterator* also has a `Symbol.iterator` function on it, which basically does a `return this`, just like the `something` *iterable* we defined earlier. In other words, a generator's *iterator* is also an *iterable*! #### Stopping the Generator In the previous example, it would appear the *iterator* instance for the `*something()` generator was basically left in a suspended state forever after the `break` in the loop was called. But there's a hidden behavior that takes care of that for you. "Abnormal completion" (i.e., "early termination") of the `for..of` loop -- generally caused by a `break`, `return`, or an uncaught exception -- sends a signal to the generator's *iterator* for it to terminate. **Note:** Technically, the `for..of` loop also sends this signal to the *iterator* at the normal completion of the loop. For a generator, that's essentially a moot operation, as the generator's *iterator* had to complete first so the `for..of` loop completed. However, custom *iterators* might desire to receive this additional signal from `for..of` loop consumers. While a `for..of` loop will automatically send this signal, you may wish to send the signal manually to an *iterator*; you do this by calling `return(..)`. If you specify a `try..finally` clause inside the generator, it will always be run even when the generator is externally completed. This is useful if you need to clean up resources (database connections, etc.): ```js function *something() { try { var nextVal; while (true) { if (nextVal === undefined) { nextVal = 1; } else { nextVal = (3 * nextVal) + 6; } yield nextVal; } } // cleanup clause finally { console.log( "cleaning up!" ); } } ``` The earlier example with `break` in the `for..of` loop will trigger the `finally` clause. But you could instead manually terminate the generator's *iterator* instance from the outside with `return(..)`: ```js var it = something(); for (var v of it) { console.log( v ); // don't let the loop run forever! if (v > 500) { console.log( // complete the generator's iterator it.return( "Hello World" ).value ); // no `break` needed here } } // 1 9 33 105 321 969 // cleaning up! // Hello World ``` When we call `it.return(..)`, it immediately terminates the generator, which of course runs the `finally` clause. Also, it sets the returned `value` to whatever you passed in to `return(..)`, which is how `"Hello World"` comes right back out. We also don't need to include a `break` now because the generator's *iterator* is set to `done:true`, so the `for..of` loop will terminate on its next iteration. Generators owe their namesake mostly to this *consuming produced values* use. But again, that's just one of the uses for generators, and frankly not even the main one we're concerned with in the context of this book. But now that we more fully understand some of the mechanics of how they work, we can *next* turn our attention to how generators apply to async concurrency. ## Iterating Generators Asynchronously What do generators have to do with async coding patterns, fixing problems with callbacks, and the like? Let's get to answering that important question. We should revisit one of our scenarios from Chapter 3. Let's recall the callback approach: ```js function foo(x,y,cb) { ajax( "http://some.url.1/?x=" + x + "&y=" + y, cb ); } foo( 11, 31, function(err,text) { if (err) { console.error( err ); } else { console.log( text ); } } ); ``` If we wanted to express this same task flow control with a generator, we could do: ```js function foo(x,y) { ajax( "http://some.url.1/?x=" + x + "&y=" + y, function(err,data){ if (err) { // throw an error into `*main()` it.throw( err ); } else { // resume `*main()` with received `data` it.next( data ); } } ); } function *main() { try { var text = yield foo( 11, 31 ); console.log( text ); } catch (err) { console.error( err ); } } var it = main(); // start it all up! it.next(); ``` At first glance, this snippet is longer, and perhaps a little more complex looking, than the callback snippet before it. But don't let that impression get you off track. The generator snippet is actually **much** better! But there's a lot going on for us to explain. First, let's look at this part of the code, which is the most important: ```js var text = yield foo( 11, 31 ); console.log( text ); ``` Think about how that code works for a moment. We're calling a normal function `foo(..)` and we're apparently able to get back the `text` from the Ajax call, even though it's asynchronous. How is that possible? If you recall the beginning of Chapter 1, we had almost identical code: ```js var data = ajax( "..url 1.." ); console.log( data ); ``` And that code didn't work! Can you spot the difference? It's the `yield` used in a generator. That's the magic! That's what allows us to have what appears to be blocking, synchronous code, but it doesn't actually block the whole program; it only pauses/blocks the code in the generator itself. In `yield foo(11,31)`, first the `foo(11,31)` call is made, which returns nothing (aka `undefined`), so we're making a call to request data, but we're actually then doing `yield undefined`. That's OK, because the code is not currently relying on a `yield`ed value to do anything interesting. We'll revisit this point later in the chapter. We're not using `yield` in a message passing sense here, only in a flow control sense to pause/block. Actually, it will have message passing, but only in one direction, after the generator is resumed. So, the generator pauses at the `yield`, essentially asking the question, "what value should I return to assign to the variable `text`?" Who's going to answer that question? Look at `foo(..)`. If the Ajax request is successful, we call: ```js it.next( data ); ``` That's resuming the generator with the response data, which means that our paused `yield` expression receives that value directly, and then as it restarts the generator code, that value gets assigned to the local variable `text`. Pretty cool, huh? Take a step back and consider the implications. We have totally synchronous-looking code inside the generator (other than the `yield` keyword itself), but hidden behind the scenes, inside of `foo(..)`, the operations can complete asynchronously. **That's huge!** That's a nearly perfect solution to our previously stated problem with callbacks not being able to express asynchrony in a sequential, synchronous fashion that our brains can relate to. In essence, we are abstracting the asynchrony away as an implementation detail, so that we can reason synchronously/sequentially about our flow control: "Make an Ajax request, and when it finishes print out the response." And of course, we just expressed two steps in the flow control, but this same capability extends without bounds, to let us express however many steps we need to. **Tip:** This is such an important realization, just go back and read the last three paragraphs again to let it sink in! ### Synchronous Error Handling But the preceding generator code has even more goodness to *yield* to us. Let's turn our attention to the `try..catch` inside the generator: ```js try { var text = yield foo( 11, 31 ); console.log( text ); } catch (err) { console.error( err ); } ``` How does this work? The `foo(..)` call is asynchronously completing, and doesn't `try..catch` fail to catch asynchronous errors, as we looked at in Chapter 3? We already saw how the `yield` lets the assignment statement pause to wait for `foo(..)` to finish, so that the completed response can be assigned to `text`. The awesome part is that this `yield` pausing *also* allows the generator to `catch` an error. We throw that error into the generator with this part of the earlier code listing: ```js if (err) { // throw an error into `*main()` it.throw( err ); } ``` The `yield`-pause nature of generators means that not only do we get synchronous-looking `return` values from async function calls, but we can also synchronously `catch` errors from those async function calls! So we've seen we can throw errors *into* a generator, but what about throwing errors *out of* a generator? Exactly as you'd expect: ```js function *main() { var x = yield "Hello World"; yield x.toLowerCase(); // cause an exception! } var it = main(); it.next().value; // Hello World try { it.next( 42 ); } catch (err) { console.error( err ); // TypeError } ``` Of course, we could have manually thrown an error with `throw ..` instead of causing an exception. We can even `catch` the same error that we `throw(..)` into the generator, essentially giving the generator a chance to handle it but if it doesn't, the *iterator* code must handle it: ```js function *main() { var x = yield "Hello World"; // never gets here console.log( x ); } var it = main(); it.next(); try { // will `*main()` handle this error? we'll see! it.throw( "Oops" ); } catch (err) { // nope, didn't handle it! console.error( err ); // Oops } ``` Synchronous-looking error handling (via `try..catch`) with async code is a huge win for readability and reason-ability. ## Generators + Promises In our previous discussion, we showed how generators can be iterated asynchronously, which is a huge step forward in sequential reason-ability over the spaghetti mess of callbacks. But we lost something very important: the trustability and composability of Promises (see Chapter 3)! Don't worry -- we can get that back. The best of all worlds in ES6 is to combine generators (synchronous-looking async code) with Promises (trustable and composable). But how? Recall from Chapter 3 the Promise-based approach to our running Ajax example: ```js function foo(x,y) { return request( "http://some.url.1/?x=" + x + "&y=" + y ); } foo( 11, 31 ) .then( function(text){ console.log( text ); }, function(err){ console.error( err ); } ); ``` In our earlier generator code for the running Ajax example, `foo(..)` returned nothing (`undefined`), and our *iterator* control code didn't care about that `yield`ed value. But here the Promise-aware `foo(..)` returns a promise after making the Ajax call. That suggests that we could construct a promise with `foo(..)` and then `yield` it from the generator, and then the *iterator* control code would receive that promise. But what should the *iterator* do with the promise? It should listen for the promise to resolve (fulfillment or rejection), and then either resume the generator with the fulfillment message or throw an error into the generator with the rejection reason. Let me repeat that, because it's so important. The natural way to get the most out of Promises and generators is **to `yield` a Promise**, and wire that Promise to control the generator's *iterator*. Let's give it a try! First, we'll put the Promise-aware `foo(..)` together with the generator `*main()`: ```js function foo(x,y) { return request( "http://some.url.1/?x=" + x + "&y=" + y ); } function *main() { try { var text = yield foo( 11, 31 ); console.log( text ); } catch (err) { console.error( err ); } } ``` The most powerful revelation in this refactor is that the code inside `*main()` **did not have to change at all!** Inside the generator, whatever values are `yield`ed out is just an opaque implementation detail, so we're not even aware it's happening, nor do we need to worry about it. But how are we going to run `*main()` now? We still have some of the implementation plumbing work to do, to receive and wire up the `yield`ed promise so that it resumes the generator upon resolution. We'll start by trying that manually: ```js var it = main(); var p = it.next().value; // wait for the `p` promise to resolve p.then( function(text){ it.next( text ); }, function(err){ it.throw( err ); } ); ``` Actually, that wasn't so painful at all, was it? This snippet should look very similar to what we did earlier with the manually wired generator controlled by the error-first callback. Instead of an `if (err) { it.throw..`, the promise already splits fulfillment (success) and rejection (failure) for us, but otherwise the *iterator* control is identical. Now, we've glossed over some important details. Most importantly, we took advantage of the fact that we knew that `*main()` only had one Promise-aware step in it. What if we wanted to be able to Promise-drive a generator no matter how many steps it has? We certainly don't want to manually write out the Promise chain differently for each generator! What would be much nicer is if there was a way to repeat (aka "loop" over) the iteration control, and each time a Promise comes out, wait on its resolution before continuing. Also, what if the generator throws out an error (intentionally or accidentally) during the `it.next(..)` call? Should we quit, or should we `catch` it and send it right back in? Similarly, what if we `it.throw(..)` a Promise rejection into the generator, but it's not handled, and comes right back out? ### Promise-Aware Generator Runner The more you start to explore this path, the more you realize, "wow, it'd be great if there was just some utility to do it for me." And you're absolutely correct. This is such an important pattern, and you don't want to get it wrong (or exhaust yourself repeating it over and over), so your best bet is to use a utility that is specifically designed to *run* Promise-`yield`ing generators in the manner we've illustrated. Several Promise abstraction libraries provide just such a utility, including my *asynquence* library and its `runner(..)`, which will be discussed in Appendix A of this book. But for the sake of learning and illustration, let's just define our own standalone utility that we'll call `run(..)`: ```js // thanks to Benjamin Gruenbaum (@benjamingr on GitHub) for // big improvements here! function run(gen) { var args = [].slice.call( arguments, 1), it; // initialize the generator in the current context it = gen.apply( this, args ); // return a promise for the generator completing return Promise.resolve() .then( function handleNext(value){ // run to the next yielded value var next = it.next( value ); return (function handleResult(next){ // generator has completed running? if (next.done) { return next.value; } // otherwise keep going else { return Promise.resolve( next.value ) .then( // resume the async loop on // success, sending the resolved // value back into the generator handleNext, // if `value` is a rejected // promise, propagate error back // into the generator for its own // error handling function handleErr(err) { return Promise.resolve( it.throw( err ) ) .then( handleResult ); } ); } })(next); } ); } ``` As you can see, it's a quite a bit more complex than you'd probably want to author yourself, and you especially wouldn't want to repeat this code for each generator you use. So, a utility/library helper is definitely the way to go. Nevertheless, I encourage you to spend a few minutes studying that code listing to get a better sense of how to manage the generator+Promise negotiation. How would you use `run(..)` with `*main()` in our *running* Ajax example? ```js function *main() { // .. } run( main ); ``` That's it! The way we wired `run(..)`, it will automatically advance the generator you pass to it, asynchronously until completion. **Note:** The `run(..)` we defined returns a promise which is wired to resolve once the generator is complete, or receive an uncaught exception if the generator doesn't handle it. We don't show that capability here, but we'll come back to it later in the chapter. #### ES7: `async` and `await`? The preceding pattern -- generators yielding Promises that then control the generator's *iterator* to advance it to completion -- is such a powerful and useful approach, it would be nicer if we could do it without the clutter of the library utility helper (aka `run(..)`). There's probably good news on that front. At the time of this writing, there's early but strong support for a proposal for more syntactic addition in this realm for the post-ES6, ES7-ish timeframe. Obviously, it's too early to guarantee the details, but there's a pretty decent chance it will shake out similar to the following: ```js function foo(x,y) { return request( "http://some.url.1/?x=" + x + "&y=" + y ); } async function main() { try { var text = await foo( 11, 31 ); console.log( text ); } catch (err) { console.error( err ); } } main(); ``` As you can see, there's no `run(..)` call (meaning no need for a library utility!) to invoke and drive `main()` -- it's just called as a normal function. Also, `main()` isn't declared as a generator function anymore; it's a new kind of function: `async function`. And finally, instead of `yield`ing a Promise, we `await` for it to resolve. The `async function` automatically knows what to do if you `await` a Promise -- it will pause the function (just like with generators) until the Promise resolves. We didn't illustrate it in this snippet, but calling an async function like `main()` automatically returns a promise that's resolved whenever the function finishes completely. **Tip:** The `async` / `await` syntax should look very familiar to readers with experience in C#, because it's basically identical. The proposal essentially codifies support for the pattern we've already derived, into a syntactic mechanism: combining Promises with sync-looking flow control code. That's the best of both worlds combined, to effectively address practically all of the major concerns we outlined with callbacks. The mere fact that such a ES7-ish proposal already exists and has early support and enthusiasm is a major vote of confidence in the future importance of this async pattern. ### Promise Concurrency in Generators So far, all we've demonstrated is a single-step async flow with Promises+generators. But real-world code will often have many async steps. If you're not careful, the sync-looking style of generators may lull you into complacency with how you structure your async concurrency, leading to suboptimal performance patterns. So we want to spend a little time exploring the options. Imagine a scenario where you need to fetch data from two different sources, then combine those responses to make a third request, and finally print out the last response. We explored a similar scenario with Promises in Chapter 3, but let's reconsider it in the context of generators. Your first instinct might be something like: ```js function *foo() { var r1 = yield request( "http://some.url.1" ); var r2 = yield request( "http://some.url.2" ); var r3 = yield request( "http://some.url.3/?v=" + r1 + "," + r2 ); console.log( r3 ); } // use previously defined `run(..)` utility run( foo ); ``` This code will work, but in the specifics of our scenario, it's not optimal. Can you spot why? Because the `r1` and `r2` requests can -- and for performance reasons, *should* -- run concurrently, but in this code they will run sequentially; the `"http://some.url.2"` URL isn't Ajax fetched until after the `"http://some.url.1"` request is finished. These two requests are independent, so the better performance approach would likely be to have them run at the same time. But how exactly would you do that with a generator and `yield`? We know that `yield` is only a single pause point in the code, so you can't really do two pauses at the same time. The most natural and effective answer is to base the async flow on Promises, specifically on their capability to manage state in a time-independent fashion (see "Future Value" in Chapter 3). The simplest approach: ```js function *foo() { // make both requests "in parallel" var p1 = request( "http://some.url.1" ); var p2 = request( "http://some.url.2" ); // wait until both promises resolve var r1 = yield p1; var r2 = yield p2; var r3 = yield request( "http://some.url.3/?v=" + r1 + "," + r2 ); console.log( r3 ); } // use previously defined `run(..)` utility run( foo ); ``` Why is this different from the previous snippet? Look at where the `yield` is and is not. `p1` and `p2` are promises for Ajax requests made concurrently (aka "in parallel"). It doesn't matter which one finishes first, because promises will hold onto their resolved state for as long as necessary. Then we use two subsequent `yield` statements to wait for and retrieve the resolutions from the promises (into `r1` and `r2`, respectively). If `p1` resolves first, the `yield p1` resumes first then waits on the `yield p2` to resume. If `p2` resolves first, it will just patiently hold onto that resolution value until asked, but the `yield p1` will hold on first, until `p1` resolves. Either way, both `p1` and `p2` will run concurrently, and both have to finish, in either order, before the `r3 = yield request..` Ajax request will be made. If that flow control processing model sounds familiar, it's basically the same as what we identified in Chapter 3 as the "gate" pattern, enabled by the `Promise.all([ .. ])` utility. So, we could also express the flow control like this: ```js function *foo() { // make both requests "in parallel," and // wait until both promises resolve var results = yield Promise.all( [ request( "http://some.url.1" ), request( "http://some.url.2" ) ] ); var r1 = results[0]; var r2 = results[1]; var r3 = yield request( "http://some.url.3/?v=" + r1 + "," + r2 ); console.log( r3 ); } // use previously defined `run(..)` utility run( foo ); ``` **Note:** As we discussed in Chapter 3, we can even use ES6 destructuring assignment to simplify the `var r1 = .. var r2 = ..` assignments, with `var [r1,r2] = results`. In other words, all of the concurrency capabilities of Promises are available to us in the generator+Promise approach. So in any place where you need more than sequential this-then-that async flow control steps, Promises are likely your best bet. #### Promises, Hidden As a word of stylistic caution, be careful about how much Promise logic you include **inside your generators**. The whole point of using generators for asynchrony in the way we've described is to create simple, sequential, sync-looking code, and to hide as much of the details of asynchrony away from that code as possible. For example, this might be a cleaner approach: ```js // note: normal function, not generator function bar(url1,url2) { return Promise.all( [ request( url1 ), request( url2 ) ] ); } function *foo() { // hide the Promise-based concurrency details // inside `bar(..)` var results = yield bar( "http://some.url.1", "http://some.url.2" ); var r1 = results[0]; var r2 = results[1]; var r3 = yield request( "http://some.url.3/?v=" + r1 + "," + r2 ); console.log( r3 ); } // use previously defined `run(..)` utility run( foo ); ``` Inside `*foo()`, it's cleaner and clearer that all we're doing is just asking `bar(..)` to get us some `results`, and we'll `yield`-wait on that to happen. We don't have to care that under the covers a `Promise.all([ .. ])` Promise composition will be used to make that happen. **We treat asynchrony, and indeed Promises, as an implementation detail.** Hiding your Promise logic inside a function that you merely call from your generator is especially useful if you're going to do a sophisticated series flow-control. For example: ```js function bar() { Promise.all( [ baz( .. ) .then( .. ), Promise.race( [ .. ] ) ] ) .then( .. ) } ``` That kind of logic is sometimes required, and if you dump it directly inside your generator(s), you've defeated most of the reason why you would want to use generators in the first place. We *should* intentionally abstract such details away from our generator code so that they don't clutter up the higher level task expression. Beyond creating code that is both functional and performant, you should also strive to make code that is as reason-able and maintainable as possible. **Note:** Abstraction is not *always* a healthy thing for programming -- many times it can increase complexity in exchange for terseness. But in this case, I believe it's much healthier for your generator+Promise async code than the alternatives. As with all such advice, though, pay attention to your specific situations and make proper decisions for you and your team. ## Generator Delegation In the previous section, we showed calling regular functions from inside a generator, and how that remains a useful technique for abstracting away implementation details (like async Promise flow). But the main drawback of using a normal function for this task is that it has to behave by the normal function rules, which means it cannot pause itself with `yield` like a generator can. It may then occur to you that you might try to call one generator from another generator, using our `run(..)` helper, such as: ```js function *foo() { var r2 = yield request( "http://some.url.2" ); var r3 = yield request( "http://some.url.3/?v=" + r2 ); return r3; } function *bar() { var r1 = yield request( "http://some.url.1" ); // "delegating" to `*foo()` via `run(..)` var r3 = yield run( foo ); console.log( r3 ); } run( bar ); ``` We run `*foo()` inside of `*bar()` by using our `run(..)` utility again. We take advantage here of the fact that the `run(..)` we defined earlier returns a promise which is resolved when its generator is run to completion (or errors out), so if we `yield` out to a `run(..)` instance the promise from another `run(..)` call, it automatically pauses `*bar()` until `*foo()` finishes. But there's an even better way to integrate calling `*foo()` into `*bar()`, and it's called `yield`-delegation. The special syntax for `yield`-delegation is: `yield * __` (notice the extra `*`). Before we see it work in our previous example, let's look at a simpler scenario: ```js function *foo() { console.log( "`*foo()` starting" ); yield 3; yield 4; console.log( "`*foo()` finished" ); } function *bar() { yield 1; yield 2; yield *foo(); // `yield`-delegation! yield 5; } var it = bar(); it.next().value; // 1 it.next().value; // 2 it.next().value; // `*foo()` starting // 3 it.next().value; // 4 it.next().value; // `*foo()` finished // 5 ``` **Note:** Similar to a note earlier in the chapter where I explained why I prefer `function *foo() ..` instead of `function* foo() ..`, I also prefer -- differing from most other documentation on the topic -- to say `yield *foo()` instead of `yield* foo()`. The placement of the `*` is purely stylistic and up to your best judgment. But I find the consistency of styling attractive. How does the `yield *foo()` delegation work? First, calling `foo()` creates an *iterator* exactly as we've already seen. Then, `yield *` delegates/transfers the *iterator* instance control (of the present `*bar()` generator) over to this other `*foo()` *iterator*. So, the first two `it.next()` calls are controlling `*bar()`, but when we make the third `it.next()` call, now `*foo()` starts up, and now we're controlling `*foo()` instead of `*bar()`. That's why it's called delegation -- `*bar()` delegated its iteration control to `*foo()`. As soon as the `it` *iterator* control exhausts the entire `*foo()` *iterator*, it automatically returns to controlling `*bar()`. So now back to the previous example with the three sequential Ajax requests: ```js function *foo() { var r2 = yield request( "http://some.url.2" ); var r3 = yield request( "http://some.url.3/?v=" + r2 ); return r3; } function *bar() { var r1 = yield request( "http://some.url.1" ); // "delegating" to `*foo()` via `yield*` var r3 = yield *foo(); console.log( r3 ); } run( bar ); ``` The only difference between this snippet and the version used earlier is the use of `yield *foo()` instead of the previous `yield run(foo)`. **Note:** `yield *` yields iteration control, not generator control; when you invoke the `*foo()` generator, you're now `yield`-delegating to its *iterator*. But you can actually `yield`-delegate to any *iterable*; `yield *[1,2,3]` would consume the default *iterator* for the `[1,2,3]` array value. ### Why Delegation? The purpose of `yield`-delegation is mostly code organization, and in that way is symmetrical with normal function calling. Imagine two modules that respectively provide methods `foo()` and `bar()`, where `bar()` calls `foo()`. The reason the two are separate is generally because the proper organization of code for the program calls for them to be in separate functions. For example, there may be cases where `foo()` is called standalone, and other places where `bar()` calls `foo()`. For all these exact same reasons, keeping generators separate aids in program readability, maintenance, and debuggability. In that respect, `yield *` is a syntactic shortcut for manually iterating over the steps of `*foo()` while inside of `*bar()`. Such manual approach would be especially complex if the steps in `*foo()` were asynchronous, which is why you'd probably need to use that `run(..)` utility to do it. And as we've shown, `yield *foo()` eliminates the need for a sub-instance of the `run(..)` utility (like `run(foo)`). ### Delegating Messages You may wonder how this `yield`-delegation works not just with *iterator* control but with the two-way message passing. Carefully follow the flow of messages in and out, through the `yield`-delegation: ```js function *foo() { console.log( "inside `*foo()`:", yield "B" ); console.log( "inside `*foo()`:", yield "C" ); return "D"; } function *bar() { console.log( "inside `*bar()`:", yield "A" ); // `yield`-delegation! console.log( "inside `*bar()`:", yield *foo() ); console.log( "inside `*bar()`:", yield "E" ); return "F"; } var it = bar(); console.log( "outside:", it.next().value ); // outside: A console.log( "outside:", it.next( 1 ).value ); // inside `*bar()`: 1 // outside: B console.log( "outside:", it.next( 2 ).value ); // inside `*foo()`: 2 // outside: C console.log( "outside:", it.next( 3 ).value ); // inside `*foo()`: 3 // inside `*bar()`: D // outside: E console.log( "outside:", it.next( 4 ).value ); // inside `*bar()`: 4 // outside: F ``` Pay particular attention to the processing steps after the `it.next(3)` call: 1. The `3` value is passed (through the `yield`-delegation in `*bar()`) into the waiting `yield "C"` expression inside of `*foo()`. 2. `*foo()` then calls `return "D"`, but this value doesn't get returned all the way back to the outside `it.next(3)` call. 3. Instead, the `"D"` value is sent as the result of the waiting `yield *foo()` expression inside of `*bar()` -- this `yield`-delegation expression has essentially been paused while all of `*foo()` was exhausted. So `"D"` ends up inside of `*bar()` for it to print out. 4. `yield "E"` is called inside of `*bar()`, and the `"E"` value is yielded to the outside as the result of the `it.next(3)` call. From the perspective of the external *iterator* (`it`), it doesn't appear any differently between controlling the initial generator or a delegated one. In fact, `yield`-delegation doesn't even have to be directed to another generator; it can just be directed to a non-generator, general *iterable*. For example: ```js function *bar() { console.log( "inside `*bar()`:", yield "A" ); // `yield`-delegation to a non-generator! console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] ); console.log( "inside `*bar()`:", yield "E" ); return "F"; } var it = bar(); console.log( "outside:", it.next().value ); // outside: A console.log( "outside:", it.next( 1 ).value ); // inside `*bar()`: 1 // outside: B console.log( "outside:", it.next( 2 ).value ); // outside: C console.log( "outside:", it.next( 3 ).value ); // outside: D console.log( "outside:", it.next( 4 ).value ); // inside `*bar()`: undefined // outside: E console.log( "outside:", it.next( 5 ).value ); // inside `*bar()`: 5 // outside: F ``` Notice the differences in where the messages were received/reported between this example and the one previous. Most strikingly, the default `array` *iterator* doesn't care about any messages sent in via `next(..)` calls, so the values `2`, `3`, and `4` are essentially ignored. Also, because that *iterator* has no explicit `return` value (unlike the previously used `*foo()`), the `yield *` expression gets an `undefined` when it finishes. #### Exceptions Delegated, Too! In the same way that `yield`-delegation transparently passes messages through in both directions, errors/exceptions also pass in both directions: ```js function *foo() { try { yield "B"; } catch (err) { console.log( "error caught inside `*foo()`:", err ); } yield "C"; throw "D"; } function *bar() { yield "A"; try { yield *foo(); } catch (err) { console.log( "error caught inside `*bar()`:", err ); } yield "E"; yield *baz(); // note: can't get here! yield "G"; } function *baz() { throw "F"; } var it = bar(); console.log( "outside:", it.next().value ); // outside: A console.log( "outside:", it.next( 1 ).value ); // outside: B console.log( "outside:", it.throw( 2 ).value ); // error caught inside `*foo()`: 2 // outside: C console.log( "outside:", it.next( 3 ).value ); // error caught inside `*bar()`: D // outside: E try { console.log( "outside:", it.next( 4 ).value ); } catch (err) { console.log( "error caught outside:", err ); } // error caught outside: F ``` Some things to note from this snippet: 1. When we call `it.throw(2)`, it sends the error message `2` into `*bar()`, which delegates that to `*foo()`, which then `catch`es it and handles it gracefully. Then, the `yield "C"` sends `"C"` back out as the return `value` from the `it.throw(2)` call. 2. The `"D"` value that's next `throw`n from inside `*foo()` propagates out to `*bar()`, which `catch`es it and handles it gracefully. Then the `yield "E"` sends `"E"` back out as the return `value` from the `it.next(3)` call. 3. Next, the exception `throw`n from `*baz()` isn't caught in `*bar()` -- though we did `catch` it outside -- so both `*baz()` and `*bar()` are set to a completed state. After this snippet, you would not be able to get the `"G"` value out with any subsequent `next(..)` call(s) -- they will just return `undefined` for `value`. ### Delegating Asynchrony Let's finally get back to our earlier `yield`-delegation example with the multiple sequential Ajax requests: ```js function *foo() { var r2 = yield request( "http://some.url.2" ); var r3 = yield request( "http://some.url.3/?v=" + r2 ); return r3; } function *bar() { var r1 = yield request( "http://some.url.1" ); var r3 = yield *foo(); console.log( r3 ); } run( bar ); ``` Instead of calling `yield run(foo)` inside of `*bar()`, we just call `yield *foo()`. In the previous version of this example, the Promise mechanism (controlled by `run(..)`) was used to transport the value from `return r3` in `*foo()` to the local variable `r3` inside `*bar()`. Now, that value is just returned back directly via the `yield *` mechanics. Otherwise, the behavior is pretty much identical. ### Delegating "Recursion" Of course, `yield`-delegation can keep following as many delegation steps as you wire up. You could even use `yield`-delegation for async-capable generator "recursion" -- a generator `yield`-delegating to itself: ```js function *foo(val) { if (val > 1) { // generator recursion val = yield *foo( val - 1 ); } return yield request( "http://some.url/?v=" + val ); } function *bar() { var r1 = yield *foo( 3 ); console.log( r1 ); } run( bar ); ``` **Note:** Our `run(..)` utility could have been called with `run( foo, 3 )`, because it supports additional parameters being passed along to the initialization of the generator. However, we used a parameter-free `*bar()` here to highlight the flexibility of `yield *`. What processing steps follow from that code? Hang on, this is going to be quite intricate to describe in detail: 1. `run(bar)` starts up the `*bar()` generator. 2. `foo(3)` creates an *iterator* for `*foo(..)` and passes `3` as its `val` parameter. 3. Because `3 > 1`, `foo(2)` creates another *iterator* and passes in `2` as its `val` parameter. 4. Because `2 > 1`, `foo(1)` creates yet another *iterator* and passes in `1` as its `val` parameter. 5. `1 > 1` is `false`, so we next call `request(..)` with the `1` value, and get a promise back for that first Ajax call. 6. That promise is `yield`ed out, which comes back to the `*foo(2)` generator instance. 7. The `yield *` passes that promise back out to the `*foo(3)` generator instance. Another `yield *` passes the promise out to the `*bar()` generator instance. And yet again another `yield *` passes the promise out to the `run(..)` utility, which will wait on that promise (for the first Ajax request) to proceed. 8. When the promise resolves, its fulfillment message is sent to resume `*bar()`, which passes through the `yield *` into the `*foo(3)` instance, which then passes through the `yield *` to the `*foo(2)` generator instance, which then passes through the `yield *` to the normal `yield` that's waiting in the `*foo(3)` generator instance. 9. That first call's Ajax response is now immediately `return`ed from the `*foo(3)` generator instance, which sends that value back as the result of the `yield *` expression in the `*foo(2)` instance, and assigned to its local `val` variable. 10. Inside `*foo(2)`, a second Ajax request is made with `request(..)`, whose promise is `yield`ed back to the `*foo(1)` instance, and then `yield *` propagates all the way out to `run(..)` (step 7 again). When the promise resolves, the second Ajax response propagates all the way back into the `*foo(2)` generator instance, and is assigned to its local `val` variable. 11. Finally, the third Ajax request is made with `request(..)`, its promise goes out to `run(..)`, and then its resolution value comes all the way back, which is then `return`ed so that it comes back to the waiting `yield *` expression in `*bar()`. Phew! A lot of crazy mental juggling, huh? You might want to read through that a few more times, and then go grab a snack to clear your head! ## Generator Concurrency As we discussed in both Chapter 1 and earlier in this chapter, two simultaneously running "processes" can cooperatively interleave their operations, and many times this can *yield* (pun intended) very powerful asynchrony expressions. Frankly, our earlier examples of concurrency interleaving of multiple generators showed how to make it really confusing. But we hinted that there's places where this capability is quite useful. Recall a scenario we looked at in Chapter 1, where two different simultaneous Ajax response handlers needed to coordinate with each other to make sure that the data communication was not a race condition. We slotted the responses into the `res` array like this: ```js function response(data) { if (data.url == "http://some.url.1") { res[0] = data; } else if (data.url == "http://some.url.2") { res[1] = data; } } ``` But how can we use multiple generators concurrently for this scenario? ```js // `request(..)` is a Promise-aware Ajax utility var res = []; function *reqData(url) { res.push( yield request( url ) ); } ``` **Note:** We're going to use two instances of the `*reqData(..)` generator here, but there's no difference to running a single instance of two different generators; both approaches are reasoned about identically. We'll see two different generators coordinating in just a bit. Instead of having to manually sort out `res[0]` and `res[1]` assignments, we'll use coordinated ordering so that `res.push(..)` properly slots the values in the expected and predictable order. The expressed logic thus should feel a bit cleaner. But how will we actually orchestrate this interaction? First, let's just do it manually, with Promises: ```js var it1 = reqData( "http://some.url.1" ); var it2 = reqData( "http://some.url.2" ); var p1 = it1.next().value; var p2 = it2.next().value; p1 .then( function(data){ it1.next( data ); return p2; } ) .then( function(data){ it2.next( data ); } ); ``` `*reqData(..)`'s two instances are both started to make their Ajax requests, then paused with `yield`. Then we choose to resume the first instance when `p1` resolves, and then `p2`'s resolution will restart the second instance. In this way, we use Promise orchestration to ensure that `res[0]` will have the first response and `res[1]` will have the second response. But frankly, this is awfully manual, and it doesn't really let the generators orchestrate themselves, which is where the true power can lie. Let's try it a different way: ```js // `request(..)` is a Promise-aware Ajax utility var res = []; function *reqData(url) { var data = yield request( url ); // transfer control yield; res.push( data ); } var it1 = reqData( "http://some.url.1" ); var it2 = reqData( "http://some.url.2" ); var p1 = it1.next().value; var p2 = it2.next().value; p1.then( function(data){ it1.next( data ); } ); p2.then( function(data){ it2.next( data ); } ); Promise.all( [p1,p2] ) .then( function(){ it1.next(); it2.next(); } ); ``` OK, this is a bit better (though still manual!), because now the two instances of `*reqData(..)` run truly concurrently, and (at least for the first part) independently. In the previous snippet, the second instance was not given its data until after the first instance was totally finished. But here, both instances receive their data as soon as their respective responses come back, and then each instance does another `yield` for control transfer purposes. We then choose what order to resume them in the `Promise.all([ .. ])` handler. What may not be as obvious is that this approach hints at an easier form for a reusable utility, because of the symmetry. We can do even better. Let's imagine using a utility called `runAll(..)`: ```js // `request(..)` is a Promise-aware Ajax utility var res = []; runAll( function*(){ var p1 = request( "http://some.url.1" ); // transfer control yield; res.push( yield p1 ); }, function*(){ var p2 = request( "http://some.url.2" ); // transfer control yield; res.push( yield p2 ); } ); ``` **Note:** We're not including a code listing for `runAll(..)` as it is not only long enough to bog down the text, but is an extension of the logic we've already implemented in `run(..)` earlier. So, as a good supplementary exercise for the reader, try your hand at evolving the code from `run(..)` to work like the imagined `runAll(..)`. Also, my *asynquence* library provides a previously mentioned `runner(..)` utility with this kind of capability already built in, and will be discussed in Appendix A of this book. Here's how the processing inside `runAll(..)` would operate: 1. The first generator gets a promise for the first Ajax response from `"http://some.url.1"`, then `yield`s control back to the `runAll(..)` utility. 2. The second generator runs and does the same for `"http://some.url.2"`, `yield`ing control back to the `runAll(..)` utility. 3. The first generator resumes, and then `yield`s out its promise `p1`. The `runAll(..)` utility does the same in this case as our previous `run(..)`, in that it waits on that promise to resolve, then resumes the same generator (no control transfer!). When `p1` resolves, `runAll(..)` resumes the first generator again with that resolution value, and then `res[0]` is given its value. When the first generator then finishes, that's an implicit transfer of control. 4. The second generator resumes, `yield`s out its promise `p2`, and waits for it to resolve. Once it does, `runAll(..)` resumes the second generator with that value, and `res[1]` is set. In this running example, we use an outer variable called `res` to store the results of the two different Ajax responses -- that's our concurrency coordination making that possible. But it might be quite helpful to further extend `runAll(..)` to provide an inner variable space for the multiple generator instances to *share*, such as an empty object we'll call `data` below. Also, it could take non-Promise values that are `yield`ed and hand them off to the next generator. Consider: ```js // `request(..)` is a Promise-aware Ajax utility runAll( function*(data){ data.res = []; // transfer control (and message pass) var url1 = yield "http://some.url.2"; var p1 = request( url1 ); // "http://some.url.1" // transfer control yield; data.res.push( yield p1 ); }, function*(data){ // transfer control (and message pass) var url2 = yield "http://some.url.1"; var p2 = request( url2 ); // "http://some.url.2" // transfer control yield; data.res.push( yield p2 ); } ); ``` In this formulation, the two generators are not just coordinating control transfer, but actually communicating with each other, both through `data.res` and the `yield`ed messages that trade `url1` and `url2` values. That's incredibly powerful! Such realization also serves as a conceptual base for a more sophisticated asynchrony technique called CSP (Communicating Sequential Processes), which we will cover in Appendix B of this book. ## Thunks So far, we've made the assumption that `yield`ing a Promise from a generator -- and having that Promise resume the generator via a helper utility like `run(..)` -- was the best possible way to manage asynchrony with generators. To be clear, it is. But we skipped over another pattern that has some mildly widespread adoption, so in the interest of completeness we'll take a brief look at it. In general computer science, there's an old pre-JS concept called a "thunk." Without getting bogged down in the historical nature, a narrow expression of a thunk in JS is a function that -- without any parameters -- is wired to call another function. In other words, you wrap a function definition around function call -- with any parameters it needs -- to *defer* the execution of that call, and that wrapping function is a thunk. When you later execute the thunk, you end up calling the original function. For example: ```js function foo(x,y) { return x + y; } function fooThunk() { return foo( 3, 4 ); } // later console.log( fooThunk() ); // 7 ``` So, a synchronous thunk is pretty straightforward. But what about an async thunk? We can essentially extend the narrow thunk definition to include it receiving a callback. Consider: ```js function foo(x,y,cb) { setTimeout( function(){ cb( x + y ); }, 1000 ); } function fooThunk(cb) { foo( 3, 4, cb ); } // later fooThunk( function(sum){ console.log( sum ); // 7 } ); ``` As you can see, `fooThunk(..)` only expects a `cb(..)` parameter, as it already has values `3` and `4` (for `x` and `y`, respectively) pre-specified and ready to pass to `foo(..)`. A thunk is just waiting around patiently for the last piece it needs to do its job: the callback. You don't want to make thunks manually, though. So, let's invent a utility that does this wrapping for us. Consider: ```js function thunkify(fn) { var args = [].slice.call( arguments, 1 ); return function(cb) { args.push( cb ); return fn.apply( null, args ); }; } var fooThunk = thunkify( foo, 3, 4 ); // later fooThunk( function(sum) { console.log( sum ); // 7 } ); ``` **Tip:** Here we assume that the original (`foo(..)`) function signature expects its callback in the last position, with any other parameters coming before it. This is a pretty ubiquitous "standard" for async JS function standards. You might call it "callback-last style." If for some reason you had a need to handle "callback-first style" signatures, you would just make a utility that used `args.unshift(..)` instead of `args.push(..)`. The preceding formulation of `thunkify(..)` takes both the `foo(..)` function reference, and any parameters it needs, and returns back the thunk itself (`fooThunk(..)`). However, that's not the typical approach you'll find to thunks in JS. Instead of `thunkify(..)` making the thunk itself, typically -- if not perplexingly -- the `thunkify(..)` utility would produce a function that produces thunks. Uhhhh... yeah. Consider: ```js function thunkify(fn) { return function() { var args = [].slice.call( arguments ); return function(cb) { args.push( cb ); return fn.apply( null, args ); }; }; } ``` The main difference here is the extra `return function() { .. }` layer. Here's how its usage differs: ```js var whatIsThis = thunkify( foo ); var fooThunk = whatIsThis( 3, 4 ); // later fooThunk( function(sum) { console.log( sum ); // 7 } ); ``` Obviously, the big question this snippet implies is what is `whatIsThis` properly called? It's not the thunk, it's the thing that will produce thunks from `foo(..)` calls. It's kind of like a "factory" for "thunks." There doesn't seem to be any kind of standard agreement for naming such a thing. So, my proposal is "thunkory" ("thunk" + "factory"). So, `thunkify(..)` produces a thunkory, and a thunkory produces thunks. That reasoning is symmetric to my proposal for "promisory" in Chapter 3: ```js var fooThunkory = thunkify( foo ); var fooThunk1 = fooThunkory( 3, 4 ); var fooThunk2 = fooThunkory( 5, 6 ); // later fooThunk1( function(sum) { console.log( sum ); // 7 } ); fooThunk2( function(sum) { console.log( sum ); // 11 } ); ``` **Note:** The running `foo(..)` example expects a style of callback that's not "error-first style." Of course, "error-first style" is much more common. If `foo(..)` had some sort of legitimate error-producing expectation, we could change it to expect and use an error-first callback. None of the subsequent `thunkify(..)` machinery cares what style of callback is assumed. The only difference in usage would be `fooThunk1(function(err,sum){..`. Exposing the thunkory method -- instead of how the earlier `thunkify(..)` hides this intermediary step -- may seem like unnecessary complication. But in general, it's quite useful to make thunkories at the beginning of your program to wrap existing API methods, and then be able to pass around and call those thunkories when you need thunks. The two distinct steps preserve a cleaner separation of capability. To illustrate: ```js // cleaner: var fooThunkory = thunkify( foo ); var fooThunk1 = fooThunkory( 3, 4 ); var fooThunk2 = fooThunkory( 5, 6 ); // instead of: var fooThunk1 = thunkify( foo, 3, 4 ); var fooThunk2 = thunkify( foo, 5, 6 ); ``` Regardless of whether you like to deal with the thunkories explicitly or not, the usage of thunks `fooThunk1(..)` and `fooThunk2(..)` remains the same. ### s/promise/thunk/ So what's all this thunk stuff have to do with generators? Comparing thunks to promises generally: they're not directly interchangable as they're not equivalent in behavior. Promises are vastly more capable and trustable than bare thunks. But in another sense, they both can be seen as a request for a value, which may be async in its answering. Recall from Chapter 3 we defined a utility for promisifying a function, which we called `Promise.wrap(..)` -- we could have called it `promisify(..)`, too! This Promise-wrapping utility doesn't produce Promises; it produces promisories that in turn produce Promises. This is completely symmetric to the thunkories and thunks presently being discussed. To illustrate the symmetry, let's first alter the running `foo(..)` example from earlier to assume an "error-first style" callback: ```js function foo(x,y,cb) { setTimeout( function(){ // assume `cb(..)` as "error-first style" cb( null, x + y ); }, 1000 ); } ``` Now, we'll compare using `thunkify(..)` and `promisify(..)` (aka `Promise.wrap(..)` from Chapter 3): ```js // symmetrical: constructing the question asker var fooThunkory = thunkify( foo ); var fooPromisory = promisify( foo ); // symmetrical: asking the question var fooThunk = fooThunkory( 3, 4 ); var fooPromise = fooPromisory( 3, 4 ); // get the thunk answer fooThunk( function(err,sum){ if (err) { console.error( err ); } else { console.log( sum ); // 7 } } ); // get the promise answer fooPromise .then( function(sum){ console.log( sum ); // 7 }, function(err){ console.error( err ); } ); ``` Both the thunkory and the promisory are essentially asking a question (for a value), and respectively the thunk `fooThunk` and promise `fooPromise` represent the future answers to that question. Presented in that light, the symmetry is clear. With that perspective in mind, we can see that generators which `yield` Promises for asynchrony could instead `yield` thunks for asynchrony. All we'd need is a smarter `run(..)` utility (like from before) that can not only look for and wire up to a `yield`ed Promise but also to provide a callback to a `yield`ed thunk. Consider: ```js function *foo() { var val = yield request( "http://some.url.1" ); console.log( val ); } run( foo ); ``` In this example, `request(..)` could either be a promisory that returns a promise, or a thunkory that returns a thunk. From the perspective of what's going on inside the generator code logic, we don't care about that implementation detail, which is quite powerful! So, `request(..)` could be either: ```js // promisory `request(..)` (see Chapter 3) var request = Promise.wrap( ajax ); // vs. // thunkory `request(..)` var request = thunkify( ajax ); ``` Finally, as a thunk-aware patch to our earlier `run(..)` utility, we would need logic like this: ```js // .. // did we receive a thunk back? else if (typeof next.value == "function") { return new Promise( function(resolve,reject){ // call the thunk with an error-first callback next.value( function(err,msg) { if (err) { reject( err ); } else { resolve( msg ); } } ); } ) .then( handleNext, function handleErr(err) { return Promise.resolve( it.throw( err ) ) .then( handleResult ); } ); } ``` Now, our generators can either call promisories to `yield` Promises, or call thunkories to `yield` thunks, and in either case, `run(..)` would handle that value and use it to wait for the completion to resume the generator. Symmetry wise, these two approaches look identical. However, we should point out that's true only from the perspective of Promises or thunks representing the future value continuation of a generator. From the larger perspective, thunks do not in and of themselves have hardly any of the trustability or composability guarantees that Promises are designed with. Using a thunk as a stand-in for a Promise in this particular generator asynchrony pattern is workable but should be seen as less than ideal when compared to all the benefits that Promises offer (see Chapter 3). If you have the option, prefer `yield pr` rather than `yield th`. But there's nothing wrong with having a `run(..)` utility which can handle both value types. **Note:** The `runner(..)` utility in my *asynquence* library, which will be discussed in Appendix A, handles `yield`s of Promises, thunks and *asynquence* sequences. ## Pre-ES6 Generators You're hopefully convinced now that generators are a very important addition to the async programming toolbox. But it's a new syntax in ES6, which means you can't just polyfill generators like you can Promises (which are just a new API). So what can we do to bring generators to our browser JS if we don't have the luxury of ignoring pre-ES6 browsers? For all new syntax extensions in ES6, there are tools -- the most common term for them is transpilers, for trans-compilers -- which can take your ES6 syntax and transform it into equivalent (but obviously uglier!) pre-ES6 code. So, generators can be transpiled into code that will have the same behavior but work in ES5 and below. But how? The "magic" of `yield` doesn't obviously sound like code that's easy to transpile. We actually hinted at a solution in our earlier discussion of closure-based *iterators*. ### Manual Transformation Before we discuss the transpilers, let's derive how manual transpilation would work in the case of generators. This isn't just an academic exercise, because doing so will actually help further reinforce how they work. Consider: ```js // `request(..)` is a Promise-aware Ajax utility function *foo(url) { try { console.log( "requesting:", url ); var val = yield request( url ); console.log( val ); } catch (err) { console.log( "Oops:", err ); return false; } } var it = foo( "http://some.url.1" ); ``` The first thing to observe is that we'll still need a normal `foo()` function that can be called, and it will still need to return an *iterator*. So, let's sketch out the non-generator transformation: ```js function foo(url) { // .. // make and return an iterator return { next: function(v) { // .. }, throw: function(e) { // .. } }; } var it = foo( "http://some.url.1" ); ``` The next thing to observe is that a generator does its "magic" by suspending its scope/state, but we can emulate that with function closure (see the *Scope & Closures* title of this series). To understand how to write such code, we'll first annotate different parts of our generator with state values: ```js // `request(..)` is a Promise-aware Ajax utility function *foo(url) { // STATE *1* try { console.log( "requesting:", url ); var TMP1 = request( url ); // STATE *2* var val = yield TMP1; console.log( val ); } catch (err) { // STATE *3* console.log( "Oops:", err ); return false; } } ``` **Note:** For more accurate illustration, we split up the `val = yield request..` statement into two parts, using the temporary `TMP1` variable. `request(..)` happens in state `*1*`, and the assignment of its completion value to `val` happens in state `*2*`. We'll get rid of that intermediate `TMP1` when we convert the code to its non-generator equivalent. In other words, `*1*` is the beginning state, `*2*` is the state if the `request(..)` succeeds, and `*3*` is the state if the `request(..)` fails. You can probably imagine how any extra `yield` steps would just be encoded as extra states. Back to our transpiled generator, let's define a variable `state` in the closure we can use to keep track of the state: ```js function foo(url) { // manage generator state var state; // .. } ``` Now, let's define an inner function called `process(..)` inside the closure which handles each state, using a `switch` statement: ```js // `request(..)` is a Promise-aware Ajax utility function foo(url) { // manage generator state var state; // generator-wide variable declarations var val; function process(v) { switch (state) { case 1: console.log( "requesting:", url ); return request( url ); case 2: val = v; console.log( val ); return; case 3: var err = v; console.log( "Oops:", err ); return false; } } // .. } ``` Each state in our generator is represented by its own `case` in the `switch` statement. `process(..)` will be called each time we need to process a new state. We'll come back to how that works in just a moment. For any generator-wide variable declarations (`val`), we move those to a `var` declaration outside of `process(..)` so they can survive multiple calls to `process(..)`. But the "block scoped" `err` variable is only needed for the `*3*` state, so we leave it in place. In state `*1*`, instead of `yield request(..)`, we did `return request(..)`. In terminal state `*2*`, there was no explicit `return`, so we just do a `return;` which is the same as `return undefined`. In terminal state `*3*`, there was a `return false`, so we preserve that. Now we need to define the code in the *iterator* functions so they call `process(..)` appropriately: ```js function foo(url) { // manage generator state var state; // generator-wide variable declarations var val; function process(v) { switch (state) { case 1: console.log( "requesting:", url ); return request( url ); case 2: val = v; console.log( val ); return; case 3: var err = v; console.log( "Oops:", err ); return false; } } // make and return an iterator return { next: function(v) { // initial state if (!state) { state = 1; return { done: false, value: process() }; } // yield resumed successfully else if (state == 1) { state = 2; return { done: true, value: process( v ) }; } // generator already completed else { return { done: true, value: undefined }; } }, "throw": function(e) { // the only explicit error handling is in // state *1* if (state == 1) { state = 3; return { done: true, value: process( e ) }; } // otherwise, an error won't be handled, // so just throw it right back out else { throw e; } } }; } ``` How does this code work? 1. The first call to the *iterator*'s `next()` call would move the generator from the uninitialized state to state `1`, and then call `process()` to handle that state. The return value from `request(..)`, which is the promise for the Ajax response, is returned back as the `value` property from the `next()` call. 2. If the Ajax request succeeds, the second call to `next(..)` should send in the Ajax response value, which moves our state to `2`. `process(..)` is again called (this time with the passed in Ajax response value), and the `value` property returned from `next(..)` will be `undefined`. 3. However, if the Ajax request fails, `throw(..)` should be called with the error, which would move the state from `1` to `3` (instead of `2`). Again `process(..)` is called, this time with the error value. That `case` returns `false`, which is set as the `value` property returned from the `throw(..)` call. From the outside -- that is, interacting only with the *iterator* -- this `foo(..)` normal function works pretty much the same as the `*foo(..)` generator would have worked. So we've effectively "transpiled" our ES6 generator to pre-ES6 compatibility! We could then manually instantiate our generator and control its iterator -- calling `var it = foo("..")` and `it.next(..)` and such -- or better, we could pass it to our previously defined `run(..)` utility as `run(foo,"..")`. ### Automatic Transpilation The preceding exercise of manually deriving a transformation of our ES6 generator to pre-ES6 equivalent teaches us how generators work conceptually. But that transformation was really intricate and very non-portable to other generators in our code. It would be quite impractical to do this work by hand, and would completely obviate all the benefit of generators. But luckily, several tools already exist that can automatically convert ES6 generators to things like what we derived in the previous section. Not only do they do the heavy lifting work for us, but they also handle several complications that we glossed over. One such tool is regenerator (https://facebook.github.io/regenerator/), from the smart folks at Facebook. If we use regenerator to transpile our previous generator, here's the code produced (at the time of this writing): ```js // `request(..)` is a Promise-aware Ajax utility var foo = regeneratorRuntime.mark(function foo(url) { var val; return regeneratorRuntime.wrap(function foo$(context$1$0) { while (1) switch (context$1$0.prev = context$1$0.next) { case 0: context$1$0.prev = 0; console.log( "requesting:", url ); context$1$0.next = 4; return request( url ); case 4: val = context$1$0.sent; console.log( val ); context$1$0.next = 12; break; case 8: context$1$0.prev = 8; context$1$0.t0 = context$1$0.catch(0); console.log("Oops:", context$1$0.t0); return context$1$0.abrupt("return", false); case 12: case "end": return context$1$0.stop(); } }, foo, this, [[0, 8]]); }); ``` There's some obvious similarities here to our manual derivation, such as the `switch` / `case` statements, and we even see `val` pulled out of the closure just as we did. Of course, one trade-off is that regenerator's transpilation requires a helper library `regeneratorRuntime` that holds all the reusable logic for managing a general generator / *iterator*. A lot of that boilerplate looks different than our version, but even then, the concepts can be seen, like with `context$1$0.next = 4` keeping track of the next state for the generator. The main takeaway is that generators are not restricted to only being useful in ES6+ environments. Once you understand the concepts, you can employ them throughout your code, and use tools to transform the code to be compatible with older environments. This is more work than just using a `Promise` API polyfill for pre-ES6 Promises, but the effort is totally worth it, because generators are so much better at expressing async flow control in a reason-able, sensible, synchronous-looking, sequential fashion. Once you get hooked on generators, you'll never want to go back to the hell of async spaghetti callbacks! ## Review Generators are a new ES6 function type that does not run-to-completion like normal functions. Instead, the generator can be paused in mid-completion (entirely preserving its state), and it can later be resumed from where it left off. This pause/resume interchange is cooperative rather than preemptive, which means that the generator has the sole capability to pause itself, using the `yield` keyword, and yet the *iterator* that controls the generator has the sole capability (via `next(..)`) to resume the generator. The `yield` / `next(..)` duality is not just a control mechanism, it's actually a two-way message passing mechanism. A `yield ..` expression essentially pauses waiting for a value, and the next `next(..)` call passes a value (or implicit `undefined`) back to that paused `yield` expression. The key benefit of generators related to async flow control is that the code inside a generator expresses a sequence of steps for the task in a naturally sync/sequential fashion. The trick is that we essentially hide potential asynchrony behind the `yield` keyword -- moving the asynchrony to the code where the generator's *iterator* is controlled. In other words, generators preserve a sequential, synchronous, blocking code pattern for async code, which lets our brains reason about the code much more naturally, addressing one of the two key drawbacks of callback-based async. ================================================ FILE: async & performance/ch5.md ================================================ # You Don't Know JS: Async & Performance # Глава 5: Производительность программы До сих пор эта книга была посвящена тому, как эффективнее использовать паттерны асинхронности. Но мы не рассматривали напрямую, почему асинхронность действительно важна для JS. Самая очевидная и явная причина - это **производительность**. Например, если вам нужно выполнить два независимых Ajax-запроса, но вам нужно дождаться их завершения перед выполнением следующей задачи, у вас есть два варианта моделирования этого взаимодействия: последовательный и параллельный. Вы можете сделать первый запрос и ждать начала второго запроса, пока не завершится первый. Или, как мы уже видели на примере обещаний и генераторов, вы можете выполнить оба запроса "параллельно" и выразить "ворота" для ожидания выполнения обоих запросов, прежде чем двигаться дальше. Очевидно, что последний вариант, как правило, будет более производительным, чем первый. А лучшая производительность обычно приводит к лучшему пользовательскому опыту Возможно даже, что асинхронность (чередующийся параллелизм) может улучшить только восприятие производительности, даже если на выполнение программы в целом уходит столько же времени. Восприятие производительности пользователем не менее -- если не более -- важно, чем реальная измеримая производительность. Теперь мы хотим выйти за рамки локализованных паттернов асинхронности и поговорить о некоторых деталях производительности на уровне программы. **Примечание:** Вам могут быть интересны вопросы микропроизводительности, например, что быстрее - `a++` или `++a`. Мы рассмотрим такие детали производительности в следующей главе "Бенчмаркинг и тюнинг". ## Web Workers Если у вас есть задачи с интенсивной обработкой данных, но вы не хотите, чтобы они выполнялись в главном потоке (что может замедлить работу браузера/интерфейса), вы могли бы пожелать, чтобы JavaScript работал в многопоточном режиме. В главе 1 мы подробно рассказывали о том, что JavaScript является однопоточным. И это по-прежнему верно. Но один поток - не единственный способ организовать выполнение программы. Представьте, что вы разделили свою программу на две части, запустили одну из них в основном потоке пользовательского интерфейса, а другую - в совершенно отдельном потоке. Какие проблемы возникнут при такой архитектуре? Например, вы бы хотели знать, означает ли выполнение в отдельном потоке, что он работает параллельно (в системах с несколькими процессорами/ядрами), так что долго выполняющийся процесс во втором потоке не будет блокировать основной поток программы. В противном случае "виртуальные потоки" не принесут особой пользы по сравнению с тем, что мы уже имеем в JS с асинхронным параллелизмом. Кроме того, вам нужно знать, имеют ли эти две части программы доступ к одной и той же общей области видимости/ресурсам. Если да, то возникают все вопросы, с которыми сталкиваются многопоточные языки (Java, C++ и т. д.), например, необходимость кооперативной или вытесняющей блокировки (мьютексы(взаимное исключение) и т. д.). Это очень много дополнительной работы, и к ней не стоит относиться легкомысленно. Кроме того, вы бы хотели знать, как эти две части могут "общаться", если они не могут делиться областью видимости/ресурсами. Всё это - отличные вопросы для рассмотрения, поскольку мы изучаем функцию, добавленную в веб-платформу примерно во времена HTML5, под названием "Web Workers". Это функция браузера (он же хост-окружение), и на самом деле она почти никак не связана с самим языком JS. То есть в JavaScript на данный момент нет никаких функций, поддерживающих многопоточное выполнение. Но такая среда, как ваш браузер, может легко предоставить несколько экземпляров движка JavaScript, каждый со своим потоком, и позволить вам запускать разные программы в каждом из потоков. Каждый из этих отдельных потоковых частей вашей программы называется "(Web) Worker". Такой тип параллелизма называется "параллелизмом задач (Task parallelism)", поскольку акцент делается на разделении фрагментов вашей программы для параллельного выполнения. Из вашей основной JS-программы (или другого Worker) вы создаете Worker следующим образом: ```js var w1 = new Worker( "http://some.url.1/mycoolworker.js" ); ``` URL должен указывать на местоположение JS-файла (не HTML-страницы!), который предназначен для загрузки в Worker. После этого браузер запустит отдельный поток и позволит этому файлу выполняться в нем как независимой программе. **Примечание:** Тип Worker, созданный с помощью такого URL, называется "Dedicated Worker". Но вместо URL-адреса внешнего файла вы можете создать "Inline Worker", предоставив Blob URL-адрес (еще одна возможность HTML5); по сути, это inline-файл, хранящийся в одном (двоичном) значении. Однако Blob'ы выходят за рамки того, что мы будем обсуждать здесь. Workers не имеют общей области видимости или ресурсов друг с другом или с основной программой - это вывело бы на первый план все кошмары многопоточного программирования - но вместо этого их связывает простой механизм обмена сообщениями о событиях. Объект `w1` Worker является слушателем событий и триггером, который позволяет вам подписываться на события, посылаемые Worker, а также посылать события Worker. Вот как прослушивать события (если точнее, фиксированное событие `"message"`): ```js w1.addEventListener( "message", function(evt){ // evt.data } ); ``` И вы можете отправить событие `"message"` в Worker: ```js w1.postMessage( "something cool to say" ); ``` Внутри Worker отправка событий полностью симметрична: ```js // "mycoolworker.js" addEventListener( "message", function(evt){ // evt.data } ); postMessage( "a really cool reply" ); ``` Обратите внимание, что выделенный Worker находится в отношениях один-к-одному с программой, которая его создала. То есть событие ``message`` не нуждается в устранении неоднозначности, потому что мы уверены, что оно могло произойти только в результате этой связи "один к одному" - либо от Worker, либо от главной страницы. Обычно приложение главной страницы создает Worker'ы, но при необходимости Worker может создавать свои собственные дочерние Worker(ы) - так называемые subworkers. Иногда полезно делегировать такие детали некоему "главному" Worker'у, который порождает других Worker'ов для обработки части задачи. К сожалению, на момент написания этой статьи Chrome все еще не поддерживает subworkers, в то время как Firefox поддерживает. Чтобы немедленно убить Worker из программы, которая его создала, вызовите `terminate()` для объекта Worker (как `w1` в предыдущих сниппетах). Резкое завершение потока Worker не дает ему возможности закончить свою работу или очистить ресурсы. Это похоже на то, как если бы вы закрыли вкладку браузера, чтобы убить страницу. Если в браузере есть две или более страниц (или несколько вкладок с одной и той же страницей!), которые пытаются создать Worker из одного и того же URL-адреса файла, то в итоге они окажутся совершенно отдельными Worker'ами. Вскоре мы обсудим способ "совместного использования" Worker'ов. **Примечание:** Может показаться, что вредоносная или невежественная JS-программа может легко провести атаку на отказ в обслуживании системы (denial-of-service attack), породив сотни Worker'ов, каждый из которых, казалось бы, имеет свой собственный поток. Хотя это правда, что есть некоторая гарантия того, что Worker окажется в отдельном потоке, эта гарантия не безгранична. Система вольна решать, сколько реальных потоков/процессоров/ядер она действительно хочет создать. Невозможно предсказать или гарантировать, к какому количеству потоков вы получите доступ, хотя многие люди полагают, что их будет не меньше, чем количество доступных процессоров/ядер. Я думаю, что самое безопасное предположение - это наличие хотя бы одного другого потока, кроме основного потока пользовательского интерфейса, но это не более того. ### Worker Environment Внутри Worker у вас нет доступа ни к одному из ресурсов основной программы. Это означает, что вы не можете получить доступ ни к глобальным переменным, ни к DOM страницы, ни к другим ресурсам. Помните: это совершенно отдельный поток. Однако вы можете выполнять сетевые операции (Ajax, WebSockets) и устанавливать таймеры. Также Worker имеет доступ к собственной копии нескольких важных глобальных переменных/функций, включая `navigator`, `location`, `JSON` и `applicationCache`. Вы также можете загружать в Worker дополнительные JS-скрипты, используя `importScripts(...)`: ```js // inside the Worker importScripts( "foo.js", "bar.js" ); ``` Эти скрипты загружаются синхронно, что означает, что вызов `importScripts(...)` будет блокировать выполнение остальной части Worker'а, пока файл(ы) не закончит(ат) загрузку и выполнение. **Примечание:** Также обсуждался вопрос о раскрытии API `` для Workers, что в сочетании с тем, что холсты будут Transferables(передаваемыми) (см. раздел "Передача данных"), позволит Workers выполнять более сложную внепоточную обработку графики, которая может быть полезна для высокопроизводительных игр (WebGL) и других подобных приложений. Хотя такой возможности пока нет ни в одном браузере, она, скорее всего, появится в ближайшем будущем. Каковы некоторые типичные области применения Web Workers? * Обработка интенсивных математических вычислений * Сортировка больших наборов данных * Операции с данными (сжатие, анализ аудио, манипуляции с пикселями изображений и т. д.) * Сетевые коммуникации с высоким трафиком ### Передача данных Вы можете заметить общую характеристику большинства этих применений: они требуют передачи большого количества информации через барьер между потоками с помощью механизма событий, возможно, в обоих направлениях. В первые дни существования Workers сериализация всех данных в строковое значение была единственным вариантом. Помимо снижения скорости при двусторонней сериализации, другим существенным минусом было то, что данные копировались, а это означало удвоение использования памяти (и последующую возню со сборкой мусора). К счастью, теперь у нас есть несколько лучших вариантов. Если вы передаете объект, то для копирования/дублирования объекта на другой стороне используется так называемый "Structured Cloning Algorithm"(Алгоритм структурированного клонирования) (https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm). Этот алгоритм довольно сложен и может даже обрабатывать дублирование объектов с циклическими ссылками. При подходе не приходится платить штраф за производительность при работе к строке/из строки, но дублирование памяти все равно имеет место. Он поддерживается в IE10 и выше, а также во всех других основных браузерах. Еще лучший вариант, особенно для больших наборов данных, - это "Transferable Objects"(переносимые объекты) (http://updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast). При этом происходит передача "прав собственности" на объект, но сами данные не перемещаются. Как только вы передаете объект Worker'у, он становится пустым или недоступным в исходном месте - это устраняет опасность потокового программирования через общую область видимости. Конечно, передача прав собственности может происходить в обоих направлениях. На самом деле не так уж много нужно сделать, чтобы выбрать Transferable Object; любая структура данных, реализующая интерфейс Transferable (https://developer.mozilla.org/en-US/docs/Web/API/Transferable), будет автоматически передаваться таким образом (поддержка Firefox и Chrome). Например, типизированные массивы типа `Uint8Array` (см. *ES6 & Beyond* часть этой серии) являются "Transferables". Вот как вы отправите передаваемый объект с помощью `postMessage(...)`: ```js // `foo` - это, например, `Uint8Array`. postMessage( foo.buffer, [ foo.buffer ] ); ``` Первый параметр - это необработанный буфер, а второй - список того, что нужно передать. Браузеры, не поддерживающие Transferable Objects, просто переходят к структурированному клонированию, что означает снижение производительности, а не полный отказ от функций. ### Shared Workers Если ваш сайт или приложение позволяет загружать несколько вкладок одной и той же страницы (это распространенная функция), вы вполне можете захотеть снизить потребление ресурсов системы, предотвратив дублирование выделенных Worker'ов. Наиболее распространенным ограниченным ресурсом в этом отношении являются сетевые соединения через сокеты, поскольку браузеры ограничивают количество одновременных подключений к одному хосту. Разумеется, ограничение множества соединений от клиента также снижает требования к ресурсам сервера. В этом случае весьма полезно создать единый централизованный Worker, с которым могут *обмениваться* все экземпляры страниц вашего сайта или приложения. Это называется `SharedWorker`, который вы создаете следующим образом (поддержка этого метода ограничена Firefox и Chrome): ```js var w1 = new SharedWorker( "http://some.url.1/mycoolworker.js" ); ``` Поскольку общий Worker может быть подключен к нескольким экземплярам программ или страницам на вашем сайте, Worker должен знать, от какой программы пришло сообщение. Этот уникальный идентификатор называется "port" - вспомните порты сетевых сокетов. Поэтому вызывающая программа должна использовать объект `port` Worker'а для связи: ```js w1.port.addEventListener( "message", handleMessages ); // .. w1.port.postMessage( "something cool" ); ``` Кроме того, соединение с портом должно быть инициализировано, как: ```js w1.port.start(); ``` Внутри общего Worker'а необходимо обработать дополнительное событие: `"connect"`. Это событие предоставляет порт `object` для данного конкретного соединения. Наиболее удобным способом разделения нескольких соединений является использование замыкания (см. раздел *Scope & Closures* в этой серии) `port`, как показано ниже, при этом событие прослушивания и передачи для этого соединения определяется в обработчике события `"connect"`: ```js // внутри общего Worker addEventListener( "connect", function(evt){ // назначенный порт для данного соединения var port = evt.ports[0]; port.addEventListener( "message", function(evt){ // .. port.postMessage( .. ); // .. } ); // инициализируем соединение с портом port.start(); } ); ``` Кроме этого различия, общие и выделенные Worker'ы имеют одинаковые возможности и семантику. **Примечание:** Shared Workers переживают разрыв соединения с портом, если другие соединения с портом еще живы, в то время как выделенные Worker'ы разрываются при разрыве соединения с инициирующей их программой. ### Полифил Web Workers Web Workers очень привлекательны с точки зрения производительности для параллельного выполнения JS-программ. Однако вы можете оказаться в ситуации, когда ваш код должен работать в старых браузерах, в которых отсутствует их поддержка. Поскольку Workers - это API, а не синтаксис, их можно в определенной степени заменить полифилом. Если браузер не поддерживает Workers, то с точки зрения производительности подделать многопоточность просто невозможно. Обычно считается, что Iframe обеспечивают параллельную среду, но во всех современных браузерах они на самом деле выполняются в том же потоке, что и основная страница, поэтому их недостаточно для имитации параллелизма. Как мы подробно рассказывали в Главе 1, асинхронность (а не параллельность) в JS обеспечивается очередью цикла событий, поэтому вы можете заставить поддельные Worker'ы быть асинхронными с помощью таймеров (`setTimeout(...)` и т. д.). Затем вам просто нужно предоставить полифил для API Worker. Некоторые из них перечислены здесь (https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#web-workers), но, честно говоря, ни один из них не выглядит хорошо. Я написал набросок полифила для `Worker` здесь (https://gist.github.com/getify/1b26accb1a09aa53ad25). Он базовый, но для простой поддержки `Worker` должен подойти, учитывая, что двусторонний обмен сообщениями работает корректно, как и обработка `"onerror"`. Возможно, вы сможете расширить его дополнительными функциями, такими как `terminate()` или поддельные Shared Workers, по своему усмотрению. **Примечание:** Вы не можете подделать синхронную блокировку, поэтому этот полифил просто запрещает использование `importScripts(...)`. Другим вариантом мог бы быть разбор и преобразование кода Worker'а (после загрузки Ajax) для обработки перезаписи в некоторую асинхронную форму полифила `importScripts(...)`, возможно, с интерфейсом, учитывающим Promise(обещания). ## SIMD Single instruction, multiple data (SIMD) - это форма "параллелизма данных", в отличие от "параллелизма задач" в Web Workers, потому что акцент делается не на распараллеливании фрагментов программной логики, а скорее на параллельной обработке нескольких кусочков данных. При использовании SIMD потоки не обеспечивают параллелизм. Вместо этого современные процессоры предоставляют возможность SIMD с помощью "векторов" чисел - подумайте: специализированные по типу массивы - а также инструкций, которые могут работать параллельно со всеми числами; это низкоуровневые операции, использующие параллелизм на уровне инструкций. Усилия по раскрытию возможностей SIMD в JavaScript в основном возглавляет компания Intel (https://01.org/node/1495), а именно Мохаммад Хагихат (на момент написания этой статьи), в сотрудничестве с командами Firefox и Chrome. SIMD находится на ранней стадии разработки стандартов и имеет все шансы попасть в будущую редакцию JavaScript, скорее всего, в период ES7. SIMD JavaScript предлагает предоставлять коду JS короткие векторные типы и API, которые в системах с поддержкой SIMD будут отображать операции непосредственно на эквиваленты процессора, с возвратом к непараллельным операциям "шимми" в системах без SIMD. Преимущества производительности для приложений с большим объемом данных (анализ сигналов, матричные операции на графике и т. д.) при такой параллельной математической обработке совершенно очевидны! Ранние предложения SIMD API на момент написания этой статьи выглядят следующим образом: ```js var v1 = SIMD.float32x4( 3.14159, 21.0, 32.3, 55.55 ); var v2 = SIMD.float32x4( 2.1, 3.2, 4.3, 5.4 ); var v3 = SIMD.int32x4( 10, 101, 1001, 10001 ); var v4 = SIMD.int32x4( 10, 20, 30, 40 ); SIMD.float32x4.mul( v1, v2 ); // [ 6.597339, 67.2, 138.89, 299.97 ] SIMD.int32x4.add( v3, v4 ); // [ 20, 121, 1031, 10041 ] ``` Здесь показаны два различных типа векторных данных: 32-битные числа с плавающей точкой и 32-битные целые числа. Видно, что размер этих векторов точно соответствует четырем 32-битным элементам, так как это соответствует размерам SIMD-векторов (128 бит), доступных в большинстве современных процессоров. Также возможно, что в будущем мы увидим `x8` (или более крупную!) версию этих API. Помимо `mul()` и `add()`, вероятно, будет включено множество других операций, таких как `sub()`, `div()`, `abs()`, `neg()`, `qrt()`, `reciprocal()`, `reciprocalSqrt()` (арифметика), `shuffle()` (перестановка элементов вектора), `and()`, `or()`, `xor()`, `not()` (логические), `equal()`, `greaterThan()`, `lessThan()` (сравнение), `shiftLeft()`, `shiftRightLogical()`, `shiftRightArithmetic()` (сдвиги), `fromFloat32x4()`, и `fromInt32x4()` (преобразования). **Примечание:** Существует официальный "prollyfill" (надеющийся, ожидающий, устремленный в будущее полифил) для SIMD-функциональности (https://github.com/johnmccutchan/ecmascript_simd), который иллюстрирует гораздо больше запланированных SIMD-возможностей, чем мы показали в этом разделе. ## asm.js "asm.js" (http://asmjs.org/) - это обозначение высоко оптимизируемого подмножества языка JavaScript. Тщательно избегая определенных механизмов и шаблонов, которые *трудно* оптимизировать (сборка мусора, принуждение и т. д.), код в стиле asm.js может быть распознан движком JS и удостоен особого внимания с помощью агрессивных низкоуровневых оптимизаций. В отличие от других механизмов повышения производительности программ, рассмотренных в этой главе, asm.js - это не обязательно что-то, что должно быть принято в спецификации языка JS. Спецификация asm.js *существует* (http://asmjs.org/spec/latest/), но она предназначена в основном для отслеживания согласованного набора выводов-кандидатов на оптимизацию, а не для установления требований к движкам JS. В настоящее время не предлагается никакого нового синтаксиса. Вместо этого asm.js предлагает способы распознавания существующего стандартного синтаксиса JS, который соответствует правилам asm.js, и позволяет движкам реализовывать свои собственные оптимизации соответствующим образом. Между производителями браузеров существуют некоторые разногласия по поводу того, как именно следует активировать asm.js в программе. В ранних версиях эксперимента с asm.js требовалась прагма `"use asm";` (подобно прагме строгого режима `"use strict";`), чтобы указать JS-движку на возможности и подсказки для оптимизации asm.js. Другие утверждают, что asm.js должен быть просто набором эвристик, которые движки распознают автоматически, без необходимости делать что-то дополнительное для автора. Это означает, что существующие программы теоретически могут извлечь пользу из оптимизации в стиле asm.js, не делая ничего особенного. ### Как оптимизировать с помощью asm.js Первое, что нужно понять об оптимизации в asm.js, - это оптимизация типов и принуждения (см. раздел *Типы и грамматика* в этой серии). Если JS-движок должен отслеживать несколько различных типов значений в переменной через различные операции, чтобы при необходимости обрабатывать принуждения между типами, это много дополнительной работы, которая делает оптимизацию программы неоптимальной. **Примечание:** Мы будем использовать код в стиле asm.js для иллюстрации, но имейте в виду, что обычно не предполагается, что вы будете писать такой код вручную. asm.js больше предназначен для компиляции из других инструментов, таких как Emscripten (https://github.com/kripken/emscripten/wiki). Конечно, можно написать свой собственный код asm.js, но это обычно плохая идея, потому что код очень низкого уровня, и управление им может быть очень трудоемким и чреватым ошибками. Тем не менее, могут быть случаи, когда вы захотите вручную подправить свой код в целях оптимизации asm.js. Есть несколько "трюков", которые можно использовать, чтобы намекнуть asm.js-aware JS-движку, какой тип предполагается использовать для переменных/операций, чтобы он мог пропустить эти шаги по отслеживанию приведения типов (coercion). Например: ```js var a = 42; // .. var b = a; ``` В этой программе задание `b = a` оставляет открытой дверь для расхождения типов переменных. Однако вместо этого его можно записать так: ```js var a = 42; // .. var b = a | 0; ``` Здесь мы использовали `|` ("двоичное ИЛИ") со значением `0`, которое никак не влияет на значение, кроме как убедиться, что оно является 32-битным целым числом. Этот код, запущенный в обычном JS-движке, работает отлично, но при запуске в asm.js-aware JS-движке он *может* сигнализировать, что `b` всегда должно рассматриваться как 32-битное целое число, поэтому отслеживание принуждения можно пропустить. Аналогично, операция сложения между двумя переменными может быть ограничена более производительным целочисленным сложением (вместо сложения с плавающей точкой): ```js (a + b) | 0 ``` И опять же, asm.js-aware JS-движок может увидеть эту подсказку и сделать вывод, что операция `+` должна быть 32-битным целым сложением, потому что конечный результат всего выражения в любом случае автоматически будет соответствовать 32-битному целому числу. ### Модули asm.js Одним из самых больших факторов, снижающих производительность в JS, является распределение памяти, сборка мусора и доступ к областям видимости. asm.js предлагает один из способов решения этих проблем - объявить более формализованный "модуль" asm.js - не путайте их с модулями ES6; см. главу *ES6 & Beyond* этой серии. Для модуля asm.js необходимо явно передать строго согласованное пространство имен - в спецификации оно называется `stdlib`, поскольку должно представлять необходимые стандартные библиотеки - для импорта необходимых символов, а не просто использовать глобальные переменные через лексическую область видимости. В базовом случае объект `window` является приемлемым объектом `stdlib` для целей модуля asm.js, но вы можете и, возможно, должны создать еще более ограниченный объект. Вы также должны объявить "heap" (кучу) - это просто модный термин для обозначения зарезервированного места в памяти, где переменные уже могут использоваться без запроса дополнительной памяти или освобождения ранее использованной памяти - и передать ее, чтобы модулю asm.js не нужно было делать ничего, что могло бы вызвать переполнение памяти; он может просто использовать заранее зарезервированное место. "Heap" - это скорее типизированный `ArrayBuffer`, такой как: ```js var heap = new ArrayBuffer( 0x10000 ); // 64k heap ``` Используя эти заранее зарезервированные 64k двоичного пространства, модуль asm.js может хранить и извлекать значения в этом буфере без каких-либо штрафов за выделение памяти или сборку мусора. Например, буфер `heap` может быть использован внутри модуля для хранения массива 64-битных значений float следующим образом: ```js var arr = new Float64Array( heap ); ``` Итак, давайте сделаем быстрый и глупый пример модуля в стиле asm.js, чтобы проиллюстрировать, как эти части сочетаются друг с другом. Мы определим `foo(...)`, который берет начальное (`x`) и конечное (`y`) целое число для диапазона, вычисляет все внутренние смежные умножения значений в диапазоне, а затем, наконец, усредняет эти значения вместе: ```js function fooASM(stdlib,foreign,heap) { "use asm"; var arr = new stdlib.Int32Array( heap ); function foo(x,y) { x = x | 0; y = y | 0; var i = 0; var p = 0; var sum = 0; var count = ((y|0) - (x|0)) | 0; // вычислить все внутренние смежные умножения for (i = x | 0; (i | 0) < (y | 0); p = (p + 8) | 0, i = (i + 1) | 0 ) { // сохраняем результат arr[ p >> 3 ] = (i * (i + 1)) | 0; } // вычислить среднее значение всех промежуточных значений for (i = 0, p = 0; (i | 0) < (count | 0); p = (p + 8) | 0, i = (i + 1) | 0 ) { sum = (sum + arr[ p >> 3 ]) | 0; } return +(sum / count); } return { foo: foo }; } var heap = new ArrayBuffer( 0x1000 ); var foo = fooASM( window, null, heap ).foo; foo( 10, 20 ); // 233 ``` **Примечание:** Этот пример asm.js написан вручную в целях иллюстрации, поэтому он не представляет собой тот же код, который был бы получен от инструмента компиляции, нацеленного на asm.js. Однако он демонстрирует типичную природу кода asm.js, особенно подсказки типов и использование буфера `heap` для хранения временных переменных. Первый вызов `fooASM(...)` устанавливает наш модуль asm.js с его `heap` расположением. В результате мы получаем функцию `foo(...)`, которую можем вызывать столько раз, сколько потребуется. Эти вызовы `foo(...)` должны быть специальным образом оптимизированы JS-движком с поддержкой asm.js. Важно отметить, что предыдущий код полностью соответствует стандартному JS и будет прекрасно работать (без специальной оптимизации) на движке, не поддерживающем asm.js. Очевидно, что природа ограничений, которые делают код asm.js настолько оптимизируемым, значительно сокращает возможные области применения такого кода. asm.js не обязательно будет общим набором оптимизаций для любой конкретной JS-программы. Вместо этого он призван обеспечить оптимизированный способ обработки специализированных задач, таких как интенсивные математические операции (например, используемые при обработке графики в играх). ## Обзор Первые четыре главы этой книги основаны на мысли, что паттерны асинхронного программирования дают вам возможность писать более производительный код, что в целом является очень важным улучшением. Но асинхронное поведение даёт вам только такую возможность, потому что оно все равно привязано к одному потоку цикла событий. Итак, в этой главе мы рассмотрели несколько механизмов программного уровня для дальнейшего повышения производительности. Web Workers позволяют запускать JS-файл (он же программа) в отдельном потоке, используя асинхронные события для передачи сообщений между потоками. Они отлично подходят для разгрузки длительных или ресурсоемких задач на другой поток, оставляя основной поток пользовательского интерфейса более отзывчивым. SIMD предлагает перенести параллельные математические операции на уровне процессора в API JavaScript для высокопроизводительных параллельных операций с данными, таких как обработка чисел в больших наборах данных. Наконец, asm.js описывает небольшое подмножество JavaScript, которое избегает трудно оптимизируемых частей JS (таких как сборка мусора и приведение типов) и позволяет движку JS распознавать и выполнять такой код с помощью агрессивной оптимизации. asm.js может быть создан вручную, но это чрезвычайно утомительно и чревато ошибками, сродни ручному написанию языка assembly (отсюда и название (сборка)). Вместо этого, основная цель состоит в том, чтобы asm.js стал хорошей мишенью для кросс-компиляции из других высоко оптимизированных языков программирования - например, Emscripten (https://github.com/kripken/emscripten/wiki), транслирующий C/C++ в JavaScript. Несмотря на то, что в этой главе не рассматривается в явном виде, есть еще более радикальные идеи для JavaScript, которые обсуждаются на ранних этапах, включая приближение к прямой потоковой функциональности (а не просто скрытой за API структур данных). Независимо от того, произойдет ли это в явном виде, или мы просто увидим, как больше параллелизма проникает в JS за кулисы, будущее более оптимизированной производительности на уровне программ в JS выглядит действительно *перспективным*. ================================================ FILE: async & performance/ch6.md ================================================ # You Don't Know JS: Async & Performance # Chapter 6: Benchmarking & Tuning As the first four chapters of this book were all about performance as a coding pattern (asynchrony and concurrency), and Chapter 5 was about performance at the macro program architecture level, this chapter goes after the topic of performance at the micro level, focusing on single expressions/statements. One of the most common areas of curiosity -- indeed, some developers can get quite obsessed about it -- is in analyzing and testing various options for how to write a line or chunk of code, and which one is faster. We're going to look at some of these issues, but it's important to understand from the outset that this chapter is **not** about feeding the obsession of micro-performance tuning, like whether some given JS engine can run `++a` faster than `a++`. The more important goal of this chapter is to figure out what kinds of JS performance matter and which ones don't, *and how to tell the difference*. But even before we get there, we need to explore how to most accurately and reliably test JS performance, because there's tons of misconceptions and myths that have flooded our collective cult knowledge base. We've got to sift through all that junk to find some clarity. ## Benchmarking OK, time to start dispelling some misconceptions. I'd wager the vast majority of JS developers, if asked to benchmark the speed (execution time) of a certain operation, would initially go about it something like this: ```js var start = (new Date()).getTime(); // or `Date.now()` // do some operation var end = (new Date()).getTime(); console.log( "Duration:", (end - start) ); ``` Raise your hand if that's roughly what came to your mind. Yep, I thought so. There's a lot wrong with this approach, but don't feel bad; **we've all been there.** What did that measurement tell you, exactly? Understanding what it does and doesn't say about the execution time of the operation in question is key to learning how to appropriately benchmark performance in JavaScript. If the duration reported is `0`, you may be tempted to believe that it took less than a millisecond. But that's not very accurate. Some platforms don't have single millisecond precision, but instead only update the timer in larger increments. For example, older versions of windows (and thus IE) had only 15ms precision, which means the operation has to take at least that long for anything other than `0` to be reported! Moreover, whatever duration is reported, the only thing you really know is that the operation took approximately that long on that exact single run. You have near-zero confidence that it will always run at that speed. You have no idea if the engine or system had some sort of interference at that exact moment, and that at other times the operation could run faster. What if the duration reported is `4`? Are you more sure it took about four milliseconds? Nope. It might have taken less time, and there may have been some other delay in getting either `start` or `end` timestamps. More troublingly, you also don't know that the circumstances of this operation test aren't overly optimistic. It's possible that the JS engine figured out a way to optimize your isolated test case, but in a more real program such optimization would be diluted or impossible, such that the operation would run slower than your test. So... what do we know? Unfortunately, with those realizations stated, **we know very little.** Something of such low confidence isn't even remotely good enough to build your determinations on. Your "benchmark" is basically useless. And worse, it's dangerous in that it implies false confidence, not just to you but also to others who don't think critically about the conditions that led to those results. ### Repetition "OK," you now say, "Just put a loop around it so the whole test takes longer." If you repeat an operation 100 times, and that whole loop reportedly takes a total of 137ms, then you can just divide by 100 and get an average duration of 1.37ms for each operation, right? Well, not exactly. A straight mathematical average by itself is definitely not sufficient for making judgments about performance which you plan to extrapolate to the breadth of your entire application. With a hundred iterations, even a couple of outliers (high or low) can skew the average, and then when you apply that conclusion repeatedly, you even further inflate the skew beyond credulity. Instead of just running for a fixed number of iterations, you can instead choose to run the loop of tests until a certain amount of time has passed. That might be more reliable, but how do you decide how long to run? You might guess that it should be some multiple of how long your operation should take to run once. Wrong. Actually, the length of time to repeat across should be based on the accuracy of the timer you're using, specifically to minimize the chances of inaccuracy. The less precise your timer, the longer you need to run to make sure you've minimized the error percentage. A 15ms timer is pretty bad for accurate benchmarking; to minimize its uncertainty (aka "error rate") to less than 1%, you need to run your each cycle of test iterations for 750ms. A 1ms timer only needs a cycle to run for 50ms to get the same confidence. But then, that's just a single sample. To be sure you're factoring out the skew, you'll want lots of samples to average across. You'll also want to understand something about just how slow the worst sample is, how fast the best sample is, how far apart those best and worse cases were, and so on. You'll want to know not just a number that tells you how fast something ran, but also to have some quantifiable measure of how trustable that number is. Also, you probably want to combine these different techniques (as well as others), so that you get the best balance of all the possible approaches. That's all bare minimum just to get started. If you've been approaching performance benchmarking with anything less serious than what I just glossed over, well... "you don't know: proper benchmarking." ### Benchmark.js Any relevant and reliable benchmark should be based on statistically sound practices. I am not going to write a chapter on statistics here, so I'll hand wave around some terms: standard deviation, variance, margin of error. If you don't know what those terms really mean -- I took a stats class back in college and I'm still a little fuzzy on them -- you are not actually qualified to write your own benchmarking logic. Luckily, smart folks like John-David Dalton and Mathias Bynens do understand these concepts, and wrote a statistically sound benchmarking tool called Benchmark.js (http://benchmarkjs.com/). So I can end the suspense by simply saying: "just use that tool." I won't repeat their whole documentation for how Benchmark.js works; they have fantastic API Docs (http://benchmarkjs.com/docs) you should read. Also there are some great (http://calendar.perfplanet.com/2010/bulletproof-javascript-benchmarks/) writeups (http://monsur.hossa.in/2012/12/11/benchmarkjs.html) on more of the details and methodology. But just for quick illustration purposes, here's how you could use Benchmark.js to run a quick performance test: ```js function foo() { // operation(s) to test } var bench = new Benchmark( "foo test", // test name foo, // function to test (just contents) { // .. // optional extra options (see docs) } ); bench.hz; // number of operations per second bench.stats.moe; // margin of error bench.stats.variance; // variance across samples // .. ``` There's *lots* more to learn about using Benchmark.js besides this glance I'm including here. But the point is that it's handling all of the complexities of setting up a fair, reliable, and valid performance benchmark for a given piece of JavaScript code. If you're going to try to test and benchmark your code, this library is the first place you should turn. We're showing here the usage to test a single operation like X, but it's fairly common that you want to compare X to Y. This is easy to do by simply setting up two different tests in a "Suite" (a Benchmark.js organizational feature). Then, you run them head-to-head, and compare the statistics to conclude whether X or Y was faster. Benchmark.js can of course be used to test JavaScript in a browser (see the "jsPerf.com" section later in this chapter), but it can also run in non-browser environments (Node.js, etc.). One largely untapped potential use-case for Benchmark.js is to use it in your Dev or QA environments to run automated performance regression tests against critical path parts of your application's JavaScript. Similar to how you might run unit test suites before deployment, you can also compare the performance against previous benchmarks to monitor if you are improving or degrading application performance. #### Setup/Teardown In the previous code snippet, we glossed over the "extra options" `{ .. }` object. But there are two options we should discuss: `setup` and `teardown`. These two options let you define functions to be called before and after your test case runs. It's incredibly important to understand that your `setup` and `teardown` code **does not run for each test iteration**. The best way to think about it is that there's an outer loop (repeating cycles), and an inner loop (repeating test iterations). `setup` and `teardown` are run at the beginning and end of each *outer* loop (aka cycle) iteration, but not inside the inner loop. Why does this matter? Let's imagine you have a test case that looks like this: ```js a = a + "w"; b = a.charAt( 1 ); ``` Then, you set up your test `setup` as follows: ```js var a = "x"; ``` Your temptation is probably to believe that `a` is starting out as `"x"` for each test iteration. But it's not! It's starting `a` at `"x"` for each test cycle, and then your repeated `+ "w"` concatenations will be making a larger and larger `a` value, even though you're only ever accessing the character `"w"` at the `1` position. Where this most commonly bites you is when you make side effect changes to something like the DOM, like appending a child element. You may think your parent element is set as empty each time, but it's actually getting lots of elements added, and that can significantly sway the results of your tests. ## Context Is King Don't forget to check the context of a particular performance benchmark, especially a comparison between X and Y tasks. Just because your test reveals that X is faster than Y doesn't mean that the conclusion "X is faster than Y" is actually relevant. For example, let's say a performance test reveals that X runs 10,000,000 operations per second, and Y runs at 8,000,000 operations per second. You could claim that Y is 20% slower than X, and you'd be mathematically correct, but your assertion doesn't hold as much water as you'd think. Let's think about the results more critically: 10,000,000 operations per second is 10,000 operations per millisecond, and 10 operations per microsecond. In other words, a single operation takes 0.1 microseconds, or 100 nanoseconds. It's hard to fathom just how small 100ns is, but for comparison, it's often cited that the human eye isn't generally capable of distinguishing anything less than 100ms, which is one million times slower than the 100ns speed of the X operation. Even recent scientific studies showing that maybe the brain can process as quick as 13ms (about 8x faster than previously asserted) would mean that X is still running 125,000 times faster than the human brain can perceive a distinct thing happening. **X is going really, really fast.** But more importantly, let's talk about the difference between X and Y, the 2,000,000 operations per second difference. If X takes 100ns, and Y takes 80ns, the difference is 20ns, which in the best case is still one 650-thousandth of the interval the human brain can perceive. What's my point? **None of this performance difference matters, at all!** But wait, what if this operation is going to happen a whole bunch of times in a row? Then the difference could add up, right? OK, so what we're asking then is, how likely is it that operation X is going to be run over and over again, one right after the other, and that this has to happen 650,000 times just to get a sliver of a hope the human brain could perceive it. More likely, it'd have to happen 5,000,000 to 10,000,000 times together in a tight loop to even approach relevance. While the computer scientist in you might protest that this is possible, the louder voice of realism in you should sanity check just how likely or unlikely that really is. Even if it is relevant in rare occasions, it's irrelevant in most situations. The vast majority of your benchmark results on tiny operations -- like the `++x` vs `x++` myth -- **are just totally bogus** for supporting the conclusion that X should be favored over Y on a performance basis. ### Engine Optimizations You simply cannot reliably extrapolate that if X was 10 microseconds faster than Y in your isolated test, that means X is always faster than Y and should always be used. That's not how performance works. It's vastly more complicated. For example, let's imagine (purely hypothetical) that you test some microperformance behavior such as comparing: ```js var twelve = "12"; var foo = "foo"; // test 1 var X1 = parseInt( twelve ); var X2 = parseInt( foo ); // test 2 var Y1 = Number( twelve ); var Y2 = Number( foo ); ``` If you understand what `parseInt(..)` does compared to `Number(..)`, you might intuit that `parseInt(..)` potentially has "more work" to do, especially in the `foo` case. Or you might intuit that they should have the same amount of work to do in the `foo` case, as both should be able to stop at the first character `"f"`. Which intuition is correct? I honestly don't know. But I'll make the case it doesn't matter what your intuition is. What might the results be when you test it? Again, I'm making up a pure hypothetical here, I haven't actually tried, nor do I care. Let's pretend the test comes back that `X` and `Y` are statistically identical. Have you then confirmed your intuition about the `"f"` character thing? Nope. It's possible in our hypothetical that the engine might recognize that the variables `twelve` and `foo` are only being used in one place in each test, and so it might decide to inline those values. Then it may realize that `Number( "12" )` can just be replaced by `12`. And maybe it comes to the same conclusion with `parseInt(..)`, or maybe not. Or an engine's dead-code removal heuristic could kick in, and it could realize that variables `X` and `Y` aren't being used, so declaring them is irrelevant, so it doesn't end up doing anything at all in either test. And all that's just made with the mindset of assumptions about a single test run. Modern engines are fantastically more complicated than what we're intuiting here. They do all sorts of tricks, like tracing and tracking how a piece of code behaves over a short period of time, or with a particularly constrained set of inputs. What if the engine optimizes a certain way because of the fixed input, but in your real program you give more varied input and the optimization decisions shake out differently (or not at all!)? Or what if the engine kicks in optimizations because it sees the code being run tens of thousands of times by the benchmarking utility, but in your real program it will only run a hundred times in near proximity, and under those conditions the engine determines the optimizations are not worth it? And all those optimizations we just hypothesized about might happen in our constrained test but maybe the engine wouldn't do them in a more complex program (for various reasons). Or it could be reversed -- the engine might not optimize such trivial code but may be more inclined to optimize it more aggressively when the system is already more taxed by a more sophisticated program. The point I'm trying to make is that you really don't know for sure exactly what's going on under the covers. All the guesses and hypothesis you can muster don't amount to hardly anything concrete for really making such decisions. Does that mean you can't really do any useful testing? **Definitely not!** What this boils down to is that testing *not real* code gives you *not real* results. In so much as is possible and practical, you should test actual real, non-trivial snippets of your code, and under as best of real conditions as you can actually hope to. Only then will the results you get have a chance to approximate reality. Microbenchmarks like `++x` vs `x++` are so incredibly likely to be bogus, we might as well just flatly assume them as such. ## jsPerf.com While Benchmark.js is useful for testing the performance of your code in whatever JS environment you're running, it cannot be stressed enough that you need to compile test results from lots of different environments (desktop browsers, mobile devices, etc.) if you want to have any hope of reliable test conclusions. For example, Chrome on a high-end desktop machine is not likely to perform anywhere near the same as Chrome mobile on a smartphone. And a smartphone with a full battery charge is not likely to perform anywhere near the same as a smartphone with 2% battery life left, when the device is starting to power down the radio and processor. If you want to make assertions like "X is faster than Y" in any reasonable sense across more than just a single environment, you're going to need to actually test as many of those real world environments as possible. Just because Chrome executes some X operation faster than Y doesn't mean that all browsers do. And of course you also probably will want to cross-reference the results of multiple browser test runs with the demographics of your users. There's an awesome website for this purpose called jsPerf (http://jsperf.com). It uses the Benchmark.js library we talked about earlier to run statistically accurate and reliable tests, and makes the test on an openly available URL that you can pass around to others. Each time a test is run, the results are collected and persisted with the test, and the cumulative test results are graphed on the page for anyone to see. When creating a test on the site, you start out with two test cases to fill in, but you can add as many as you need. You also have the ability to set up `setup` code that is run at the beginning of each test cycle and `teardown` code run at the end of each cycle. **Note:** A trick for doing just one test case (if you're benchmarking a single approach instead of a head-to-head) is to fill in the second test input boxes with placeholder text on first creation, then edit the test and leave the second test blank, which will delete it. You can always add more test cases later. You can define the initial page setup (importing libraries, defining utility helper functions, declaring variables, etc.). There are also options for defining setup and teardown behavior if needed -- consult the "Setup/Teardown" section in the Benchmark.js discussion earlier. ### Sanity Check jsPerf is a fantastic resource, but there's an awful lot of tests published that when you analyze them are quite flawed or bogus, for any of a variety of reasons as outlined so far in this chapter. Consider: ```js // Case 1 var x = []; for (var i=0; i<10; i++) { x[i] = "x"; } // Case 2 var x = []; for (var i=0; i<10; i++) { x[x.length] = "x"; } // Case 3 var x = []; for (var i=0; i<10; i++) { x.push( "x" ); } ``` Some observations to ponder about this test scenario: * It's extremely common for devs to put their own loops into test cases, and they forget that Benchmark.js already does all the repetition you need. There's a really strong chance that the `for` loops in these cases are totally unnecessary noise. * The declaring and initializing of `x` is included in each test case, possibly unnecessarily. Recall from earlier that if `x = []` were in the `setup` code, it wouldn't actually be run before each test iteration, but instead once at the beginning of each cycle. That means `x` would continue growing quite large, not just the size `10` implied by the `for` loops. So is the intent to make sure the tests are constrained only to how the JS engine behaves with very small arrays (size `10`)? That *could* be the intent, but if it is, you have to consider if that's not focusing far too much on nuanced internal implementation details. On the other hand, does the intent of the test embrace the context that the arrays will actually be growing quite large? Is the JS engines' behavior with larger arrays relevant and accurate when compared with the intended real world usage? * Is the intent to find out how much `x.length` or `x.push(..)` add to the performance of the operation to append to the `x` array? OK, that might be a valid thing to test. But then again, `push(..)` is a function call, so of course it's going to be slower than `[..]` access. Arguably, cases 1 and 2 are fairer than case 3. Here's another example that illustrates a common apples-to-oranges flaw: ```js // Case 1 var x = ["John","Albert","Sue","Frank","Bob"]; x.sort(); // Case 2 var x = ["John","Albert","Sue","Frank","Bob"]; x.sort( function mySort(a,b){ if (a < b) return -1; if (a > b) return 1; return 0; } ); ``` Here, the obvious intent is to find out how much slower the custom `mySort(..)` comparator is than the built-in default comparator. But by specifying the function `mySort(..)` as inline function expression, you've created an unfair/bogus test. Here, the second case is not only testing a custom user JS function, **but it's also testing creating a new function expression for each iteration.** Would it surprise you to find out that if you run a similar test but update it to isolate only for creating an inline function expression versus using a pre-declared function, the inline function expression creation can be from 2% to 20% slower!? Unless your intent with this test *is* to consider the inline function expression creation "cost," a better/fairer test would put `mySort(..)`'s declaration in the page setup -- don't put it in the test `setup` as that's unnecessary redeclaration for each cycle -- and simply reference it by name in the test case: `x.sort(mySort)`. Building on the previous example, another pitfall is in opaquely avoiding or adding "extra work" to one test case that creates an apples-to-oranges scenario: ```js // Case 1 var x = [12,-14,0,3,18,0,2.9]; x.sort(); // Case 2 var x = [12,-14,0,3,18,0,2.9]; x.sort( function mySort(a,b){ return a - b; } ); ``` Setting aside the previously mentioned inline function expression pitfall, the second case's `mySort(..)` works in this case because you have provided it numbers, but would have of course failed with strings. The first case doesn't throw an error, but it actually behaves differently and has a different outcome! It should be obvious, but: **a different outcome between two test cases almost certainly invalidates the entire test!** But beyond the different outcomes, in this case, the built in `sort(..)`'s comparator is actually doing "extra work" that `mySort()` does not, in that the built-in one coerces the compared values to strings and does lexicographic comparison. The first snippet results in `[-14, 0, 0, 12, 18, 2.9, 3]` while the second snippet results (likely more accurately based on intent) in `[-14, 0, 0, 2.9, 3, 12, 18]`. So that test is unfair because it's not actually doing the same task between the cases. Any results you get are bogus. These same pitfalls can even be much more subtle: ```js // Case 1 var x = false; var y = x ? 1 : 2; // Case 2 var x; var y = x ? 1 : 2; ``` Here, the intent might be to test the performance impact of the coercion to a Boolean that the `? :` operator will do if the `x` expression is not already a Boolean (see the *Types & Grammar* title of this book series). So, you're apparently OK with the fact that there is extra work to do the coercion in the second case. The subtle problem? You're setting `x`'s value in the first case and not setting it in the other, so you're actually doing work in the first case that you're not doing in the second. To eliminate any potential (albeit minor) skew, try: ```js // Case 1 var x = false; var y = x ? 1 : 2; // Case 2 var x = undefined; var y = x ? 1 : 2; ``` Now there's an assignment in both cases, so the thing you want to test -- the coercion of `x` or not -- has likely been more accurately isolated and tested. ## Writing Good Tests Let me see if I can articulate the bigger point I'm trying to make here. Good test authoring requires careful analytical thinking about what differences exist between two test cases and whether the differences between them are *intentional* or *unintentional*. Intentional differences are of course normal and OK, but it's too easy to create unintentional differences that skew your results. You have to be really, really careful to avoid that skew. Moreover, you may intend a difference but it may not be obvious to other readers of your test what your intent was, so they may doubt (or trust!) your test incorrectly. How do you fix that? **Write better, clearer tests.** But also, take the time to document (using the jsPerf.com "Description" field and/or code comments) exactly what the intent of your test is, even to the nuanced detail. Call out the intentional differences, which will help others and your future self to better identify unintentional differences that could be skewing the test results. Isolate things which aren't relevant to your test by pre-declaring them in the page or test setup settings so they're outside the timed parts of the test. Instead of trying to narrow in on a tiny snippet of your real code and benchmarking just that piece out of context, tests and benchmarks are better when they include a larger (while still relevant) context. Those tests also tend to run slower, which means any differences you spot are more relevant in context. ## Microperformance OK, until now we've been dancing around various microperformance issues and generally looking disfavorably upon obsessing about them. I want to take just a moment to address them directly. The first thing you need to get more comfortable with when thinking about performance benchmarking your code is that the code you write is not always the code the engine actually runs. We briefly looked at that topic back in Chapter 1 when we discussed statement reordering by the compiler, but here we're going to suggest the compiler can sometimes decide to run different code than you wrote, not just in different orders but different in substance. Let's consider this piece of code: ```js var foo = 41; (function(){ (function(){ (function(baz){ var bar = foo + baz; // .. })(1); })(); })(); ``` You may think about the `foo` reference in the innermost function as needing to do a three-level scope lookup. We covered in the *Scope & Closures* title of this book series how lexical scope works, and the fact that the compiler generally caches such lookups so that referencing `foo` from different scopes doesn't really practically "cost" anything extra. But there's something deeper to consider. What if the compiler realizes that `foo` isn't referenced anywhere else but that one location, and it further notices that the value never is anything except the `41` as shown? Isn't it quite possible and acceptable that the JS compiler could decide to just remove the `foo` variable entirely, and *inline* the value, such as this: ```js (function(){ (function(){ (function(baz){ var bar = 41 + baz; // .. })(1); })(); })(); ``` **Note:** Of course, the compiler could probably also do a similar analysis and rewrite with the `baz` variable here, too. When you begin to think about your JS code as being a hint or suggestion to the engine of what to do, rather than a literal requirement, you realize that a lot of the obsession over discrete syntactic minutia is most likely unfounded. Another example: ```js function factorial(n) { if (n < 2) return 1; return n * factorial( n - 1 ); } factorial( 5 ); // 120 ``` Ah, the good ol' fashioned "factorial" algorithm! You might assume that the JS engine will run that code mostly as is. And to be honest, it might -- I'm not really sure. But as an anecdote, the same code expressed in C and compiled with advanced optimizations would result in the compiler realizing that the call `factorial(5)` can just be replaced with the constant value `120`, eliminating the function and call entirely! Moreover, some engines have a practice called "unrolling recursion," where it can realize that the recursion you've expressed can actually be done "easier" (i.e., more optimally) with a loop. It's possible the preceding code could be *rewritten* by a JS engine to run as: ```js function factorial(n) { if (n < 2) return 1; var res = 1; for (var i=n; i>1; i--) { res *= i; } return res; } factorial( 5 ); // 120 ``` Now, let's imagine that in the earlier snippet you had been worried about whether `n * factorial(n-1)` or `n *= factorial(--n)` runs faster. Maybe you even did a performance benchmark to try to figure out which was better. But you miss the fact that in the bigger context, the engine may not run either line of code because it may unroll the recursion! Speaking of `--`, `--n` versus `n--` is often cited as one of those places where you can optimize by choosing the `--n` version, because theoretically it requires less effort down at the assembly level of processing. That sort of obsession is basically nonsense in modern JavaScript. That's the kind of thing you should be letting the engine take care of. You should write the code that makes the most sense. Compare these three `for` loops: ```js // Option 1 for (var i=0; i<10; i++) { console.log( i ); } // Option 2 for (var i=0; i<10; ++i) { console.log( i ); } // Option 3 for (var i=-1; ++i<10; ) { console.log( i ); } ``` Even if you have some theory where the second or third option is more performant than the first option by a tiny bit, which is dubious at best, the third loop is more confusing because you have to start with `-1` for `i` to account for the fact that `++i` pre-increment is used. And the difference between the first and second options is really quite irrelevant. It's entirely possible that a JS engine may see a place where `i++` is used and realize that it can safely replace it with the `++i` equivalent, which means your time spent deciding which one to pick was completely wasted and the outcome moot. Here's another common example of silly microperformance obsession: ```js var x = [ .. ]; // Option 1 for (var i=0; i < x.length; i++) { // .. } // Option 2 for (var i=0, len = x.length; i < len; i++) { // .. } ``` The theory here goes that you should cache the length of the `x` array in the variable `len`, because ostensibly it doesn't change, to avoid paying the price of `x.length` being consulted for each iteration of the loop. If you run performance benchmarks around `x.length` usage compared to caching it in a `len` variable, you'll find that while the theory sounds nice, in practice any measured differences are statistically completely irrelevant. In fact, in some engines like v8, it can be shown (http://mrale.ph/blog/2014/12/24/array-length-caching.html) that you could make things slightly worse by pre-caching the length instead of letting the engine figure it out for you. Don't try to outsmart your JavaScript engine, you'll probably lose when it comes to performance optimizations. ### Not All Engines Are Alike The different JS engines in various browsers can all be "spec compliant" while having radically different ways of handling code. The JS specification doesn't require anything performance related -- well, except ES6's "Tail Call Optimization" covered later in this chapter. The engines are free to decide that one operation will receive its attention to optimize, perhaps trading off for lesser performance on another operation. It can be very tenuous to find an approach for an operation that always runs faster in all browsers. There's a movement among some in the JS dev community, especially those who work with Node.js, to analyze the specific internal implementation details of the v8 JavaScript engine and make decisions about writing JS code that is tailored to take best advantage of how v8 works. You can actually achieve a surprisingly high degree of performance optimization with such endeavors, so the payoff for the effort can be quite high. Some commonly cited examples (https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) for v8: * Don't pass the `arguments` variable from one function to any other function, as such "leakage" slows down the function implementation. * Isolate a `try..catch` in its own function. Browsers struggle with optimizing any function with a `try..catch` in it, so moving that construct to its own function means you contain the de-optimization harm while letting the surrounding code be optimizable. But rather than focus on those tips specifically, let's sanity check the v8-only optimization approach in a general sense. Are you genuinely writing code that only needs to run in one JS engine? Even if your code is entirely intended for Node.js *right now*, is the assumption that v8 will *always* be the used JS engine reliable? Is it possible that someday a few years from now, there's another server-side JS platform besides Node.js that you choose to run your code on? What if what you optimized for before is now a much slower way of doing that operation on the new engine? Or what if your code always stays running on v8 from here on out, but v8 decides at some point to change the way some set of operations works such that what used to be fast is now slow, and vice versa? These scenarios aren't just theoretical, either. It used to be that it was faster to put multiple string values into an array and then call `join("")` on the array to concatenate the values than to just use `+` concatenation directly with the values. The historical reason for this is nuanced, but it has to do with internal implementation details about how string values were stored and managed in memory. As a result, "best practice" advice at the time disseminated across the industry suggesting developers always use the array `join(..)` approach. And many followed. Except, somewhere along the way, the JS engines changed approaches for internally managing strings, and specifically put in optimizations for `+` concatenation. They didn't slow down `join(..)` per se, but they put more effort into helping `+` usage, as it was still quite a bit more widespread. **Note:** The practice of standardizing or optimizing some particular approach based mostly on its existing widespread usage is often called (metaphorically) "paving the cowpath." Once that new approach to handling strings and concatenation took hold, unfortunately all the code out in the wild that was using array `join(..)` to concatenate strings was then sub-optimal. Another example: at one time, the Opera browser differed from other browsers in how it handled the boxing/unboxing of primitive wrapper objects (see the *Types & Grammar* title of this book series). As such, their advice to developers was to use a `String` object instead of the primitive `string` value if properties like `length` or methods like `charAt(..)` needed to be accessed. This advice may have been correct for Opera at the time, but it was literally completely opposite for other major contemporary browsers, as they had optimizations specifically for the `string` primitives and not their object wrapper counterparts. I think these various gotchas are at least possible, if not likely, for code even today. So I'm very cautious about making wide ranging performance optimizations in my JS code based purely on engine implementation details, **especially if those details are only true of a single engine**. The reverse is also something to be wary of: you shouldn't necessarily change a piece of code to work around one engine's difficulty with running a piece of code in an acceptably performant way. Historically, IE has been the brunt of many such frustrations, given that there have been plenty of scenarios in older IE versions where it struggled with some performance aspect that other major browsers of the time seemed not to have much trouble with. The string concatenation discussion we just had was actually a real concern back in the IE6 and IE7 days, where it was possible to get better performance out of `join(..)` than `+`. But it's troublesome to suggest that just one browser's trouble with performance is justification for using a code approach that quite possibly could be sub-optimal in all other browsers. Even if the browser in question has a large market share for your site's audience, it may be more practical to write the proper code and rely on the browser to update itself with better optimizations eventually. "There is nothing more permanent than a temporary hack." Chances are, the code you write now to work around some performance bug will probably outlive the performance bug in the browser itself. In the days when a browser only updated once every five years, that was a tougher call to make. But as it stands now, browsers across the board are updating at a much more rapid interval (though obviously the mobile world still lags), and they're all competing to optimize web features better and better. If you run across a case where a browser *does* have a performance wart that others don't suffer from, make sure to report it to them through whatever means you have available. Most browsers have open public bug trackers suitable for this purpose. **Tip:** I'd only suggest working around a performance issue in a browser if it was a really drastic show-stopper, not just an annoyance or frustration. And I'd be very careful to check that the performance hack didn't have noticeable negative side effects in another browser. ### Big Picture Instead of worrying about all these microperformance nuances, we should instead be looking at big-picture types of optimizations. How do you know what's big picture or not? You have to first understand if your code is running on a critical path or not. If it's not on the critical path, chances are your optimizations are not worth much. Ever heard the admonition, "that's premature optimization!"? It comes from a famous quote from Donald Knuth: "premature optimization is the root of all evil.". Many developers cite this quote to suggest that most optimizations are "premature" and are thus a waste of effort. The truth is, as usual, more nuanced. Here is Knuth's quote, in context: > Programmers waste enormous amounts of time thinking about, or worrying about, the speed of **noncritical** parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that **critical** 3%. [emphasis added] (http://web.archive.org/web/20130731202547/http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf, Computing Surveys, Vol 6, No 4, December 1974) I believe it's a fair paraphrasing to say that Knuth *meant*: "non-critical path optimization is the root of all evil." So the key is to figure out if your code is on the critical path -- you should optimize it! -- or not. I'd even go so far as to say this: no amount of time spent optimizing critical paths is wasted, no matter how little is saved; but no amount of optimization on noncritical paths is justified, no matter how much is saved. If your code is on the critical path, such as a "hot" piece of code that's going to be run over and over again, or in UX critical places where users will notice, like an animation loop or CSS style updates, then you should spare no effort in trying to employ relevant, measurably significant optimizations. For example, consider a critical path animation loop that needs to coerce a string value to a number. There are of course multiple ways to do that (see the *Types & Grammar* title of this book series), but which one if any is the fastest? ```js var x = "42"; // need number `42` // Option 1: let implicit coercion automatically happen var y = x / 2; // Option 2: use `parseInt(..)` var y = parseInt( x, 0 ) / 2; // Option 3: use `Number(..)` var y = Number( x ) / 2; // Option 4: use `+` unary operator var y = +x / 2; // Option 5: use `|` unary operator var y = (x | 0) / 2; ``` **Note:** I will leave it as an exercise to the reader to set up a test if you're interested in examining the minute differences in performance among these options. When considering these different options, as they say, "One of these things is not like the others." `parseInt(..)` does the job, but it also does a lot more -- it parses the string rather than just coercing. You can probably guess, correctly, that `parseInt(..)` is a slower option, and you should probably avoid it. Of course, if `x` can ever be a value that **needs parsing**, such as `"42px"` (like from a CSS style lookup), then `parseInt(..)` really is the only suitable option! `Number(..)` is also a function call. From a behavioral perspective, it's identical to the `+` unary operator option, but it may in fact be a little slower, requiring more machinery to execute the function. Of course, it's also possible that the JS engine recognizes this behavioral symmetry and just handles the inlining of `Number(..)`'s behavior (aka `+x`) for you! But remember, obsessing about `+x` versus `x | 0` is in most cases likely a waste of effort. This is a microperformance issue, and one that you shouldn't let dictate/degrade the readability of your program. While performance is very important in critical paths of your program, it's not the only factor. Among several options that are roughly similar in performance, readability should be another important concern. ## Tail Call Optimization (TCO) As we briefly mentioned earlier, ES6 includes a specific requirement that ventures into the world of performance. It's related to a specific form of optimization that can occur with function calls: *tail call optimization*. Briefly, a "tail call" is a function call that appears at the "tail" of another function, such that after the call finishes, there's nothing left to do (except perhaps return its result value). For example, here's a non-recursive setup with tail calls: ```js function foo(x) { return x; } function bar(y) { return foo( y + 1 ); // tail call } function baz() { return 1 + bar( 40 ); // not tail call } baz(); // 42 ``` `foo(y+1)` is a tail call in `bar(..)` because after `foo(..)` finishes, `bar(..)` is also finished except in this case returning the result of the `foo(..)` call. However, `bar(40)` is *not* a tail call because after it completes, its result value must be added to `1` before `baz()` can return it. Without getting into too much nitty-gritty detail, calling a new function requires an extra amount of reserved memory to manage the call stack, called a "stack frame." So the preceding snippet would generally require a stack frame for each of `baz()`, `bar(..)`, and `foo(..)` all at the same time. However, if a TCO-capable engine can realize that the `foo(y+1)` call is in *tail position* meaning `bar(..)` is basically complete, then when calling `foo(..)`, it doesn't need to create a new stack frame, but can instead reuse the existing stack frame from `bar(..)`. That's not only faster, but it also uses less memory. That sort of optimization isn't a big deal in a simple snippet, but it becomes a *much bigger deal* when dealing with recursion, especially if the recursion could have resulted in hundreds or thousands of stack frames. With TCO the engine can perform all those calls with a single stack frame! Recursion is a hairy topic in JS because without TCO, engines have had to implement arbitrary (and different!) limits to how deep they will let the recursion stack get before they stop it, to prevent running out of memory. With TCO, recursive functions with *tail position* calls can essentially run unbounded, because there's never any extra usage of memory! Consider that recursive `factorial(..)` from before, but rewritten to make it TCO friendly: ```js function factorial(n) { function fact(n,res) { if (n < 2) return res; return fact( n - 1, n * res ); } return fact( n, 1 ); } factorial( 5 ); // 120 ``` This version of `factorial(..)` is still recursive, but it's also optimizable with TCO, because both inner `fact(..)` calls are in *tail position*. **Note:** It's important to note that TCO only applies if there's actually a tail call. If you write recursive functions without tail calls, the performance will still fall back to normal stack frame allocation, and the engines' limits on such recursive call stacks will still apply. Many recursive functions can be rewritten as we just showed with `factorial(..)`, but it takes careful attention to detail. One reason that ES6 requires engines to implement TCO rather than leaving it up to their discretion is because the *lack of TCO* actually tends to reduce the chances that certain algorithms will be implemented in JS using recursion, for fear of the call stack limits. If the lack of TCO in the engine would just gracefully degrade to slower performance in all cases, it wouldn't probably have been something that ES6 needed to *require*. But because the lack of TCO can actually make certain programs impractical, it's more an important feature of the language than just a hidden implementation detail. ES6 guarantees that from now on, JS developers will be able to rely on this optimization across all ES6+ compliant browsers. That's a win for JS performance! ## Review Effectively benchmarking performance of a piece of code, especially to compare it to another option for that same code to see which approach is faster, requires careful attention to detail. Rather than rolling your own statistically valid benchmarking logic, just use the Benchmark.js library, which does that for you. But be careful about how you author tests, because it's far too easy to construct a test that seems valid but that's actually flawed -- even tiny differences can skew the results to be completely unreliable. It's important to get as many test results from as many different environments as possible to eliminate hardware/device bias. jsPerf.com is a fantastic website for crowdsourcing performance benchmark test runs. Many common performance tests unfortunately obsess about irrelevant microperformance details like `x++` versus `++x`. Writing good tests means understanding how to focus on big picture concerns, like optimizing on the critical path, and avoiding falling into traps like different JS engines' implementation details. Tail call optimization (TCO) is a required optimization as of ES6 that will make some recursive patterns practical in JS where they would have been impossible otherwise. TCO allows a function call in the *tail position* of another function to execute without needing any extra resources, which means the engine no longer needs to place arbitrary restrictions on call stack depth for recursive algorithms. ================================================ FILE: async & performance/foreword.md ================================================ # Вы не знаете JS: Асинхронность и Производительность # Введение Мой работодатель доверяет мне проводить собеседования на протяжении многих лет. Если мы ищем JavaScript разработчика, то я начинаю опрос с…, нет, не начинаю, я сначала узнаю не нужно ли моему потенциальному коллеге выйти в туалет и/или попить, потому что важна комфортная обстановка, а потом я начну задавать вопросы, чтобы узнать: "Он знает JavaScript или jQuery". В jQuery нет ничего плохого. С помощью него можно многое сделать, не зная JavaScript. Но если требуется высокая квалификация и глубокие знания в оптимизации JavaScript, чтобы при необходимости быстро решать проблемы, нам нужен тот кто знает и понимает, как устроены такие библиотеки, как jQuery изнутри, чтобы использовать чистый JavaScript, как это делают другие библиотеки. Как получить картинку используя только JavaScript, или как делаются замыкания (вы ведь уже читали книгу про [Область видимости и замыкания](https://github.com/azat-io/you-dont-know-js-ru/tree/master/scope%20%26%20closures)) и как получить максимальную пользу от асинхронности, именно об этом будет написано в этой книге. Сначала мы познакомимся с обратными вызовами (callbacks), а в дальнейшем и с обещаниями (promises). Если вы ничего не знаете о обещаниях (promises), сейчас самое время это исправить. Обещания теперь стандарт использования асинхронности в JavaScript и DOM. Все будущие DOM API будут использовать обещания (promises), многие уже это делают, так что будьте готовы! В момент написания этой книги, обещания (promises) поддерживаются в основных браузерах [Подробнее](http://caniuse.com/#search=Promises), а IE уже на подходе. Я надеюсь, что после изучения у вас останется место для следующего курса про генераторы. Генераторы пробрались в стабильные версии браузеров Chrome и Firefox, от них больше головной боли, чем пользы. Именно так я думал пока не увидел их в вместе с обещаниями (promises). Там они стали важным инструментом в читабельности и поддержке кода. На десерт, я не буду портить сюрприз, приготовьтесь увидеть будущее JavaScript! Особенности, которые дают вам больше контроля над параллельностью и асинхронностью. Что ж, я больше не буду задерживать вас от прочтения этой книги. Если вы уже прочитали часть книги перед тем, как прочитать это предисловие, то дайте себе 10 очков по асинхронности! Вы действительно их заслуживаете! Джейк Арчибальд
[jakearchibald.com](https://jakearchibald.com), [@jaffathecake](https://twitter.com/jaffathecake)
Google Chrome Разработчик ================================================ FILE: async & performance/toc.md ================================================ # Вы не знаете JS: Асинхронность и Производительность ## Содержимое * Предисловие * Preface * Chapter 1: Asynchrony: Now & Later * A Program In Chunks * Event Loop * Parallel Threading * Concurrency * Jobs * Statement Ordering * Chapter 2: Callbacks * Continuations * Sequential Brain * Trust Issues * Trying To Save Callbacks * Chapter 3: Promises * What is a Promise? * Thenable Duck-Typing * Promise Trust * Chain Flow * Error Handling * Promise Patterns * Promise API Recap * Promise Limitations * Chapter 4: Generators * Breaking Run-to-completion * Generator'ing Values * Iterating Generators Asynchronously * Generators + Promises * Generator Delegation * Generator Concurrency * Thunks * Pre-ES6 Generators * Chapter 5: Program Performance * Web Workers * SIMD * asm.js * Chapter 6: Benchmarking & Tuning * Benchmarking * Context Is King * jsPerf.com * Writing Good Tests * Microperformance * Tail Call Optimization (TCO) * Appendix A: *asynquence* Library * Appendix B: Advanced Async Patterns * Appendix C: Acknowledgments ================================================ FILE: es6 & beyond/README.md ================================================ # Вы не знаете JS: ES6 и не только ----- **[Купить цифровую или печатную книгу от издательства O'Reilly (англ.)](http://shop.oreilly.com/product/0636920033769.do)** ----- [Оглавление](toc.md) * [Введение](foreword.md) (by [Rick Waldron](http://bocoup.com/weblog/author/rick-waldron/)) * [Предисловие](../preface.md) * [Глава 1: ES: современность и будущее](ch1.md) * [Глава 2: Синтаксис](ch2.md) * [Глава 3: Структура](ch3.md) * [Глава 4: Управление асинхронными операциями](ch4.md) * [Глава 5: Коллекции](ch5.md) * [Глава 6: Дополнения к API](ch6.md) * [Глава 7: Метапрограммирование](ch7.md) * [Глава 8: За пределами ES6](ch8.md) * [Приложение A: Благодарности!](apA.md) ================================================ FILE: es6 & beyond/apA.md ================================================ # Вы не знаете JS: ES6 и не только # Приложение A: Благодарности Есть множество людей, которых нужно поблагодарить за то, что появилась на свет эта книга и вся серия. Во-первых, я должен поблагодарить мою жену Кристен Симпсон (Christen Simpson) и двух моих детей Итана (Ethan) и Эмили (Emily), за то, что мирились с тем, что их папу вечно надо было отрывать от компьютера. Даже когда я не писал книги, моя одержимость JavaScript приклеивала мой взгляд к экрану больше, чем следовало. То время, которое я занял у моей семьи, и есть причина, по которой эти книги могут так глубоко и полностью объяснить JavaScript для вас, читатель. Я в большом долгу перед своей семьей. Хочу поблагодарить моих редакторов в O'Reilly, а именно Simon St.Laurent и Brian MacDonald, как и остальных в команде редакторов и маркетинга. Работать с ними — одно удовольствие, и хочется особенно поблагодарить их за создание подходящих условий во время этого эксперимента с написанием "open source"-книги, за редактирование и выпуск. Спасибо всем тем, кто участвовал в улучшении этой серии книг присылая предложения по редактированию и корректировке, включая Shelley Powers, Tim Ferro, Evan Borden, Forrest L. Norvell, Jennifer Davis, Jesse Harlin и многих других. Большое спасибо Shane Hudson за написание предисловия к этой серии книг. Спасибо бесчисленным участникам сообщества, включая членов комитета TC39, кто поделился столько многим с нами и особенно за терпеливое отношение к моим бесконечным вопросам и изысканиям. John-David Dalton, Juriy "kangax" Zaytsev, Mathias Bynens, Axel Rauschmayer, Nicholas Zakas, Angus Croll, Reginald Braithwaite, Dave Herman, Brendan Eich, Allen Wirfs-Brock, Bradley Meck, Domenic Denicola, David Walsh, Tim Disney, Peter van der Zee, Andrea Giammarchi, Kit Cambridge, Eric Elliott и многие другие, я упомянул лишь малую часть. Серия книг *Вы не знаете JS* родилась на Kickstarter, поэтому я также хочу поблагодарить всех моих (почти) 500 щедрых инвесторов, без которых эта серия книг не появилась бы: > Jan Szpila, nokiko, Murali Krishnamoorthy, Ryan Joy, Craig Patchett, pdqtrader, Dale Fukami, ray hatfield, R0drigo Perez [Mx], Dan Petitt, Jack Franklin, Andrew Berry, Brian Grinstead, Rob Sutherland, Sergi Meseguer, Phillip Gourley, Mark Watson, Jeff Carouth, Alfredo Sumaran, Martin Sachse, Marcio Barrios, Dan, AimelyneM, Matt Sullivan, Delnatte Pierre-Antoine, Jake Smith, Eugen Tudorancea, Iris, David Trinh, simonstl, Ray Daly, Uros Gruber, Justin Myers, Shai Zonis, Mom & Dad, Devin Clark, Dennis Palmer, Brian Panahi Johnson, Josh Marshall, Marshall, Dennis Kerr, Matt Steele, Erik Slagter, Sacah, Justin Rainbow, Christian Nilsson, Delapouite, D.Pereira, Nicolas Hoizey, George V. Reilly, Dan Reeves, Bruno Laturner, Chad Jennings, Shane King, Jeremiah Lee Cohick, od3n, Stan Yamane, Marko Vucinic, Jim B, Stephen Collins, Ægir Þorsteinsson, Eric Pederson, Owain, Nathan Smith, Jeanetteurphy, Alexandre ELISÉ, Chris Peterson, Rik Watson, Luke Matthews, Justin Lowery, Morten Nielsen, Vernon Kesner, Chetan Shenoy, Paul Tregoing, Marc Grabanski, Dion Almaer, Andrew Sullivan, Keith Elsass, Tom Burke, Brian Ashenfelter, David Stuart, Karl Swedberg, Graeme, Brandon Hays, John Christopher, Gior, manoj reddy, Chad Smith, Jared Harbour, Minoru TODA, Chris Wigley, Daniel Mee, Mike, Handyface, Alex Jahraus, Carl Furrow, Rob Foulkrod, Max Shishkin, Leigh Penny Jr., Robert Ferguson, Mike van Hoenselaar, Hasse Schougaard, rajan venkataguru, Jeff Adams, Trae Robbins, Rolf Langenhuijzen, Jorge Antunes, Alex Koloskov, Hugh Greenish, Tim Jones, Jose Ochoa, Michael Brennan-White, Naga Harish Muvva, Barkóczi Dávid, Kitt Hodsden, Paul McGraw, Sascha Goldhofer, Andrew Metcalf, Markus Krogh, Michael Mathews, Matt Jared, Juanfran, Georgie Kirschner, Kenny Lee, Ted Zhang, Amit Pahwa, Inbal Sinai, Dan Raine, Schabse Laks, Michael Tervoort, Alexandre Abreu, Alan Joseph Williams, NicolasD, Cindy Wong, Reg Braithwaite, LocalPCGuy, Jon Friskics, Chris Merriman, John Pena, Jacob Katz, Sue Lockwood, Magnus Johansson, Jeremy Crapsey, Grzegorz Pawłowski, nico nuzzaci, Christine Wilks, Hans Bergren, charles montgomery, Ariel בר-לבב Fogel, Ivan Kolev, Daniel Campos, Hugh Wood, Christian Bradford, Frédéric Harper, Ionuţ Dan Popa, Jeff Trimble, Rupert Wood, Trey Carrico, Pancho Lopez, Joël kuijten, Tom A Marra, Jeff Jewiss, Jacob Rios, Paolo Di Stefano, Soledad Penades, Chris Gerber, Andrey Dolganov, Wil Moore III, Thomas Martineau, Kareem, Ben Thouret, Udi Nir, Morgan Laupies, jory carson-burson, Nathan L Smith, Eric Damon Walters, Derry Lozano-Hoyland, Geoffrey Wiseman, mkeehner, KatieK, Scott MacFarlane, Brian LaShomb, Adrien Mas, christopher ross, Ian Littman, Dan Atkinson, Elliot Jobe, Nick Dozier, Peter Wooley, John Hoover, dan, Martin A. Jackson, Héctor Fernando Hurtado, andy ennamorato, Paul Seltmann, Melissa Gore, Dave Pollard, Jack Smith, Philip Da Silva, Guy Israeli, @megalithic, Damian Crawford, Felix Gliesche, April Carter Grant, Heidi, jim tierney, Andrea Giammarchi, Nico Vignola, Don Jones, Chris Hartjes, Alex Howes, john gibbon, David J. Groom, BBox, Yu 'Dilys' Sun, Nate Steiner, Brandon Satrom, Brian Wyant, Wesley Hales, Ian Pouncey, Timothy Kevin Oxley, George Terezakis, sanjay raj, Jordan Harband, Marko McLion, Wolfgang Kaufmann, Pascal Peuckert, Dave Nugent, Markus Liebelt, Welling Guzman, Nick Cooley, Daniel Mesquita, Robert Syvarth, Chris Coyier, Rémy Bach, Adam Dougal, Alistair Duggin, David Loidolt, Ed Richer, Brian Chenault, GoldFire Studios, Carles Andrés, Carlos Cabo, Yuya Saito, roberto ricardo, Barnett Klane, Mike Moore, Kevin Marx, Justin Love, Joe Taylor, Paul Dijou, Michael Kohler, Rob Cassie, Mike Tierney, Cody Leroy Lindley, tofuji, Shimon Schwartz, Raymond, Luc De Brouwer, David Hayes, Rhys Brett-Bowen, Dmitry, Aziz Khoury, Dean, Scott Tolinski - Level Up, Clement Boirie, Djordje Lukic, Anton Kotenko, Rafael Corral, Philip Hurwitz, Jonathan Pidgeon, Jason Campbell, Joseph C., SwiftOne, Jan Hohner, Derick Bailey, getify, Daniel Cousineau, Chris Charlton, Eric Turner, David Turner, Joël Galeran, Dharma Vagabond, adam, Dirk van Bergen, dave ♥♫★ furf, Vedran Zakanj, Ryan McAllen, Natalie Patrice Tucker, Eric J. Bivona, Adam Spooner, Aaron Cavano, Kelly Packer, Eric J, Martin Drenovac, Emilis, Michael Pelikan, Scott F. Walter, Josh Freeman, Brandon Hudgeons, vijay chennupati, Bill Glennon, Robin R., Troy Forster, otaku_coder, Brad, Scott, Frederick Ostrander, Adam Brill, Seb Flippence, Michael Anderson, Jacob, Adam Randlett, Standard, Joshua Clanton, Sebastian Kouba, Chris Deck, SwordFire, Hannes Papenberg, Richard Woeber, hnzz, Rob Crowther, Jedidiah Broadbent, Sergey Chernyshev, Jay-Ar Jamon, Ben Combee, luciano bonachela, Mark Tomlinson, Kit Cambridge, Michael Melgares, Jacob Adams, Adrian Bruinhout, Bev Wieber, Scott Puleo, Thomas Herzog, April Leone, Daniel Mizieliński, Kees van Ginkel, Jon Abrams, Erwin Heiser, Avi Laviad, David newell, Jean-Francois Turcot, Niko Roberts, Erik Dana, Charles Neill, Aaron Holmes, Grzegorz Ziółkowski, Nathan Youngman, Timothy, Jacob Mather, Michael Allan, Mohit Seth, Ryan Ewing, Benjamin Van Treese, Marcelo Santos, Denis Wolf, Phil Keys, Chris Yung, Timo Tijhof, Martin Lekvall, Agendine, Greg Whitworth, Helen Humphrey, Dougal Campbell, Johannes Harth, Bruno Girin, Brian Hough, Darren Newton, Craig McPheat, Olivier Tille, Dennis Roethig, Mathias Bynens, Brendan Stromberger, sundeep, John Meyer, Ron Male, John F Croston III, gigante, Carl Bergenhem, B.J. May, Rebekah Tyler, Ted Foxberry, Jordan Reese, Terry Suitor, afeliz, Tom Kiefer, Darragh Duffy, Kevin Vanderbeken, Andy Pearson, Simon Mac Donald, Abid Din, Chris Joel, Tomas Theunissen, David Dick, Paul Grock, Brandon Wood, John Weis, dgrebb, Nick Jenkins, Chuck Lane, Johnny Megahan, marzsman, Tatu Tamminen, Geoffrey Knauth, Alexander Tarmolov, Jeremy Tymes, Chad Auld, Sean Parmelee, Rob Staenke, Dan Bender, Yannick derwa, Joshua Jones, Geert Plaisier, Tom LeZotte, Christen Simpson, Stefan Bruvik, Justin Falcone, Carlos Santana, Michael Weiss, Pablo Villoslada, Peter deHaan, Dimitris Iliopoulos, seyDoggy, Adam Jordens, Noah Kantrowitz, Amol M, Matthew Winnard, Dirk Ginader, Phinam Bui, David Rapson, Andrew Baxter, Florian Bougel, Michael George, Alban Escalier, Daniel Sellers, Sasha Rudan, John Green, Robert Kowalski, David I. Teixeira (@ditma, Charles Carpenter, Justin Yost, Sam S, Denis Ciccale, Kevin Sheurs, Yannick Croissant, Pau Fracés, Stephen McGowan, Shawn Searcy, Chris Ruppel, Kevin Lamping, Jessica Campbell, Christopher Schmitt, Sablons, Jonathan Reisdorf, Bunni Gek, Teddy Huff, Michael Mullany, Michael Fürstenberg, Carl Henderson, Rick Yoesting, Scott Nichols, Hernán Ciudad, Andrew Maier, Mike Stapp, Jesse Shawl, Sérgio Lopes, jsulak, Shawn Price, Joel Clermont, Chris Ridmann, Sean Timm, Jason Finch, Aiden Montgomery, Elijah Manor, Derek Gathright, Jesse Harlin, Dillon Curry, Courtney Myers, Diego Cadenas, Arne de Bree, João Paulo Dubas, James Taylor, Philipp Kraeutli, Mihai Păun, Sam Gharegozlou, joshjs, Matt Murchison, Eric Windham, Timo Behrmann, Andrew Hall, joshua price, Théophile Villard Эта серия книг выпускается в стиле "open source", включая редактирование и выпуск. Мы отдаем дань благодарности GitHub за предоставление такой возможности для сообщества! Еще раз спасибо всех бесчисленным людям, которых я не перечислил по имени, но кого я тем не менее должен поблагодарить. Пусть эта серия книг будет "принадлежать" всем нам и служить вкладом в увеличение информированности и понимании языка JavaScript, на благо все нынешних и будущих вкладчиков в общее дело сообщества. ================================================ FILE: es6 & beyond/ch1.md ================================================ # Вы не знаете JS: ES6 и не только # Глава 1: ES: современность и будущее Для чтения этой книги вы должны хорошо владеть языком JavaScript вплоть до последнего (на момент написания книги) стандарта, который называется ES5 (точнее, ES5.1), поскольку мы с вами будем рассматривать новый стандарт ES6, попутно пытаясь понять, какие перспективы ждут JS. Если вы не очень уверены в своих знаниях JavaScript, рекомендую предварительно ознакомиться с предшествующими книгами серии You Don’t Know JS. * *Up & Going*: Вы только начинаете изучать программирование и JS? Перед вами карта, которая поможет вам в путешествии по новой области знаний. * *Scope & Closures*: Известно ли вам, что в основе лексического контекста JS лежит семантика компилятора (а не интерпретатора)? Можете ли вы объяснить, каким образом замыкания являются прямым результатом лексической области видимости и функций как значений? * *this & Object Prototypes*: Можете ли вы назвать четыре варианта значения ключевого слова this в зависимости от контекста вызова? Приходилось ли вам путаться в псевдоклассах JS, вместо того чтобы воспользоваться более простым шаблоном проектирования behavior delegation? А слышали ли вы когда-нибудь про объекты, связанные с другими объектами (OLOO)? * *Types & Grammar*: Знакомы ли вы со встроенными типами в JS и, что более важно, знаете ли способы корректного и безопасного приведения типов? Насколько уверенно вы разбираетесь в нюансах грамматики и синтаксиса этого языка? * *Async & Performance*: Вы все еще используете обратные вызовы для управления асинхронными действиями? А можете ли вы объяснить, что такое объект promise и как он позволяет избежать ситуации, когда каждая фоновая операция возвращает свой результат (или ошибку) в обратном вызове? Знаете ли вы, как с помощью генераторов улучшить читабельность асинхронного кода? Наконец, известно ли вам, что представляет собой полноценная оптимизация JS-программи отдельных операций? Если вы уже прочитали все эти книги и освоили рассматриваемые там темы, значит, пришло время погрузиться в эволюцию языка JS и исследовать перемены, которые ждут нас как в ближайшее время, так и в отдаленной перспективе. В отличие от предыдущего стандарта, ES6 нельзя назвать еще одним скромным набором добавленных к языку API. Он принес с собой множество новых синтаксических форм, и к некоторым из них, вполне возможно, будет не так-то просто привыкнуть. Появились также новые структуры и новые вспомогательные модули API для различных типов данных. ES6 — это шаг далеко вперед. Даже если вы считаете, что хорошо знаете JS стандарта ES5, вы столкнетесь с множеством незнакомых вещей, так что будьте готовы! В книге рассмотрены все основные нововведения ES6, без которых невозможно войти в курс дела, а также дан краткий обзор планируемых функций — о них имеет смысл знать уже сейчас. **Внимание:** Весь приведенный в книге код рассчитан на среду исполнения ES6+. На момент написания этих строк уровень поддержки ES6 в браузерах и в JS-средах (таких, как Node.js) несколько разнился, так что вы можете обнаружить, что полученный вами результат отличается от описанного.. ## Поддержка версий Стандарт JavaScript официально называется ECMAScript (или сокращенно ES), и до недавнего времени все его версии обозначались только целыми числами. ES1 и ES2 не получили известности и массовой реализации. Первой широко распространившейся основой для JavaScript стал ES3 — стандарт этого языка для браузеров Internet Explorer с 6-й по 8-ю версию и для мобильных браузеров Android 2.x. По политическим причинам, о которых я умолчу, злополучная версия ES4 так и не увидела света. В 2009 году был официально завершен ES5 (ES5.1 появился в 2011-м), получивший распространение в качестве стандарта для множества современных браузеров, таких как Firefox, Chrome, Opera, Safari и др. Следующая версия JS (появление которой было перенесено с 2013-го сначала на 2014-й, а затем на 2015 год) в обсуждениях фигурировала под очевидным именем ES6. Но позднее стали поступать предложения перейти к схеме именования, основанной на годе выхода очередной версии, например ES2016 (она же ES7), которая будет закончена до конца 2016 года. Согласны с таким подходом далеко не все, но есть вероятность, что стандарт ES6 станет известен пользователям под названием ES2015. А появление версии ES2016 станет свидетельством окончательного перехода на новую схему именования. Кроме того, было отмечено, что скорость эволюции JS превышает одну версию в год. Как только в обсуждениях стандарта возникает новая идея, разработчики браузеров предлагают прототипы нового функционала, а программисты-первопроходцы принимаются экспериментировать с кодом. Обычно задолго до официального одобрения новый функционал становится стандартом де-факто благодаря ранним прототипам движка и инструментария. Соответственно, имеет смысл рассматривать будущие версии JS как связанные с появлением нового функционала, а не с произвольным набором основных особенностей (как делается сейчас) или с годом (как планируется). В этом случае номер версии перестает иметь ту важность, которой обладал раньше, а JavaScript превращается в живой, постоянно меняющийся стандарт. И лучше не говорить о коде как о «написанном в соответствии с таким-то стандартом», а рассматривать его в зависимости от поддерживаемых функциональных особенностей. ## Транскомпиляция Быстрая эволюция функционала ставит серьезную проблему перед разработчиками, желающими использовать новые возможности в ситуации, когда их сайты или приложения работают в более старых браузерах, не поддерживающих нововведения. По всей видимости, ES5 не прижился во многих местах, потому что в основном базу кода не приводили в соответствие с новым стандартом до тех пор, пока не прекратилась поддержка большинства, если не всех, предшествующих платформ. В результате многие разработчики только недавно начали пользоваться такими вещами, как, к примеру, строгий режим, появившийся в ES5 более пяти лет назад. Подобные многолетние промедления повсеместно считаются вредными для будущего экосистемы JS. Люди, занимающиеся развитием языка, мечтают, чтобы разработчики начинали создавать код с учетом новых функциональных особенностей и шаблонов, сразу же после того, как будет утверждена спецификация, и браузеры смогут все это реализовывать. Как же разрешить противоречие? Здесь на помощь приходят специальные инструменты, в частности техника транскомпиляции. Грубо говоря, вы посредством специального инструмента преобразуете код ES6 в эквивалент (или нечто близкое к таковому), работающий в окружениях ES5. В качестве примера возьмем сокращенные определения свойства (см. раздел «Расширения объектных литералов» в главе 2). Вот как это делается в ES6: ```js var foo = [1,2,3]; var obj = { foo // означает `foo: foo` }; obj.foo; // [1,2,3] ``` А вот каким образом (примерно) он транскомпилируется: ```js var foo = [1,2,3]; var obj = { foo: foo }; obj.foo; // [1,2,3] ``` Такое небольшое, но удобное преобразование позволяет в случае одинаковых имен сократить объявление объектного литерала foo: foo до foo. Действия транскомпилятора в этом случае представляют собой встроенный рабочий процесс, аналогичный линтингу, минификации и другим подобным операциям. ### Библиотеки Shim (полизаполнения) Далеко не всем новым функциональным особенностям ES6 требуется транскомпилятор. Полизаполнения (polyfills), которые также называют библиотеками Shim, представляют собой шаблоны для определения поведений из новой среды для более старых сред. В синтаксисе полизаполнения недопустимы, но для различных API их вполне можно использовать. Давайте рассмотрим новый метод `Object.is(..)`, предназначенный для проверки строгой эквивалентности двух значений, но без подробных исключений, которые есть у оператора `===` для значений `NaN и -0`. Полизаполнение для метода `Object.is(..)` создается очень просто: ```js if (!Object.is) { Object.is = function(v1, v2) { // проверка для значения `-0` if (v1 === 0 && v2 === 0) { return 1 / v1 === 1 / v2; } // проверка для значения `NaN` if (v1 !== v1) { return v2 !== v2; } // все остальное return v1 === v2; }; } ``` **Tip:** Обратите внимание на внешнее граничное условие оператора `if`, охватывающее полизаполнение. Это важная деталь, означающая, что резервное поведение данный фрагмент кода включает только в более старых контекстах, где рассматриваемый API еще не определен; необходимости переписывать существующий API практически никогда не возникает. Есть замечательная коллекция ES6 Shim (https://github.com/paulmillr/es6-shim/), которую стоит включать во все новые JS-проекты. Предполагается, что JS ждет непрерывное развитие и что поддержка в браузерах новых функций будет реализовываться постепенно по мере их появления, а не большими фрагментами. Так что самая лучшая стратегия сохранения актуальности — это добавление в базу кода полизаполнений, включение транскомпиляции в процесс сборки и постоянная готовность самого разработчика к изменениям. Те же, кто мыслит консервативно и откладывает использование нового функционала, пока не исчезнут все работающие без него браузеры, всегда будут плестись далеко позади. Их обойдут стороной все инновации, позволяющие сделать написание кода на JavaScript более результативным, рациональным и надежным. ## Подводим итоги На момент написания книги стандарт ES6 (кто-то называет его ES2015) только появился, поэтому вам предстоит многому научиться. Однако куда важнее перестроить свое мировоззрение в соответствии с новым вариантом развития языка JavaScript. Обыкновение годами ждать официальных документов, одобряющих смену стандарта, должно остаться в прошлом. Теперь новые функциональные особенности JavaScript сразу же после своего появления реализуются в браузерах, и только от вас зависит, начнете вы пользоваться ими немедленно или же продолжите действовать неэффективно в попытках запрыгнуть в уходящий поезд. Неважно, какие еще формы примет JavaScript, — теперь это будет происходить быстрее, чем когда-либо в прошлом. Транскомпиляторы и полизаполнения — вот инструменты, которые позволят вам все время оставаться на переднем крае развития языка. Вы должны принять новую реальность JavaScript, где разработчикам настоятельно рекомендуется перейти от выжидания к активной позиции. А начнется все с изучения ES6. ================================================ FILE: es6 & beyond/ch2.md ================================================ # You Don't Know JS: ES6 & Beyond # Chapter 2: Syntax If you've been writing JS for any length of time, odds are the syntax is pretty familiar to you. There are certainly many quirks, but overall it's a fairly reasonable and straightforward syntax that draws many similarities from other languages. However, ES6 adds quite a few new syntactic forms that take some getting used to. In this chapter, we'll tour through them to find out what's in store. **Tip:** At the time of this writing, some of the features discussed in this book have been implemented in various browsers (Firefox, Chrome, etc.), but some have only been partially implemented and many others have not been implemented at all. Your experience may be mixed trying these examples directly. If so, try them out with transpilers, as most of these features are covered by those tools. ES6Fiddle (http://www.es6fiddle.net/) is a great, easy-to-use playground for trying out ES6, as is the online REPL for the Babel transpiler (http://babeljs.io/repl/). ## Block-Scoped Declarations You're probably aware that the fundamental unit of variable scoping in JavaScript has always been the `function`. If you needed to create a block of scope, the most prevalent way to do so other than a regular function declaration was the immediately invoked function expression (IIFE). For example: ```js var a = 2; (function IIFE(){ var a = 3; console.log( a ); // 3 })(); console.log( a ); // 2 ``` ### `let` Declarations However, we can now create declarations that are bound to any block, called (unsurprisingly) *block scoping*. This means all we need is a pair of `{ .. }` to create a scope. Instead of using `var`, which always declares variables attached to the enclosing function (or global, if top level) scope, use `let`: ```js var a = 2; { let a = 3; console.log( a ); // 3 } console.log( a ); // 2 ``` It's not very common or idiomatic thus far in JS to use a standalone `{ .. }` block, but it's always been valid. And developers from other languages that have *block scoping* will readily recognize that pattern. I believe this is the best way to create block-scoped variables, with a dedicated `{ .. }` block. Moreover, you should always put the `let` declaration(s) at the very top of that block. If you have more than one to declare, I'd recommend using just one `let`. Stylistically, I even prefer to put the `let` on the same line as the opening `{`, to make it clearer that this block is only for the purpose of declaring the scope for those variables. ```js { let a = 2, b, c; // .. } ``` Now, that's going to look strange and it's not likely going to match the recommendations given in most other ES6 literature. But I have reasons for my madness. There's another experimental (not standardized) form of the `let` declaration called the `let`-block, which looks like: ```js let (a = 2, b, c) { // .. } ``` That form is what I call *explicit* block scoping, whereas the `let ..` declaration form that mirrors `var` is more *implicit*, as it kind of hijacks whatever `{ .. }` pair it's found in. Generally developers find *explicit* mechanisms a bit more preferable than *implicit* mechanisms, and I claim this is one of those cases. If you compare the previous two snippet forms, they're very similar, and in my opinion both qualify stylistically as *explicit* block scoping. Unfortunately, the `let (..) { .. }` form, the most *explicit* of the options, was not adopted in ES6. That may be revisited post-ES6, but for now the former option is our best bet, I think. To reinforce the *implicit* nature of `let ..` declarations, consider these usages: ```js let a = 2; if (a > 1) { let b = a * 3; console.log( b ); // 6 for (let i = a; i <= b; i++) { let j = i + 10; console.log( j ); } // 12 13 14 15 16 let c = a + b; console.log( c ); // 8 } ``` Quick quiz without looking back at that snippet: which variable(s) exist only inside the `if` statement, and which variable(s) exist only inside the `for` loop? The answers: the `if` statement contains `b` and `c` block-scoped variables, and the `for` loop contains `i` and `j` block-scoped variables. Did you have to think about it for a moment? Does it surprise you that `i` isn't added to the enclosing `if` statement scope? That mental pause and questioning -- I call it a "mental tax" -- comes from the fact that this `let` mechanism is not only new to us, but it's also *implicit*. There's also hazard in the `let c = ..` declaration appearing so far down in the scope. Unlike traditional `var`-declared variables, which are attached to the entire enclosing function scope regardless of where they appear, `let` declarations attach to the block scope but are not initialized until they appear in the block. Accessing a `let`-declared variable earlier than its `let ..` declaration/initialization causes an error, whereas with `var` declarations the ordering doesn't matter (except stylistically). Consider: ```js { console.log( a ); // undefined console.log( b ); // ReferenceError! var a; let b; } ``` **Warning:** This `ReferenceError` from accessing too-early `let`-declared references is technically called a *Temporal Dead Zone (TDZ)* error -- you're accessing a variable that's been declared but not yet initialized. This will not be the only time we see TDZ errors -- they crop up in several places in ES6. Also, note that "initialized" doesn't require explicitly assigning a value in your code, as `let b;` is totally valid. A variable that's not given an assignment at declaration time is assumed to have been assigned the `undefined` value, so `let b;` is the same as `let b = undefined;`. Explicit assignment or not, you cannot access `b` until the `let b` statement is run. One last gotcha: `typeof` behaves differently with TDZ variables than it does with undeclared (or declared!) variables. For example: ```js { // `a` is not declared if (typeof a === "undefined") { console.log( "cool" ); } // `b` is declared, but in its TDZ if (typeof b === "undefined") { // ReferenceError! // .. } // .. let b; } ``` The `a` is not declared, so `typeof` is the only safe way to check for its existence or not. But `typeof b` throws the TDZ error because farther down in the code there happens to be a `let b` declaration. Oops. Now it should be clearer why I insist that `let` declarations should all be at the top of their scope. That totally avoids the accidental errors of accessing too early. It also makes it more *explicit* when you look at the start of a block, any block, what variables it contains. Your blocks (`if` statements, `while` loops, etc.) don't have to share their original behavior with scoping behavior. This explicitness on your part, which is up to you to maintain with discipline, will save you lots of refactor headaches and footguns down the line. **Note:** For more information on `let` and block scoping, see Chapter 3 of the *Scope & Closures* title of this series. #### `let` + `for` The only exception I'd make to the preference for the *explicit* form of `let` declaration blocking is a `let` that appears in the header of a `for` loop. The reason may seem nuanced, but I believe it to be one of the more important ES6 features. Consider: ```js var funcs = []; for (let i = 0; i < 5; i++) { funcs.push( function(){ console.log( i ); } ); } funcs[3](); // 3 ``` The `let i` in the `for` header declares an `i` not just for the `for` loop itself, but it redeclares a new `i` for each iteration of the loop. That means that closures created inside the loop iteration close over those per-iteration variables the way you'd expect. If you tried that same snippet but with `var i` in the `for` loop header, you'd get `5` instead of `3`, because there'd only be one `i` in the outer scope that was closed over, instead of a new `i` for each iteration's function to close over. You could also have accomplished the same thing slightly more verbosely: ```js var funcs = []; for (var i = 0; i < 5; i++) { let j = i; funcs.push( function(){ console.log( j ); } ); } funcs[3](); // 3 ``` Here, we forcibly create a new `j` for each iteration, and then the closure works the same way. I prefer the former approach; that extra special capability is why I endorse the `for (let .. ) ..` form. It could be argued it's somewhat more *implicit*, but it's *explicit* enough, and useful enough, for my tastes. `let` also works the same way with `for..in` and `for..of` loops (see "`for..of` Loops"). ### `const` Declarations There's one other form of block-scoped declaration to consider: the `const`, which creates *constants*. What exactly is a constant? It's a variable that's read-only after its initial value is set. Consider: ```js { const a = 2; console.log( a ); // 2 a = 3; // TypeError! } ``` You are not allowed to change the value the variable holds once it's been set, at declaration time. A `const` declaration must have an explicit initialization. If you wanted a *constant* with the `undefined` value, you'd have to declare `const a = undefined` to get it. Constants are not a restriction on the value itself, but on the variable's assignment of that value. In other words, the value is not frozen or immutable because of `const`, just the assignment of it. If the value is complex, such as an object or array, the contents of the value can still be modified: ```js { const a = [1,2,3]; a.push( 4 ); console.log( a ); // [1,2,3,4] a = 42; // TypeError! } ``` The `a` variable doesn't actually hold a constant array; rather, it holds a constant reference to the array. The array itself is freely mutable. **Warning:** Assigning an object or array as a constant means that value will not be able to be garbage collected until that constant's lexical scope goes away, as the reference to the value can never be unset. That may be desirable, but be careful if it's not your intent! Essentially, `const` declarations enforce what we've stylistically signaled with our code for years, where we declared a variable name of all uppercase letters and assigned it some literal value that we took care never to change. There's no enforcement on a `var` assignment, but there is now with a `const` assignment, which can help you catch unintended changes. `const` *can* be used with variable declarations of `for`, `for..in`, and `for..of` loops (see "`for..of` Loops"). However, an error will be thrown if there's any attempt to reassign, such as the typical `i++` clause of a `for` loop. #### `const` Or Not There's some rumored assumptions that a `const` could be more optimizable by the JS engine in certain scenarios than a `let` or `var` would be. Theoretically, the engine more easily knows the variable's value/type will never change, so it can eliminate some possible tracking. Whether `const` really helps here or this is just our own fantasies and intuitions, the much more important decision to make is if you intend constant behavior or not. Remember: one of the most important roles for source code is to communicate clearly, not only to you, but your future self and other code collaborators, what your intent is. Some developers prefer to start out every variable declaration as a `const` and then relax a declaration back to a `let` if it becomes necessary for its value to change in the code. This is an interesting perspective, but it's not clear that it genuinely improves the readability or reason-ability of code. It's not really a *protection*, as many believe, because any later developer who wants to change a value of a `const` can just blindly change `const` to `let` on the declaration. At best, it protects accidental change. But again, other than our intuitions and sensibilities, there doesn't appear to be objective and clear measure of what constitutes "accidents" or prevention thereof. Similar mindsets exist around type enforcement. My advice: to avoid potentially confusing code, only use `const` for variables that you're intentionally and obviously signaling will not change. In other words, don't *rely on* `const` for code behavior, but instead use it as a tool for signaling intent, when intent can be signaled clearly. ### Block-scoped Functions Starting with ES6, function declarations that occur inside of blocks are now specified to be scoped to that block. Prior to ES6, the specification did not call for this, but many implementations did it anyway. So now the specification meets reality. Consider: ```js { foo(); // works! function foo() { // .. } } foo(); // ReferenceError ``` The `foo()` function is declared inside the `{ .. }` block, and as of ES6 is block-scoped there. So it's not available outside that block. But also note that it is "hoisted" within the block, as opposed to `let` declarations, which suffer the TDZ error trap mentioned earlier. Block-scoping of function declarations could be a problem if you've ever written code like this before, and relied on the old legacy non-block-scoped behavior: ```js if (something) { function foo() { console.log( "1" ); } } else { function foo() { console.log( "2" ); } } foo(); // ?? ``` In pre-ES6 environments, `foo()` would print `"2"` regardless of the value of `something`, because both function declarations were hoisted out of the blocks, and the second one always wins. In ES6, that last line throws a `ReferenceError`. ## Spread/Rest ES6 introduces a new `...` operator that's typically referred to as the *spread* or *rest* operator, depending on where/how it's used. Let's take a look: ```js function foo(x,y,z) { console.log( x, y, z ); } foo( ...[1,2,3] ); // 1 2 3 ``` When `...` is used in front of an array (actually, any *iterable*, which we cover in Chapter 3), it acts to "spread" it out into its individual values. You'll typically see that usage as is shown in that previous snippet, when spreading out an array as a set of arguments to a function call. In this usage, `...` acts to give us a simpler syntactic replacement for the `apply(..)` method, which we would typically have used pre-ES6 as: ```js foo.apply( null, [1,2,3] ); // 1 2 3 ``` But `...` can be used to spread out/expand a value in other contexts as well, such as inside another array declaration: ```js var a = [2,3,4]; var b = [ 1, ...a, 5 ]; console.log( b ); // [1,2,3,4,5] ``` In this usage, `...` is basically replacing `concat(..)`, as it behaves like `[1].concat( a, [5] )` here. The other common usage of `...` can be seen as essentially the opposite; instead of spreading a value out, the `...` *gathers* a set of values together into an array. Consider: ```js function foo(x, y, ...z) { console.log( x, y, z ); } foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5] ``` The `...z` in this snippet is essentially saying: "gather the *rest* of the arguments (if any) into an array called `z`." Because `x` was assigned `1`, and `y` was assigned `2`, the rest of the arguments `3`, `4`, and `5` were gathered into `z`. Of course, if you don't have any named parameters, the `...` gathers all arguments: ```js function foo(...args) { console.log( args ); } foo( 1, 2, 3, 4, 5); // [1,2,3,4,5] ``` **Note:** The `...args` in the `foo(..)` function declaration is usually called "rest parameters," because you're collecting the rest of the parameters. I prefer "gather," because it's more descriptive of what it does rather than what it contains. The best part about this usage is that it provides a very solid alternative to using the long-since-deprecated `arguments` array -- actually, it's not really an array, but an array-like object. Because `args` (or whatever you call it -- a lot of people prefer `r` or `rest`) is a real array, we can get rid of lots of silly pre-ES6 tricks we jumped through to make `arguments` into something we can treat as an array. Consider: ```js // doing things the new ES6 way function foo(...args) { // `args` is already a real array // discard first element in `args` args.shift(); // pass along all of `args` as arguments // to `console.log(..)` console.log( ...args ); } // doing things the old-school pre-ES6 way function bar() { // turn `arguments` into a real array var args = Array.prototype.slice.call( arguments ); // add some elements on the end args.push( 4, 5 ); // filter out odd numbers args = args.filter( function(v){ return v % 2 == 0; } ); // pass along all of `args` as arguments // to `foo(..)` foo.apply( null, args ); } bar( 0, 1, 2, 3 ); // 2 4 ``` The `...args` in the `foo(..)` function declaration gathers arguments, and the `...args` in the `console.log(..)` call spreads them out. That's a good illustration of the symmetric but opposite uses of the `...` operator. Besides the `...` usage in a function declaration, there's another case where `...` is used for gathering values, and we'll look at it in the "Too Many, Too Few, Just Enough" section later in this chapter. ## Default Parameter Values Perhaps one of the most common idioms in JavaScript relates to setting a default value for a function parameter. The way we've done this for years should look quite familiar: ```js function foo(x,y) { x = x || 11; y = y || 31; console.log( x + y ); } foo(); // 42 foo( 5, 6 ); // 11 foo( 5 ); // 36 foo( null, 6 ); // 17 ``` Of course, if you've used this pattern before, you know that it's both helpful and a little bit dangerous, if for example you need to be able to pass in what would otherwise be considered a falsy value for one of the parameters. Consider: ```js foo( 0, 42 ); // 53 <-- Oops, not 42 ``` Why? Because the `0` is falsy, and so the `x || 11` results in `11`, not the directly passed in `0`. To fix this gotcha, some people will instead write the check more verbosely like this: ```js function foo(x,y) { x = (x !== undefined) ? x : 11; y = (y !== undefined) ? y : 31; console.log( x + y ); } foo( 0, 42 ); // 42 foo( undefined, 6 ); // 17 ``` Of course, that means that any value except `undefined` can be directly passed in. However, `undefined` will be assumed to signal, "I didn't pass this in." That works great unless you actually need to be able to pass `undefined` in. In that case, you could test to see if the argument is actually omitted, by it actually not being present in the `arguments` array, perhaps like this: ```js function foo(x,y) { x = (0 in arguments) ? x : 11; y = (1 in arguments) ? y : 31; console.log( x + y ); } foo( 5 ); // 36 foo( 5, undefined ); // NaN ``` But how would you omit the first `x` argument without the ability to pass in any kind of value (not even `undefined`) that signals "I'm omitting this argument"? `foo(,5)` is tempting, but it's invalid syntax. `foo.apply(null,[,5])` seems like it should do the trick, but `apply(..)`'s quirks here mean that the arguments are treated as `[undefined,5]`, which of course doesn't omit. If you investigate further, you'll find you can only omit arguments on the end (i.e., righthand side) by simply passing fewer arguments than "expected," but you cannot omit arguments in the middle or at the beginning of the arguments list. It's just not possible. There's a principle applied to JavaScript's design here that is important to remember: `undefined` means *missing*. That is, there's no difference between `undefined` and *missing*, at least as far as function arguments go. **Note:** There are, confusingly, other places in JS where this particular design principle doesn't apply, such as for arrays with empty slots. See the *Types & Grammar* title of this series for more information. With all this in mind, we can now examine a nice helpful syntax added as of ES6 to streamline the assignment of default values to missing arguments: ```js function foo(x = 11, y = 31) { console.log( x + y ); } foo(); // 42 foo( 5, 6 ); // 11 foo( 0, 42 ); // 42 foo( 5 ); // 36 foo( 5, undefined ); // 36 <-- `undefined` is missing foo( 5, null ); // 5 <-- null coerces to `0` foo( undefined, 6 ); // 17 <-- `undefined` is missing foo( null, 6 ); // 6 <-- null coerces to `0` ``` Notice the results and how they imply both subtle differences and similarities to the earlier approaches. `x = 11` in a function declaration is more like `x !== undefined ? x : 11` than the much more common idiom `x || 11`, so you'll need to be careful in converting your pre-ES6 code to this ES6 default parameter value syntax. **Note:** A rest/gather parameter (see "Spread/Rest") cannot have a default value. So, while `function foo(...vals=[1,2,3]) {` might seem an intriguing capability, it's not valid syntax. You'll need to continue to apply that sort of logic manually if necessary. ### Default Value Expressions Function default values can be more than just simple values like `31`; they can be any valid expression, even a function call: ```js function bar(val) { console.log( "bar called!" ); return y + val; } function foo(x = y + 3, z = bar( x )) { console.log( x, z ); } var y = 5; foo(); // "bar called" // 8 13 foo( 10 ); // "bar called" // 10 15 y = 6; foo( undefined, 10 ); // 9 10 ``` As you can see, the default value expressions are lazily evaluated, meaning they're only run if and when they're needed -- that is, when a parameter's argument is omitted or is `undefined`. It's a subtle detail, but the formal parameters in a function declaration are in their own scope (think of it as a scope bubble wrapped around just the `( .. )` of the function declaration), not in the function body's scope. That means a reference to an identifier in a default value expression first matches the formal parameters' scope before looking to an outer scope. See the *Scope & Closures* title of this series for more information. Consider: ```js var w = 1, z = 2; function foo( x = w + 1, y = x + 1, z = z + 1 ) { console.log( x, y, z ); } foo(); // ReferenceError ``` The `w` in the `w + 1` default value expression looks for `w` in the formal parameters' scope, but does not find it, so the outer scope's `w` is used. Next, The `x` in the `x + 1` default value expression finds `x` in the formal parameters' scope, and luckily `x` has already been initialized, so the assignment to `y` works fine. However, the `z` in `z + 1` finds `z` as a not-yet-initialized-at-that-moment parameter variable, so it never tries to find the `z` from the outer scope. As we mentioned in the "`let` Declarations" section earlier in this chapter, ES6 has a TDZ, which prevents a variable from being accessed in its uninitialized state. As such, the `z + 1` default value expression throws a TDZ `ReferenceError` error. Though it's not necessarily a good idea for code clarity, a default value expression can even be an inline function expression call -- commonly referred to as an immediately invoked function expression (IIFE): ```js function foo( x = (function(v){ return v + 11; })( 31 ) ) { console.log( x ); } foo(); // 42 ``` There will very rarely be any cases where an IIFE (or any other executed inline function expression) will be appropriate for default value expressions. If you find yourself tempted to do this, take a step back and reevaluate! **Warning:** If the IIFE had tried to access the `x` identifier and had not declared its own `x`, this would also have been a TDZ error, just as discussed before. The default value expression in the previous snippet is an IIFE in that in the sense that it's a function that's executed right inline, via `(31)`. If we had left that part off, the default value assigned to `x` would have just been a function reference itself, perhaps like a default callback. There will probably be cases where that pattern will be quite useful, such as: ```js function ajax(url, cb = function(){}) { // .. } ajax( "http://some.url.1" ); ``` In this case, we essentially want to default `cb` to be a no-op empty function call if not otherwise specified. The function expression is just a function reference, not a function call itself (no invoking `()` on the end of it), which accomplishes that goal. Since the early days of JS, there's been a little-known but useful quirk available to us: `Function.prototype` is itself an empty no-op function. So, the declaration could have been `cb = Function.prototype` and saved the inline function expression creation. ## Destructuring ES6 introduces a new syntactic feature called *destructuring*, which may be a little less confusing if you instead think of it as *structured assignment*. To understand this meaning, consider: ```js function foo() { return [1,2,3]; } var tmp = foo(), a = tmp[0], b = tmp[1], c = tmp[2]; console.log( a, b, c ); // 1 2 3 ``` As you can see, we created a manual assignment of the values in the array that `foo()` returns to individual variables `a`, `b`, and `c`, and to do so we (unfortunately) needed the `tmp` variable. Similarly, we can do the following with objects: ```js function bar() { return { x: 4, y: 5, z: 6 }; } var tmp = bar(), x = tmp.x, y = tmp.y, z = tmp.z; console.log( x, y, z ); // 4 5 6 ``` The `tmp.x` property value is assigned to the `x` variable, and likewise for `tmp.y` to `y` and `tmp.z` to `z`. Manually assigning indexed values from an array or properties from an object can be thought of as *structured assignment*. ES6 adds a dedicated syntax for *destructuring*, specifically *array destructuring* and *object destructuring*. This syntax eliminates the need for the `tmp` variable in the previous snippets, making them much cleaner. Consider: ```js var [ a, b, c ] = foo(); var { x: x, y: y, z: z } = bar(); console.log( a, b, c ); // 1 2 3 console.log( x, y, z ); // 4 5 6 ``` You're likely more accustomed to seeing syntax like `[a,b,c]` on the righthand side of an `=` assignment, as the value being assigned. Destructuring symmetrically flips that pattern, so that `[a,b,c]` on the lefthand side of the `=` assignment is treated as a kind of "pattern" for decomposing the righthand side array value into separate variable assignments. Similarly, `{ x: x, y: y, z: z }` specifies a "pattern" to decompose the object value from `bar()` into separate variable assignments. ### Object Property Assignment Pattern Let's dig into that `{ x: x, .. }` syntax from the previous snippet. If the property name being matched is the same as the variable you want to declare, you can actually shorten the syntax: ```js var { x, y, z } = bar(); console.log( x, y, z ); // 4 5 6 ``` Pretty cool, right? But is `{ x, .. }` leaving off the `x: ` part or leaving off the `: x` part? We're actually leaving off the `x: ` part when we use the shorter syntax. That may not seem like an important detail, but you'll understand its importance in just a moment. If you can write the shorter form, why would you ever write out the longer form? Because that longer form actually allows you to assign a property to a different variable name, which can sometimes be quite useful: ```js var { x: bam, y: baz, z: bap } = bar(); console.log( bam, baz, bap ); // 4 5 6 console.log( x, y, z ); // ReferenceError ``` There's a subtle but super-important quirk to understand about this variation of the object destructuring form. To illustrate why it can be a gotcha you need to be careful of, let's consider the "pattern" of how normal object literals are specified: ```js var X = 10, Y = 20; var o = { a: X, b: Y }; console.log( o.a, o.b ); // 10 20 ``` In `{ a: X, b: Y }`, we know that `a` is the object property, and `X` is the source value that gets assigned to it. In other words, the syntactic pattern is `target: source`, or more obviously, `property-alias: value`. We intuitively understand this because it's the same as `=` assignment, where the pattern is `target = source`. However, when you use object destructuring assignment -- that is, putting the `{ .. }` object literal-looking syntax on the lefthand side of the `=` operator -- you invert that `target: source` pattern. Recall: ```js var { x: bam, y: baz, z: bap } = bar(); ``` The syntactic pattern here is `source: target` (or `value: variable-alias`). `x: bam` means the `x` property is the source value and `bam` is the target variable to assign to. In other words, object literals are `target <-- source`, and object destructuring assignments are `source --> target`. See how that's flipped? There's another way to think about this syntax though, which may help ease the confusion. Consider: ```js var aa = 10, bb = 20; var o = { x: aa, y: bb }; var { x: AA, y: BB } = o; console.log( AA, BB ); // 10 20 ``` In the `{ x: aa, y: bb }` line, the `x` and `y` represent the object properties. In the `{ x: AA, y: BB }` line, the `x` and the `y` *also* represent the object properties. Recall how earlier I asserted that `{ x, .. }` was leaving off the `x: ` part? In those two lines, if you erase the `x: ` and `y: ` parts in that snippet, you're left only with `aa, bb` and `AA, BB`, which in effect -- only conceptually, not actually -- are assignments from `aa` to `AA` and from `bb` to `BB`. So, that symmetry may help to explain why the syntactic pattern was intentionally flipped for this ES6 feature. **Note:** I would have preferred the syntax to be `{ AA: x , BB: y }` for the destructuring assignment, as that would have preserved consistency of the more familiar `target: source` pattern for both usages. Alas, I'm having to train my brain for the inversion, as some readers may also have to do. ### Not Just Declarations So far, we've used destructuring assignment with `var` declarations (of course, they could also use `let` and `const`), but destructuring is a general assignment operation, not just a declaration. Consider: ```js var a, b, c, x, y, z; [a,b,c] = foo(); ( { x, y, z } = bar() ); console.log( a, b, c ); // 1 2 3 console.log( x, y, z ); // 4 5 6 ``` The variables can already be declared, and then the destructuring only does assignments, exactly as we've already seen. **Note:** For the object destructuring form specifically, when leaving off a `var`/`let`/`const` declarator, we had to surround the whole assignment expression in `( )`, because otherwise the `{ .. }` on the lefthand side as the first element in the statement is taken to be a block statement instead of an object. In fact, the assignment expressions (`a`, `y`, etc.) don't actually need to be just variable identifiers. Anything that's a valid assignment expression is allowed. For example: ```js var o = {}; [o.a, o.b, o.c] = foo(); ( { x: o.x, y: o.y, z: o.z } = bar() ); console.log( o.a, o.b, o.c ); // 1 2 3 console.log( o.x, o.y, o.z ); // 4 5 6 ``` You can even use computed property expressions in the destructuring. Consider: ```js var which = "x", o = {}; ( { [which]: o[which] } = bar() ); console.log( o.x ); // 4 ``` The `[which]:` part is the computed property, which results in `x` -- the property to destructure from the object in question as the source of the assignment. The `o[which]` part is just a normal object key reference, which equates to `o.x` as the target of the assignment. You can use the general assignments to create object mappings/transformations, such as: ```js var o1 = { a: 1, b: 2, c: 3 }, o2 = {}; ( { a: o2.x, b: o2.y, c: o2.z } = o1 ); console.log( o2.x, o2.y, o2.z ); // 1 2 3 ``` Or you can map an object to an array, such as: ```js var o1 = { a: 1, b: 2, c: 3 }, a2 = []; ( { a: a2[0], b: a2[1], c: a2[2] } = o1 ); console.log( a2 ); // [1,2,3] ``` Or the other way around: ```js var a1 = [ 1, 2, 3 ], o2 = {}; [ o2.a, o2.b, o2.c ] = a1; console.log( o2.a, o2.b, o2.c ); // 1 2 3 ``` Or you could reorder one array to another: ```js var a1 = [ 1, 2, 3 ], a2 = []; [ a2[2], a2[0], a2[1] ] = a1; console.log( a2 ); // [2,3,1] ``` You can even solve the traditional "swap two variables" task without a temporary variable: ```js var x = 10, y = 20; [ y, x ] = [ x, y ]; console.log( x, y ); // 20 10 ``` **Warning:** Be careful: you shouldn't mix in declaration with assignment unless you want all of the assignment expressions *also* to be treated as declarations. Otherwise, you'll get syntax errors. That's why in the earlier example I had to do `var a2 = []` separately from the `[ a2[0], .. ] = ..` destructuring assignment. It wouldn't make any sense to try `var [ a2[0], .. ] = ..`, because `a2[0]` isn't a valid declaration identifier; it also obviously couldn't implicitly create a `var a2 = []` declaration to use. ### Repeated Assignments The object destructuring form allows a source property (holding any value type) to be listed multiple times. For example: ```js var { a: X, a: Y } = { a: 1 }; X; // 1 Y; // 1 ``` That also means you can both destructure a sub-object/array property and also capture the sub-object/array's value itself. Consider: ```js var { a: { x: X, x: Y }, a } = { a: { x: 1 } }; X; // 1 Y; // 1 a; // { x: 1 } ( { a: X, a: Y, a: [ Z ] } = { a: [ 1 ] } ); X.push( 2 ); Y[0] = 10; X; // [10,2] Y; // [10,2] Z; // 1 ``` A word of caution about destructuring: it may be tempting to list destructuring assignments all on a single line as has been done thus far in our discussion. However, it's a much better idea to spread destructuring assignment patterns over multiple lines, using proper indentation -- much like you would in JSON or with an object literal value -- for readability sake. ```js // harder to read: var { a: { b: [ c, d ], e: { f } }, g } = obj; // better: var { a: { b: [ c, d ], e: { f } }, g } = obj; ``` Remember: **the purpose of destructuring is not just less typing, but more declarative readability.** #### Destructuring Assignment Expressions The assignment expression with object or array destructuring has as its completion value the full righthand object/array value. Consider: ```js var o = { a:1, b:2, c:3 }, a, b, c, p; p = { a, b, c } = o; console.log( a, b, c ); // 1 2 3 p === o; // true ``` In the previous snippet, `p` was assigned the `o` object reference, not one of the `a`, `b`, or `c` values. The same is true of array destructuring: ```js var o = [1,2,3], a, b, c, p; p = [ a, b, c ] = o; console.log( a, b, c ); // 1 2 3 p === o; // true ``` By carrying the object/array value through as the completion, you can chain destructuring assignment expressions together: ```js var o = { a:1, b:2, c:3 }, p = [4,5,6], a, b, c, x, y, z; ( {a} = {b,c} = o ); [x,y] = [z] = p; console.log( a, b, c ); // 1 2 3 console.log( x, y, z ); // 4 5 4 ``` ### Too Many, Too Few, Just Enough With both array destructuring assignment and object destructuring assignment, you do not have to assign all the values that are present. For example: ```js var [,b] = foo(); var { x, z } = bar(); console.log( b, x, z ); // 2 4 6 ``` The `1` and `3` values that came back from `foo()` are discarded, as is the `5` value from `bar()`. Similarly, if you try to assign more values than are present in the value you're destructuring/decomposing, you get graceful fallback to `undefined`, as you'd expect: ```js var [,,c,d] = foo(); var { w, z } = bar(); console.log( c, z ); // 3 6 console.log( d, w ); // undefined undefined ``` This behavior follows symmetrically from the earlier stated "`undefined` is missing" principle. We examined the `...` operator earlier in this chapter, and saw that it can sometimes be used to spread an array value out into its separate values, and sometimes it can be used to do the opposite: to gather a set of values together into an array. In addition to the gather/rest usage in function declarations, `...` can perform the same behavior in destructuring assignments. To illustrate, let's recall a snippet from earlier in this chapter: ```js var a = [2,3,4]; var b = [ 1, ...a, 5 ]; console.log( b ); // [1,2,3,4,5] ``` Here we see that `...a` is spreading `a` out, because it appears in the array `[ .. ]` value position. If `...a` appears in an array destructuring position, it performs the gather behavior: ```js var a = [2,3,4]; var [ b, ...c ] = a; console.log( b, c ); // 2 [3,4] ``` The `var [ .. ] = a` destructuring assignment spreads `a` out to be assigned to the pattern described inside the `[ .. ]`. The first part names `b` for the first value in `a` (`2`). But then `...c` gathers the rest of the values (`3` and `4`) into an array and calls it `c`. **Note:** We've seen how `...` works with arrays, but what about with objects? It's not an ES6 feature, but see Chapter 8 for discussion of a possible "beyond ES6" feature where `...` works with spreading or gathering objects. ### Default Value Assignment Both forms of destructuring can offer a default value option for an assignment, using the `=` syntax similar to the default function argument values discussed earlier. Consider: ```js var [ a = 3, b = 6, c = 9, d = 12 ] = foo(); var { x = 5, y = 10, z = 15, w = 20 } = bar(); console.log( a, b, c, d ); // 1 2 3 12 console.log( x, y, z, w ); // 4 5 6 20 ``` You can combine the default value assignment with the alternative assignment expression syntax covered earlier. For example: ```js var { x, y, z, w: WW = 20 } = bar(); console.log( x, y, z, WW ); // 4 5 6 20 ``` Be careful about confusing yourself (or other developers who read your code) if you use an object or array as the default value in a destructuring. You can create some really hard to understand code: ```js var x = 200, y = 300, z = 100; var o1 = { x: { y: 42 }, z: { y: z } }; ( { y: x = { y: y } } = o1 ); ( { z: y = { y: z } } = o1 ); ( { x: z = { y: x } } = o1 ); ``` Can you tell from that snippet what values `x`, `y`, and `z` have at the end? Takes a moment of pondering, I would imagine. I'll end the suspense: ```js console.log( x.y, y.y, z.y ); // 300 100 42 ``` The takeaway here: destructuring is great and can be very useful, but it's also a sharp sword that can cause injury (to someone's brain) if used unwisely. ### Nested Destructuring If the values you're destructuring have nested objects or arrays, you can destructure those nested values as well: ```js var a1 = [ 1, [2, 3, 4], 5 ]; var o1 = { x: { y: { z: 6 } } }; var [ a, [ b, c, d ], e ] = a1; var { x: { y: { z: w } } } = o1; console.log( a, b, c, d, e ); // 1 2 3 4 5 console.log( w ); // 6 ``` Nested destructuring can be a simple way to flatten out object namespaces. For example: ```js var App = { model: { User: function(){ .. } } }; // instead of: // var User = App.model.User; var { model: { User } } = App; ``` ### Destructuring Parameters In the following snippet, can you spot the assignment? ```js function foo(x) { console.log( x ); } foo( 42 ); ``` The assignment is kinda hidden: `42` (the argument) is assigned to `x` (the parameter) when `foo(42)` is executed. If parameter/argument pairing is an assignment, then it stands to reason that it's an assignment that could be destructured, right? Of course! Consider array destructuring for parameters: ```js function foo( [ x, y ] ) { console.log( x, y ); } foo( [ 1, 2 ] ); // 1 2 foo( [ 1 ] ); // 1 undefined foo( [] ); // undefined undefined ``` Object destructuring for parameters works, too: ```js function foo( { x, y } ) { console.log( x, y ); } foo( { y: 1, x: 2 } ); // 2 1 foo( { y: 42 } ); // undefined 42 foo( {} ); // undefined undefined ``` This technique is an approximation of named arguments (a long requested feature for JS!), in that the properties on the object map to the destructured parameters of the same names. That also means that we get optional parameters (in any position) for free, as you can see leaving off the `x` "parameter" worked as we'd expect. Of course, all the previously discussed variations of destructuring are available to us with parameter destructuring, including nested destructuring, default values, and more. Destructuring also mixes fine with other ES6 function parameter capabilities, like default parameter values and rest/gather parameters. Consider these quick illustrations (certainly not exhaustive of the possible variations): ```js function f1([ x=2, y=3, z ]) { .. } function f2([ x, y, ...z], w) { .. } function f3([ x, y, ...z], ...w) { .. } function f4({ x: X, y }) { .. } function f5({ x: X = 10, y = 20 }) { .. } function f6({ x = 10 } = {}, { y } = { y: 10 }) { .. } ``` Let's take one example from this snippet and examine it, for illustration purposes: ```js function f3([ x, y, ...z], ...w) { console.log( x, y, z, w ); } f3( [] ); // undefined undefined [] [] f3( [1,2,3,4], 5, 6 ); // 1 2 [3,4] [5,6] ``` There are two `...` operators in use here, and they're both gathering values in arrays (`z` and `w`), though `...z` gathers from the rest of the values left over in the first array argument, while `...w` gathers from the rest of the main arguments left over after the first. #### Destructuring Defaults + Parameter Defaults There's one subtle point you should be particularly careful to notice -- the difference in behavior between a destructuring default value and a function parameter default value. For example: ```js function f6({ x = 10 } = {}, { y } = { y: 10 }) { console.log( x, y ); } f6(); // 10 10 ``` At first, it would seem that we've declared a default value of `10` for both the `x` and `y` parameters, but in two different ways. However, these two different approaches will behave differently in certain cases, and the difference is awfully subtle. Consider: ```js f6( {}, {} ); // 10 undefined ``` Wait, why did that happen? It's pretty clear that named parameter `x` is defaulting to `10` if not passed as a property of that same name in the first argument's object. But what about `y` being `undefined`? The `{ y: 10 }` value is an object as a function parameter default value, not a destructuring default value. As such, it only applies if the second argument is not passed at all, or is passed as `undefined`. In the previous snippet, we *are* passing a second argument (`{}`), so the default `{ y: 10 }` value is not used, and the `{ y }` destructuring occurs against the passed in `{}` empty object value. Now, compare `{ y } = { y: 10 }` to `{ x = 10 } = {}`. For the `x`'s form usage, if the first function argument is omitted or `undefined`, the `{}` empty object default applies. Then, whatever value is in the first argument position -- either the default `{}` or whatever you passed in -- is destructured with the `{ x = 10 }`, which checks to see if an `x` property is found, and if not found (or `undefined`), the `10` default value is applied to the `x` named parameter. Deep breath. Read back over those last few paragraphs a couple of times. Let's review via code: ```js function f6({ x = 10 } = {}, { y } = { y: 10 }) { console.log( x, y ); } f6(); // 10 10 f6( undefined, undefined ); // 10 10 f6( {}, undefined ); // 10 10 f6( {}, {} ); // 10 undefined f6( undefined, {} ); // 10 undefined f6( { x: 2 }, { y: 3 } ); // 2 3 ``` It would generally seem that the defaulting behavior of the `x` parameter is probably the more desirable and sensible case compared to that of `y`. As such, it's important to understand why and how `{ x = 10 } = {}` form is different from `{ y } = { y: 10 }` form. If that's still a bit fuzzy, go back and read it again, and play with this yourself. Your future self will thank you for taking the time to get this very subtle gotcha nuance detail straight. #### Nested Defaults: Destructured and Restructured Although it may at first be difficult to grasp, an interesting idiom emerges for setting defaults for a nested object's properties: using object destructuring along with what I'd call *restructuring*. Consider a set of defaults in a nested object structure, like the following: ```js // taken from: http://es-discourse.com/t/partial-default-arguments/120/7 var defaults = { options: { remove: true, enable: false, instance: {} }, log: { warn: true, error: true } }; ``` Now, let's say that you have an object called `config`, which has some of these applied, but perhaps not all, and you'd like to set all the defaults into this object in the missing spots, but not override specific settings already present: ```js var config = { options: { remove: false, instance: null } }; ``` You can of course do so manually, as you might have done in the past: ```js config.options = config.options || {}; config.options.remove = (config.options.remove !== undefined) ? config.options.remove : defaults.options.remove; config.options.enable = (config.options.enable !== undefined) ? config.options.enable : defaults.options.enable; ... ``` Yuck. Others may prefer the assign-overwrite approach to this task. You might be tempted by the ES6 `Object.assign(..)` utility (see Chapter 6) to clone the properties first from `defaults` and then overwritten with the cloned properties from `config`, as so: ```js config = Object.assign( {}, defaults, config ); ``` That looks way nicer, huh? But there's a major problem! `Object.assign(..)` is shallow, which means when it copies `defaults.options`, it just copies that object reference, not deep cloning that object's properties to a `config.options` object. `Object.assign(..)` would need to be applied (sort of "recursively") at all levels of your object's tree to get the deep cloning you're expecting. **Note:** Many JS utility libraries/frameworks provide their own option for deep cloning of an object, but those approaches and their gotchas are beyond our scope to discuss here. So let's examine if ES6 object destructuring with defaults can help at all: ```js config.options = config.options || {}; config.log = config.log || {}; ({ options: { remove: config.options.remove = defaults.options.remove, enable: config.options.enable = defaults.options.enable, instance: config.options.instance = defaults.options.instance } = {}, log: { warn: config.log.warn = defaults.log.warn, error: config.log.error = defaults.log.error } = {} } = config); ``` Not as nice as the false promise of `Object.assign(..)` (being that it's shallow only), but it's better than the manual approach by a fair bit, I think. It is still unfortunately verbose and repetitive, though. The previous snippet's approach works because I'm hacking the destructuring and defaults mechanism to do the property `=== undefined` checks and assignment decisions for me. It's a trick in that I'm destructuring `config` (see the `= config` at the end of the snippet), but I'm reassigning all the destructured values right back into `config`, with the `config.options.enable` assignment references. Still too much, though. Let's see if we can make anything better. The following trick works best if you know that all the various properties you're destructuring are uniquely named. You can still do it even if that's not the case, but it's not as nice -- you'll have to do the destructuring in stages, or create unique local variables as temporary aliases. If we fully destructure all the properties into top-level variables, we can then immediately restructure to reconstitute the original nested object structure. But all those temporary variables hanging around would pollute scope. So, let's use block scoping (see "Block-Scoped Declarations" earlier in this chapter) with a general `{ }` enclosing block: ```js // merge `defaults` into `config` { // destructure (with default value assignments) let { options: { remove = defaults.options.remove, enable = defaults.options.enable, instance = defaults.options.instance } = {}, log: { warn = defaults.log.warn, error = defaults.log.error } = {} } = config; // restructure config = { options: { remove, enable, instance }, log: { warn, error } }; } ``` That seems a fair bit nicer, huh? **Note:** You could also accomplish the scope enclosure with an arrow IIFE instead of the general `{ }` block and `let` declarations. Your destructuring assignments/defaults would be in the parameter list and your restructuring would be the `return` statement in the function body. The `{ warn, error }` syntax in the restructuring part may look new to you; that's called "concise properties" and we cover it in the next section! ## Object Literal Extensions ES6 adds a number of important convenience extensions to the humble `{ .. }` object literal. ### Concise Properties You're certainly familiar with declaring object literals in this form: ```js var x = 2, y = 3, o = { x: x, y: y }; ``` If it's always felt redundant to say `x: x` all over, there's good news. If you need to define a property that is the same name as a lexical identifier, you can shorten it from `x: x` to `x`. Consider: ```js var x = 2, y = 3, o = { x, y }; ``` ### Concise Methods In a similar spirit to concise properties we just examined, functions attached to properties in object literals also have a concise form, for convenience. The old way: ```js var o = { x: function(){ // .. }, y: function(){ // .. } } ``` And as of ES6: ```js var o = { x() { // .. }, y() { // .. } } ``` **Warning:** While `x() { .. }` seems to just be shorthand for `x: function(){ .. }`, concise methods have special behaviors that their older counterparts don't; specifically, the allowance for `super` (see "Object `super`" later in this chapter). Generators (see Chapter 4) also have a concise method form: ```js var o = { *foo() { .. } }; ``` #### Concisely Unnamed While that convenience shorthand is quite attractive, there's a subtle gotcha to be aware of. To illustrate, let's examine pre-ES6 code like the following, which you might try to refactor to use concise methods: ```js function runSomething(o) { var x = Math.random(), y = Math.random(); return o.something( x, y ); } runSomething( { something: function something(x,y) { if (x > y) { // recursively call with `x` // and `y` swapped return something( y, x ); } return y - x; } } ); ``` This obviously silly code just generates two random numbers and subtracts the smaller from the bigger. But what's important here isn't what it does, but rather how it's defined. Let's focus on the object literal and function definition, as we see here: ```js runSomething( { something: function something(x,y) { // .. } } ); ``` Why do we say both `something:` and `function something`? Isn't that redundant? Actually, no, both are needed for different purposes. The property `something` is how we can call `o.something(..)`, sort of like its public name. But the second `something` is a lexical name to refer to the function from inside itself, for recursion purposes. Can you see why the line `return something(y,x)` needs the name `something` to refer to the function? There's no lexical name for the object, such that it could have said `return o.something(y,x)` or something of that sort. That's actually a pretty common practice when the object literal does have an identifying name, such as: ```js var controller = { makeRequest: function(..){ // .. controller.makeRequest(..); } }; ``` Is this a good idea? Perhaps, perhaps not. You're assuming that the name `controller` will always point to the object in question. But it very well may not -- the `makeRequest(..)` function doesn't control the outer code and so can't force that to be the case. This could come back to bite you. Others prefer to use `this` to define such things: ```js var controller = { makeRequest: function(..){ // .. this.makeRequest(..); } }; ``` That looks fine, and should work if you always invoke the method as `controller.makeRequest(..)`. But you now have a `this` binding gotcha if you do something like: ```js btn.addEventListener( "click", controller.makeRequest, false ); ``` Of course, you can solve that by passing `controller.makeRequest.bind(controller)` as the handler reference to bind the event to. But yuck -- it isn't very appealing. Or what if your inner `this.makeRequest(..)` call needs to be made from a nested function? You'll have another `this` binding hazard, which people will often solve with the hacky `var self = this`, such as: ```js var controller = { makeRequest: function(..){ var self = this; btn.addEventListener( "click", function(){ // .. self.makeRequest(..); }, false ); } }; ``` More yuck. **Note:** For more information on `this` binding rules and gotchas, see Chapters 1-2 of the *this & Object Prototypes* title of this series. OK, what does all this have to do with concise methods? Recall our `something(..)` method definition: ```js runSomething( { something: function something(x,y) { // .. } } ); ``` The second `something` here provides a super convenient lexical identifier that will always point to the function itself, giving us the perfect reference for recursion, event binding/unbinding, and so on -- no messing around with `this` or trying to use an untrustable object reference. Great! So, now we try to refactor that function reference to this ES6 concise method form: ```js runSomething( { something(x,y) { if (x > y) { return something( y, x ); } return y - x; } } ); ``` Seems fine at first glance, except this code will break. The `return something(..)` call will not find a `something` identifier, so you'll get a `ReferenceError`. Oops. But why? The above ES6 snippet is interpreted as meaning: ```js runSomething( { something: function(x,y){ if (x > y) { return something( y, x ); } return y - x; } } ); ``` Look closely. Do you see the problem? The concise method definition implies `something: function(x,y)`. See how the second `something` we were relying on has been omitted? In other words, concise methods imply anonymous function expressions. Yeah, yuck. **Note:** You may be tempted to think that `=>` arrow functions are a good solution here, but they're equally insufficient, as they're also anonymous function expressions. We'll cover them in "Arrow Functions" later in this chapter. The partially redeeming news is that our `something(x,y)` concise method won't be totally anonymous. See "Function Names" in Chapter 7 for information about ES6 function name inference rules. That won't help us for our recursion, but it helps with debugging at least. So what are we left to conclude about concise methods? They're short and sweet, and a nice convenience. But you should only use them if you're never going to need them to do recursion or event binding/unbinding. Otherwise, stick to your old-school `something: function something(..)` method definitions. A lot of your methods are probably going to benefit from concise method definitions, so that's great news! Just be careful of the few where there's an un-naming hazard. #### ES5 Getter/Setter Technically, ES5 defined getter/setter literals forms, but they didn't seem to get used much, mostly due to the lack of transpilers to handle that new syntax (the only major new syntax added in ES5, really). So while it's not a new ES6 feature, we'll briefly refresh on that form, as it's probably going to be much more useful with ES6 going forward. Consider: ```js var o = { __id: 10, get id() { return this.__id++; }, set id(v) { this.__id = v; } } o.id; // 10 o.id; // 11 o.id = 20; o.id; // 20 // and: o.__id; // 21 o.__id; // 21 -- still! ``` These getter and setter literal forms are also present in classes; see Chapter 3. **Warning:** It may not be obvious, but the setter literal must have exactly one declared parameter; omitting it or listing others is illegal syntax. The single required parameter *can* use destructuring and defaults (e.g., `set id({ id: v = 0 }) { .. }`), but the gather/rest `...` is not allowed (`set id(...v) { .. }`). ### Computed Property Names You've probably been in a situation like the following snippet, where you have one or more property names that come from some sort of expression and thus can't be put into the object literal: ```js var prefix = "user_"; var o = { baz: function(..){ .. } }; o[ prefix + "foo" ] = function(..){ .. }; o[ prefix + "bar" ] = function(..){ .. }; .. ``` ES6 adds a syntax to the object literal definition which allows you to specify an expression that should be computed, whose result is the property name assigned. Consider: ```js var prefix = "user_"; var o = { baz: function(..){ .. }, [ prefix + "foo" ]: function(..){ .. }, [ prefix + "bar" ]: function(..){ .. } .. }; ``` Any valid expression can appear inside the `[ .. ]` that sits in the property name position of the object literal definition. Probably the most common use of computed property names will be with `Symbol`s (which we cover in "Symbols" later in this chapter), such as: ```js var o = { [Symbol.toStringTag]: "really cool thing", .. }; ``` `Symbol.toStringTag` is a special built-in value, which we evaluate with the `[ .. ]` syntax, so we can assign the `"really cool thing"` value to the special property name. Computed property names can also appear as the name of a concise method or a concise generator: ```js var o = { ["f" + "oo"]() { .. } // computed concise method *["b" + "ar"]() { .. } // computed concise generator }; ``` ### Setting `[[Prototype]]` We won't cover prototypes in detail here, so for more information, see the *this & Object Prototypes* title of this series. Sometimes it will be helpful to assign the `[[Prototype]]` of an object at the same time you're declaring its object literal. The following has been a nonstandard extension in many JS engines for a while, but is standardized as of ES6: ```js var o1 = { // .. }; var o2 = { __proto__: o1, // .. }; ``` `o2` is declared with a normal object literal, but it's also `[[Prototype]]`-linked to `o1`. The `__proto__` property name here can also be a string `"__proto__"`, but note that it *cannot* be the result of a computed property name (see the previous section). `__proto__` is controversial, to say the least. It's a decades-old proprietary extension to JS that is finally standardized, somewhat begrudgingly it seems, in ES6. Many developers feel it shouldn't ever be used. In fact, it's in "Annex B" of ES6, which is the section that lists things JS feels it has to standardize for compatibility reasons only. **Warning:** Though I'm narrowly endorsing `__proto__` as a key in an object literal definition, I definitely do not endorse using it in its object property form, like `o.__proto__`. That form is both a getter and setter (again for compatibility reasons), but there are definitely better options. See the *this & Object Prototypes* title of this series for more information. For setting the `[[Prototype]]` of an existing object, you can use the ES6 utility `Object.setPrototypeOf(..)`. Consider: ```js var o1 = { // .. }; var o2 = { // .. }; Object.setPrototypeOf( o2, o1 ); ``` **Note:** We'll discuss `Object` again in Chapter 6. "`Object.setPrototypeOf(..)` Static Function" provides additional details on `Object.setPrototypeOf(..)`. Also see "`Object.assign(..)` Static Function" for another form that relates `o2` prototypically to `o1`. ### Object `super` `super` is typically thought of as being only related to classes. However, due to JS's classless-objects-with-prototypes nature, `super` is equally effective, and nearly the same in behavior, with plain objects' concise methods. Consider: ```js var o1 = { foo() { console.log( "o1:foo" ); } }; var o2 = { foo() { super.foo(); console.log( "o2:foo" ); } }; Object.setPrototypeOf( o2, o1 ); o2.foo(); // o1:foo // o2:foo ``` **Warning:** `super` is only allowed in concise methods, not regular function expression properties. It also is only allowed in `super.XXX` form (for property/method access), not in `super()` form. The `super` reference in the `o2.foo()` method is locked statically to `o2`, and specifically to the `[[Prototype]]` of `o2`. `super` here would basically be `Object.getPrototypeOf(o2)` -- resolves to `o1` of course -- which is how it finds and calls `o1.foo()`. For complete details on `super`, see "Classes" in Chapter 3. ## Template Literals At the very outset of this section, I'm going to have to call out the name of this ES6 feature as being awfully... misleading, depending on your experiences with what the word *template* means. Many developers think of templates as being reusable renderable pieces of text, such as the capability provided by most template engines (Mustache, Handlebars, etc.). ES6's use of the word *template* would imply something similar, like a way to declare inline template literals that can be re-rendered. However, that's not at all the right way to think about this feature. So, before we go on, I'm renaming to what it should have been called: *interpolated string literals* (or *interpoliterals* for short). You're already well aware of declaring string literals with `"` or `'` delimiters, and you also know that these are not *smart strings* (as some languages have), where the contents would be parsed for interpolation expressions. However, ES6 introduces a new type of string literal, using the `` ` `` backtick as the delimiter. These string literals allow basic string interpolation expressions to be embedded, which are then automatically parsed and evaluated. Here's the old pre-ES6 way: ```js var name = "Kyle"; var greeting = "Hello " + name + "!"; console.log( greeting ); // "Hello Kyle!" console.log( typeof greeting ); // "string" ``` Now, consider the new ES6 way: ```js var name = "Kyle"; var greeting = `Hello ${name}!`; console.log( greeting ); // "Hello Kyle!" console.log( typeof greeting ); // "string" ``` As you can see, we used the `` `..` `` around a series of characters, which are interpreted as a string literal, but any expressions of the form `${..}` are parsed and evaluated inline immediately. The fancy term for such parsing and evaluating is *interpolation* (much more accurate than templating). The result of the interpolated string literal expression is just a plain old normal string, assigned to the `greeting` variable. **Warning:** `typeof greeting == "string"` illustrates why it's important not to think of these entities as special template values, as you cannot assign the unevaluated form of the literal to something and reuse it. The `` `..` `` string literal is more like an IIFE in the sense that it's automatically evaluated inline. The result of a `` `..` `` string literal is, simply, just a string. One really nice benefit of interpolated string literals is they are allowed to split across multiple lines: ```js var text = `Now is the time for all good men to come to the aid of their country!`; console.log( text ); // Now is the time for all good men // to come to the aid of their // country! ``` The line breaks (newlines) in the interpolated string literal were preserved in the string value. Unless appearing as explicit escape sequences in the literal value, the value of the `\r` carriage return character (code point `U+000D`) or the value of the `\r\n` carriage return + line feed sequence (code points `U+000D` and `U+000A`) are both normalized to a `\n` line feed character (code point `U+000A`). Don't worry though; this normalization is rare and would likely only happen if copy-pasting text into your JS file. ### Interpolated Expressions Any valid expression is allowed to appear inside `${..}` in an interpolated string literal, including function calls, inline function expression calls, and even other interpolated string literals! Consider: ```js function upper(s) { return s.toUpperCase(); } var who = "reader"; var text = `A very ${upper( "warm" )} welcome to all of you ${upper( `${who}s` )}!`; console.log( text ); // A very WARM welcome // to all of you READERS! ``` Here, the inner `` `${who}s` `` interpolated string literal was a little bit nicer convenience for us when combining the `who` variable with the `"s"` string, as opposed to `who + "s"`. There will be cases that nesting interpolated string literals is helpful, but be wary if you find yourself doing that kind of thing often, or if you find yourself nesting several levels deep. If that's the case, the odds are good that your string value production could benefit from some abstractions. **Warning:** As a word of caution, be very careful about the readability of your code with such new found power. Just like with default value expressions and destructuring assignment expressions, just because you *can* do something doesn't mean you *should* do it. Never go so overboard with new ES6 tricks that your code becomes more clever than you or your other team members. #### Expression Scope One quick note about the scope that is used to resolve variables in expressions. I mentioned earlier that an interpolated string literal is kind of like an IIFE, and it turns out thinking about it like that explains the scoping behavior as well. Consider: ```js function foo(str) { var name = "foo"; console.log( str ); } function bar() { var name = "bar"; foo( `Hello from ${name}!` ); } var name = "global"; bar(); // "Hello from bar!" ``` At the moment the `` `..` `` string literal is expressed, inside the `bar()` function, the scope available to it finds `bar()`'s `name` variable with value `"bar"`. Neither the global `name` nor `foo(..)`'s `name` matter. In other words, an interpolated string literal is just lexically scoped where it appears, not dynamically scoped in any way. ### Tagged Template Literals Again, renaming the feature for sanity sake: *tagged string literals*. To be honest, this is one of the cooler tricks that ES6 offers. It may seem a little strange, and perhaps not all that generally practical at first. But once you've spent some time with it, tagged string literals may just surprise you in their usefulness. For example: ```js function foo(strings, ...values) { console.log( strings ); console.log( values ); } var desc = "awesome"; foo`Everything is ${desc}!`; // [ "Everything is ", "!"] // [ "awesome" ] ``` Let's take a moment to consider what's happening in the previous snippet. First, the most jarring thing that jumps out is ``foo`Everything...`;``. That doesn't look like anything we've seen before. What is it? It's essentially a special kind of function call that doesn't need the `( .. )`. The *tag* -- the `foo` part before the `` `..` `` string literal -- is a function value that should be called. Actually, it can be any expression that results in a function, even a function call that returns another function, like: ```js function bar() { return function foo(strings, ...values) { console.log( strings ); console.log( values ); } } var desc = "awesome"; bar()`Everything is ${desc}!`; // [ "Everything is ", "!"] // [ "awesome" ] ``` But what gets passed to the `foo(..)` function when invoked as a tag for a string literal? The first argument -- we called it `strings` -- is an array of all the plain strings (the stuff between any interpolated expressions). We get two values in the `strings` array: `"Everything is "` and `"!"`. For convenience sake in our example, we then gather up all subsequent arguments into an array called `values` using the `...` gather/rest operator (see the "Spread/Rest" section earlier in this chapter), though you could of course have left them as individual named parameters following the `strings` parameter. The argument(s) gathered into our `values` array are the results of the already-evaluated interpolation expressions found in the string literal. So obviously the only element in `values` in our example is `"awesome"`. You can think of these two arrays as: the values in `values` are the separators if you were to splice them in between the values in `strings`, and then if you joined everything together, you'd get the complete interpolated string value. A tagged string literal is like a processing step after the interpolation expressions are evaluated but before the final string value is compiled, allowing you more control over generating the string from the literal. Typically, the string literal tag function (`foo(..)` in the previous snippets) should compute an appropriate string value and return it, so that you can use the tagged string literal as a value just like untagged string literals: ```js function tag(strings, ...values) { return strings.reduce( function(s,v,idx){ return s + (idx > 0 ? values[idx-1] : "") + v; }, "" ); } var desc = "awesome"; var text = tag`Everything is ${desc}!`; console.log( text ); // Everything is awesome! ``` In this snippet, `tag(..)` is a pass-through operation, in that it doesn't perform any special modifications, but just uses `reduce(..)` to loop over and splice/interleave `strings` and `values` together the same way an untagged string literal would have done. So what are some practical uses? There are many advanced ones that are beyond our scope to discuss here. But here's a simple idea that formats numbers as U.S. dollars (sort of like basic localization): ```js function dollabillsyall(strings, ...values) { return strings.reduce( function(s,v,idx){ if (idx > 0) { if (typeof values[idx-1] == "number") { // look, also using interpolated // string literals! s += `$${values[idx-1].toFixed( 2 )}`; } else { s += values[idx-1]; } } return s + v; }, "" ); } var amt1 = 11.99, amt2 = amt1 * 1.08, name = "Kyle"; var text = dollabillsyall `Thanks for your purchase, ${name}! Your product cost was ${amt1}, which with tax comes out to ${amt2}.` console.log( text ); // Thanks for your purchase, Kyle! Your // product cost was $11.99, which with tax // comes out to $12.95. ``` If a `number` value is encountered in the `values` array, we put `"$"` in front of it and format it to two decimal places with `toFixed(2)`. Otherwise, we let the value pass-through untouched. #### Raw Strings In the previous snippets, our tag functions receive the first argument we called `strings`, which is an array. But there's an additional bit of data included: the raw unprocessed versions of all the strings. You can access those raw string values using the `.raw` property, like this: ```js function showraw(strings, ...values) { console.log( strings ); console.log( strings.raw ); } showraw`Hello\nWorld`; // [ "Hello // World" ] // [ "Hello\nWorld" ] ``` The raw version of the value preserves the raw escaped `\n` sequence (the `\` and the `n` are separate characters), while the processed version considers it a single newline character. However, the earlier mentioned line-ending normalization is applied to both values. ES6 comes with a built-in function that can be used as a string literal tag: `String.raw(..)`. It simply passes through the raw versions of the `strings` values: ```js console.log( `Hello\nWorld` ); // Hello // World console.log( String.raw`Hello\nWorld` ); // Hello\nWorld String.raw`Hello\nWorld`.length; // 12 ``` Other uses for string literal tags included special processing for internationalization, localization, and more! ## Arrow Functions We've touched on `this` binding complications with functions earlier in this chapter, and they're covered at length in the *this & Object Prototypes* title of this series. It's important to understand the frustrations that `this`-based programming with normal functions brings, because that is the primary motivation for the new ES6 `=>` arrow function feature. Let's first illustrate what an arrow function looks like, as compared to normal functions: ```js function foo(x,y) { return x + y; } // versus var foo = (x,y) => x + y; ``` The arrow function definition consists of a parameter list (of zero or more parameters, and surrounding `( .. )` if there's not exactly one parameter), followed by the `=>` marker, followed by a function body. So, in the previous snippet, the arrow function is just the `(x,y) => x + y` part, and that function reference happens to be assigned to the variable `foo`. The body only needs to be enclosed by `{ .. }` if there's more than one expression, or if the body consists of a non-expression statement. If there's only one expression, and you omit the surrounding `{ .. }`, there's an implied `return` in front of the expression, as illustrated in the previous snippet. Here's some other arrow function variations to consider: ```js var f1 = () => 12; var f2 = x => x * 2; var f3 = (x,y) => { var z = x * 2 + y; y++; x *= 3; return (x + y + z) / 2; }; ``` Arrow functions are *always* function expressions; there is no arrow function declaration. It also should be clear that they are anonymous function expressions -- they have no named reference for the purposes of recursion or event binding/unbinding -- though "Function Names" in Chapter 7 will describe ES6's function name inference rules for debugging purposes. **Note:** All the capabilities of normal function parameters are available to arrow functions, including default values, destructuring, rest parameters, and so on. Arrow functions have a nice, shorter syntax, which makes them on the surface very attractive for writing terser code. Indeed, nearly all literature on ES6 (other than the titles in this series) seems to immediately and exclusively adopt the arrow function as "the new function." It is telling that nearly all examples in discussion of arrow functions are short single statement utilities, such as those passed as callbacks to various utilities. For example: ```js var a = [1,2,3,4,5]; a = a.map( v => v * 2 ); console.log( a ); // [2,4,6,8,10] ``` In those cases, where you have such inline function expressions, and they fit the pattern of computing a quick calculation in a single statement and returning that result, arrow functions indeed look to be an attractive and lightweight alternative to the more verbose `function` keyword and syntax. Most people tend to *ooh and aah* at nice terse examples like that, as I imagine you just did! However, I would caution you that it would seem to me somewhat a misapplication of this feature to use arrow function syntax with otherwise normal, multistatement functions, especially those that would otherwise be naturally expressed as function declarations. Recall the `dollabillsyall(..)` string literal tag function from earlier in this chapter -- let's change it to use `=>` syntax: ```js var dollabillsyall = (strings, ...values) => strings.reduce( (s,v,idx) => { if (idx > 0) { if (typeof values[idx-1] == "number") { // look, also using interpolated // string literals! s += `$${values[idx-1].toFixed( 2 )}`; } else { s += values[idx-1]; } } return s + v; }, "" ); ``` In this example, the only modifications I made were the removal of `function`, `return`, and some `{ .. }`, and then the insertion of `=>` and a `var`. Is this a significant improvement in the readability of the code? Meh. I'd actually argue that the lack of `return` and outer `{ .. }` partially obscures the fact that the `reduce(..)` call is the only statement in the `dollabillsyall(..)` function and that its result is the intended result of the call. Also, the trained eye that is so used to hunting for the word `function` in code to find scope boundaries now needs to look for the `=>` marker, which can definitely be harder to find in the thick of the code. While not a hard-and-fast rule, I'd say that the readability gains from `=>` arrow function conversion are inversely proportional to the length of the function being converted. The longer the function, the less `=>` helps; the shorter the function, the more `=>` can shine. I think it's probably more sensible and reasonable to adopt `=>` for the places in code where you do need short inline function expressions, but leave your normal-length main functions as is. ### Not Just Shorter Syntax, But `this` Most of the popular attention toward `=>` has been on saving those precious keystrokes by dropping `function`, `return`, and `{ .. }` from your code. But there's a big detail we've skipped over so far. I said at the beginning of the section that `=>` functions are closely related to `this` binding behavior. In fact, `=>` arrow functions are *primarily designed* to alter `this` behavior in a specific way, solving a particular and common pain point with `this`-aware coding. The saving of keystrokes is a red herring, a misleading sideshow at best. Let's revisit another example from earlier in this chapter: ```js var controller = { makeRequest: function(..){ var self = this; btn.addEventListener( "click", function(){ // .. self.makeRequest(..); }, false ); } }; ``` We used the `var self = this` hack, and then referenced `self.makeRequest(..)`, because inside the callback function we're passing to `addEventListener(..)`, the `this` binding will not be the same as it is in `makeRequest(..)` itself. In other words, because `this` bindings are dynamic, we fall back to the predictability of lexical scope via the `self` variable. Herein we finally can see the primary design characteristic of `=>` arrow functions. Inside arrow functions, the `this` binding is not dynamic, but is instead lexical. In the previous snippet, if we used an arrow function for the callback, `this` will be predictably what we wanted it to be. Consider: ```js var controller = { makeRequest: function(..){ btn.addEventListener( "click", () => { // .. this.makeRequest(..); }, false ); } }; ``` Lexical `this` in the arrow function callback in the previous snippet now points to the same value as in the enclosing `makeRequest(..)` function. In other words, `=>` is a syntactic stand-in for `var self = this`. In cases where `var self = this` (or, alternatively, a function `.bind(this)` call) would normally be helpful, `=>` arrow functions are a nicer alternative operating on the same prinicple. Sounds great, right? Not quite so simple. If `=>` replaces `var self = this` or `.bind(this)` and it helps, guess what happens if you use `=>` with a `this`-aware function that *doesn't* need `var self = this` to work? You might be able to guess that it's going to mess things up. Yeah. Consider: ```js var controller = { makeRequest: (..) => { // .. this.helper(..); }, helper: (..) => { // .. } }; controller.makeRequest(..); ``` Although we invoke as `controller.makeRequest(..)`, the `this.helper` reference fails, because `this` here doesn't point to `controller` as it normally would. Where does it point? It lexically inherits `this` from the surrounding scope. In this previous snippet, that's the global scope, where `this` points to the global object. Ugh. In addition to lexical `this`, arrow functions also have lexical `arguments` -- they don't have their own `arguments` array but instead inherit from their parent -- as well as lexical `super` and `new.target` (see "Classes" in Chapter 3). So now we can conclude a more nuanced set of rules for when `=>` is appropriate and not: * If you have a short, single-statement inline function expression, where the only statement is a `return` of some computed value, *and* that function doesn't already make a `this` reference inside it, *and* there's no self-reference (recursion, event binding/unbinding), *and* you don't reasonably expect the function to ever be that way, you can probably safely refactor it to be an `=>` arrow function. * If you have an inner function expression that's relying on a `var self = this` hack or a `.bind(this)` call on it in the enclosing function to ensure proper `this` binding, that inner function expression can probably safely become an `=>` arrow function. * If you have an inner function expression that's relying on something like `var args = Array.prototype.slice.call(arguments)` in the enclosing function to make a lexical copy of `arguments`, that inner function expression can probably safely become an `=>` arrow function. * For everything else -- normal function declarations, longer multistatement function expressions, functions that need a lexical name identifier self-reference (recursion, etc.), and any other function that doesn't fit the previous characteristics -- you should probably avoid `=>` function syntax. Bottom line: `=>` is about lexical binding of `this`, `arguments`, and `super`. These are intentional features designed to fix some common problems, not bugs, quirks, or mistakes in ES6. Don't believe any hype that `=>` is primarily, or even mostly, about fewer keystrokes. Whether you save keystrokes or waste them, you should know exactly what you are intentionally doing with every character typed. **Tip:** If you have a function that for any of these articulated reasons is not a good match for an `=>` arrow function, but it's being declared as part of an object literal, recall from "Concise Methods" earlier in this chapter that there's another option for shorter function syntax. If you prefer a visual decision chart for how/why to pick an arrow function: ## `for..of` Loops Joining the `for` and `for..in` loops from the JavaScript we're all familiar with, ES6 adds a `for..of` loop, which loops over the set of values produced by an *iterator*. The value you loop over with `for..of` must be an *iterable*, or it must be a value which can be coerced/boxed to an object (see the *Types & Grammar* title of this series) that is an iterable. An iterable is simply an object that is able to produce an iterator, which the loop then uses. Let's compare `for..of` to `for..in` to illustrate the difference: ```js var a = ["a","b","c","d","e"]; for (var idx in a) { console.log( idx ); } // 0 1 2 3 4 for (var val of a) { console.log( val ); } // "a" "b" "c" "d" "e" ``` As you can see, `for..in` loops over the keys/indexes in the `a` array, while `for..of` loops over the values in `a`. Here's the pre-ES6 version of the `for..of` from that previous snippet: ```js var a = ["a","b","c","d","e"], k = Object.keys( a ); for (var val, i = 0; i < k.length; i++) { val = a[ k[i] ]; console.log( val ); } // "a" "b" "c" "d" "e" ``` And here's the ES6 but non-`for..of` equivalent, which also gives a glimpse at manually iterating an iterator (see "Iterators" in Chapter 3): ```js var a = ["a","b","c","d","e"]; for (var val, ret, it = a[Symbol.iterator](); (ret = it.next()) && !ret.done; ) { val = ret.value; console.log( val ); } // "a" "b" "c" "d" "e" ``` Under the covers, the `for..of` loop asks the iterable for an iterator (using the built-in `Symbol.iterator`; see "Well-Known Symbols" in Chapter 7), then it repeatedly calls the iterator and assigns its produced value to the loop iteration variable. Standard built-in values in JavaScript that are by default iterables (or provide them) include: * Arrays * Strings * Generators (see Chapter 3) * Collections / TypedArrays (see Chapter 5) **Warning:** Plain objects are not by default suitable for `for..of` looping. That's because they don't have a default iterator, which is intentional, not a mistake. However, we won't go any further into those nuanced reasonings here. In "Iterators" in Chapter 3, we'll see how to define iterators for our own objects, which lets `for..of` loop over any object to get a set of values we define. Here's how to loop over the characters in a primitive string: ```js for (var c of "hello") { console.log( c ); } // "h" "e" "l" "l" "o" ``` The `"hello"` primitive string value is coerced/boxed to the `String` object wrapper equivalent, which is an iterable by default. In `for (XYZ of ABC)..`, the `XYZ` clause can either be an assignment expression or a declaration, identical to that same clause in `for` and `for..in` loops. So you can do stuff like this: ```js var o = {}; for (o.a of [1,2,3]) { console.log( o.a ); } // 1 2 3 for ({x: o.a} of [ {x: 1}, {x: 2}, {x: 3} ]) { console.log( o.a ); } // 1 2 3 ``` `for..of` loops can be prematurely stopped, just like other loops, with `break`, `continue`, `return` (if in a function), and thrown exceptions. In any of these cases, the iterator's `return(..)` function is automatically called (if one exists) to let the iterator perform cleanup tasks, if necessary. **Note:** See "Iterators" in Chapter 3 for more complete coverage on iterables and iterators. ## Regular Expressions Let's face it: regular expressions haven't changed much in JS in a long time. So it's a great thing that they've finally learned a couple of new tricks in ES6. We'll briefly cover the additions here, but the overall topic of regular expressions is so dense that you'll need to turn to chapters/books dedicated to it (of which there are many!) if you need a refresher. ### Unicode Flag We'll cover the topic of Unicode in more detail in "Unicode" later in this chapter. Here, we'll just look briefly at the new `u` flag for ES6+ regular expressions, which turns on Unicode matching for that expression. JavaScript strings are typically interpreted as sequences of 16-bit characters, which correspond to the characters in the *Basic Multilingual Plane (BMP)* (http://en.wikipedia.org/wiki/Plane_%28Unicode%29). But there are many UTF-16 characters that fall outside this range, and so strings may have these multibyte characters in them. Prior to ES6, regular expressions could only match based on BMP characters, which means that those extended characters were treated as two separate characters for matching purposes. This is often not ideal. So, as of ES6, the `u` flag tells a regular expression to process a string with the interpretation of Unicode (UTF-16) characters, such that such an extended character will be matched as a single entity. **Warning:** Despite the name implication, "UTF-16" doesn't strictly mean 16 bits. Modern Unicode uses 21 bits, and standards like UTF-8 and UTF-16 refer roughly to how many bits are used in the representation of a character. An example (straight from the ES6 specification): 𝄞 (the musical symbol G-clef) is Unicode point U+1D11E (0x1D11E). If this character appears in a regular expression pattern (like `/𝄞/`), the standard BMP interpretation would be that it's two separate characters (0xD834 and 0xDD1E) to match with. But the new ES6 Unicode-aware mode means that `/𝄞/u` (or the escaped Unicode form `/\u{1D11E}/u`) will match `"𝄞"` in a string as a single matched character. You might be wondering why this matters? In non-Unicode BMP mode, the pattern is treated as two separate characters, but would still find the match in a string with the `"𝄞"` character in it, as you can see if you try: ```js /𝄞/.test( "𝄞-clef" ); // true ``` The length of the match is what matters. For example: ```js /^.-clef/ .test( "𝄞-clef" ); // false /^.-clef/u.test( "𝄞-clef" ); // true ``` The `^.-clef` in the pattern says to match only a single character at the beginning before the normal `"-clef"` text. In standard BMP mode, the match fails (two characters), but with `u` Unicode mode flagged on, the match succeeds (one character). It's also important to note that `u` makes quantifiers like `+` and `*` apply to the entire Unicode code point as a single character, not just the *lower surrogate* (aka rightmost half of the symbol) of the character. The same goes for Unicode characters appearing in character classes, like `/[💩-💫]/u`. **Note:** There's plenty more nitty-gritty details about `u` behavior in regular expressions, which Mathias Bynens (https://twitter.com/mathias) has written extensively about (https://mathiasbynens.be/notes/es6-unicode-regex). ### Sticky Flag Another flag mode added to ES6 regular expressions is `y`, which is often called "sticky mode." *Sticky* essentially means the regular expression has a virtual anchor at its beginning that keeps it rooted to matching at only the position indicated by the regular expression's `lastIndex` property. To illustrate, let's consider two regular expressions, the first without sticky mode and the second with: ```js var re1 = /foo/, str = "++foo++"; re1.lastIndex; // 0 re1.test( str ); // true re1.lastIndex; // 0 -- not updated re1.lastIndex = 4; re1.test( str ); // true -- ignored `lastIndex` re1.lastIndex; // 4 -- not updated ``` Three things to observe about this snippet: * `test(..)` doesn't pay any attention to `lastIndex`'s value, and always just performs its match from the beginning of the input string. * Because our pattern does not have a `^` start-of-input anchor, the search for `"foo"` is free to move ahead through the whole string looking for a match. * `lastIndex` is not updated by `test(..)`. Now, let's try a sticky mode regular expression: ```js var re2 = /foo/y, // <-- notice the `y` sticky flag str = "++foo++"; re2.lastIndex; // 0 re2.test( str ); // false -- "foo" not found at `0` re2.lastIndex; // 0 re2.lastIndex = 2; re2.test( str ); // true re2.lastIndex; // 5 -- updated to after previous match re2.test( str ); // false re2.lastIndex; // 0 -- reset after previous match failure ``` And so our new observations about sticky mode: * `test(..)` uses `lastIndex` as the exact and only position in `str` to look to make a match. There is no moving ahead to look for the match -- it's either there at the `lastIndex` position or not. * If a match is made, `test(..)` updates `lastIndex` to point to the character immediately following the match. If a match fails, `test(..)` resets `lastIndex` back to `0`. Normal non-sticky patterns that aren't otherwise `^`-rooted to the start-of-input are free to move ahead in the input string looking for a match. But sticky mode restricts the pattern to matching just at the position of `lastIndex`. As I suggested at the beginning of this section, another way of looking at this is that `y` implies a virtual anchor at the beginning of the pattern that is relative (aka constrains the start of the match) to exactly the `lastIndex` position. **Warning:** In previous literature on the topic, it has alternatively been asserted that this behavior is like `y` implying a `^` (start-of-input) anchor in the pattern. This is inaccurate. We'll explain in further detail in "Anchored Sticky" later. #### Sticky Positioning It may seem strangely limiting that to use `y` for repeated matches, you have to manually ensure `lastIndex` is in the exact right position, as it has no move-ahead capability for matching. Here's one possible scenario: if you know that the match you care about is always going to be at a position that's a multiple of a number (e.g., `0`, `10`, `20`, etc.), you can just construct a limited pattern matching what you care about, but then manually set `lastIndex` each time before match to those fixed positions. Consider: ```js var re = /f../y, str = "foo far fad"; str.match( re ); // ["foo"] re.lastIndex = 10; str.match( re ); // ["far"] re.lastIndex = 20; str.match( re ); // ["fad"] ``` However, if you're parsing a string that isn't formatted in fixed positions like that, figuring out what to set `lastIndex` to before each match is likely going to be untenable. There's a saving nuance to consider here. `y` requires that `lastIndex` be in the exact position for a match to occur. But it doesn't strictly require that *you* manually set `lastIndex`. Instead, you can construct your expressions in such a way that they capture in each main match everything before and after the thing you care about, up to right before the next thing you'll care to match. Because `lastIndex` will set to the next character beyond the end of a match, if you've matched everything up to that point, `lastIndex` will always be in the correct position for the `y` pattern to start from the next time. **Warning:** If you can't predict the structure of the input string in a sufficiently patterned way like that, this technique may not be suitable and you may not be able to use `y`. Having structured string input is likely the most practical scenario where `y` will be capable of performing repeated matching throughout a string. Consider: ```js var re = /\d+\.\s(.*?)(?:\s|$)/y str = "1. foo 2. bar 3. baz"; str.match( re ); // [ "1. foo ", "foo" ] re.lastIndex; // 7 -- correct position! str.match( re ); // [ "2. bar ", "bar" ] re.lastIndex; // 14 -- correct position! str.match( re ); // ["3. baz", "baz"] ``` This works because I knew something ahead of time about the structure of the input string: there is always a numeral prefix like `"1. "` before the desired match (`"foo"`, etc.), and either a space after it, or the end of the string (`$` anchor). So the regular expression I constructed captures all of that in each main match, and then I use a matching group `( )` so that the stuff I really care about is separated out for convenience. After the first match (`"1. foo "`), the `lastIndex` is `7`, which is already the position needed to start the next match, for `"2. bar "`, and so on. If you're going to use `y` sticky mode for repeated matches, you'll probably want to look for opportunities to have `lastIndex` automatically positioned as we've just demonstrated. #### Sticky Versus Global Some readers may be aware that you can emulate something like this `lastIndex`-relative matching with the `g` global match flag and the `exec(..)` method, as so: ```js var re = /o+./g, // <-- look, `g`! str = "foot book more"; re.exec( str ); // ["oot"] re.lastIndex; // 4 re.exec( str ); // ["ook"] re.lastIndex; // 9 re.exec( str ); // ["or"] re.lastIndex; // 13 re.exec( str ); // null -- no more matches! re.lastIndex; // 0 -- starts over now! ``` While it's true that `g` pattern matches with `exec(..)` start their matching from `lastIndex`'s current value, and also update `lastIndex` after each match (or failure), this is not the same thing as `y`'s behavior. Notice in the previous snippet that `"ook"`, located at position `6`, was matched and found by the second `exec(..)` call, even though at the time, `lastIndex` was `4` (from the end of the previous match). Why? Because as we said earlier, non-sticky matches are free to move ahead in their matching. A sticky mode expression would have failed here, because it would not be allowed to move ahead. In addition to perhaps undesired move-ahead matching behavior, another downside to just using `g` instead of `y` is that `g` changes the behavior of some matching methods, like `str.match(re)`. Consider: ```js var re = /o+./g, // <-- look, `g`! str = "foot book more"; str.match( re ); // ["oot","ook","or"] ``` See how all the matches were returned at once? Sometimes that's OK, but sometimes that's not what you want. The `y` sticky flag will give you one-at-a-time progressive matching with utilities like `test(..)` and `match(..)`. Just make sure the `lastIndex` is always in the right position for each match! #### Anchored Sticky As we warned earlier, it's inaccurate to think of sticky mode as implying a pattern starts with `^`. The `^` anchor has a distinct meaning in regular expressions, which is *not altered* by sticky mode. `^` is an anchor that *always* refers to the beginning of the input, and *is not* in any way relative to `lastIndex`. Besides poor/inaccurate documentation on this topic, the confusion is unfortunately strengthened further because an older pre-ES6 experiment with sticky mode in Firefox *did* make `^` relative to `lastIndex`, so that behavior has been around for years. ES6 elected not to do it that way. `^` in a pattern means start-of-input absolutely and only. As a consequence, a pattern like `/^foo/y` will always and only find a `"foo"` match at the beginning of a string, *if it's allowed to match there*. If `lastIndex` is not `0`, the match will fail. Consider: ```js var re = /^foo/y, str = "foo"; re.test( str ); // true re.test( str ); // false re.lastIndex; // 0 -- reset after failure re.lastIndex = 1; re.test( str ); // false -- failed for positioning re.lastIndex; // 0 -- reset after failure ``` Bottom line: `y` plus `^` plus `lastIndex > 0` is an incompatible combination that will always cause a failed match. **Note:** While `y` does not alter the meaning of `^` in any way, the `m` multiline mode *does*, such that `^` means start-of-input *or* start of text after a newline. So, if you combine `y` and `m` flags together for a pattern, you can find multiple `^`-rooted matches in a string. But remember: because it's `y` sticky, you'll have to make sure `lastIndex` is pointing at the correct new line position (likely by matching to the end of the line) each subsequent time, or no subsequent matches will be made. ### Regular Expression `flags` Prior to ES6, if you wanted to examine a regular expression object to see what flags it had applied, you needed to parse them out -- ironically, probably with another regular expression -- from the content of the `source` property, such as: ```js var re = /foo/ig; re.toString(); // "/foo/ig" var flags = re.toString().match( /\/([gim]*)$/ )[1]; flags; // "ig" ``` As of ES6, you can now get these values directly, with the new `flags` property: ```js var re = /foo/ig; re.flags; // "gi" ``` It's a small nuance, but the ES6 specification calls for the expression's flags to be listed in this order: `"gimuy"`, regardless of what order the original pattern was specified with. That's the reason for the difference between `/ig` and `"gi"`. No, the order of flags specified or listed doesn't matter. Another tweak from ES6 is that the `RegExp(..)` constructor is now `flags`-aware if you pass it an existing regular expression: ```js var re1 = /foo*/y; re1.source; // "foo*" re1.flags; // "y" var re2 = new RegExp( re1 ); re2.source; // "foo*" re2.flags; // "y" var re3 = new RegExp( re1, "ig" ); re3.source; // "foo*" re3.flags; // "gi" ``` Prior to ES6, the `re3` construction would throw an error, but as of ES6 you can override the flags when duplicating. ## Number Literal Extensions Prior to ES5, number literals looked like the following -- the octal form was not officially specified, only allowed as an extension that browsers had come to de facto agreement on: ```js var dec = 42, oct = 052, hex = 0x2a; ``` **Note:** Though you are specifying a number in different bases, the number's mathematic value is what is stored, and the default output interpretation is always base-10. The three variables in the previous snippet all have the `42` value stored in them. To further illustrate that `052` was a nonstandard form extension, consider: ```js Number( "42" ); // 42 Number( "052" ); // 52 Number( "0x2a" ); // 42 ``` ES5 continued to permit the browser-extended octal form (including such inconsistencies), except that in strict mode, the octal literal (`052`) form is disallowed. This restriction was done mainly because many developers had the habit (from other languages) of seemingly innocuously prefixing otherwise base-10 numbers with `0`'s for code alignment purposes, and then running into the accidental fact that they'd changed the number value entirely! ES6 continues the legacy of changes/variations to how number literals outside base-10 numbers can be represented. There's now an official octal form, an amended hexadecimal form, and a brand-new binary form. For web compatibility reasons, the old octal `052` form will continue to be legal (though unspecified) in non-strict mode, but should really never be used anymore. Here are the new ES6 number literal forms: ```js var dec = 42, oct = 0o52, // or `0O52` :( hex = 0x2a, // or `0X2a` :/ bin = 0b101010; // or `0B101010` :/ ``` The only decimal form allowed is base-10. Octal, hexadecimal, and binary are all integer forms. And the string representations of these forms are all able to be coerced/converted to their number equivalent: ```js Number( "42" ); // 42 Number( "0o52" ); // 42 Number( "0x2a" ); // 42 Number( "0b101010" ); // 42 ``` Though not strictly new to ES6, it's a little-known fact that you can actually go the opposite direction of conversion (well, sort of): ```js var a = 42; a.toString(); // "42" -- also `a.toString( 10 )` a.toString( 8 ); // "52" a.toString( 16 ); // "2a" a.toString( 2 ); // "101010" ``` In fact, you can represent a number this way in any base from `2` to `36`, though it'd be rare that you'd go outside the standard bases: 2, 8, 10, and 16. ## Unicode Let me just say that this section is not an exhaustive everything-you-ever-wanted-to-know-about-Unicode resource. I want to cover what you need to know that's *changing* for Unicode in ES6, but we won't go much deeper than that. Mathias Bynens (http://twitter.com/mathias) has written/spoken extensively and brilliantly about JS and Unicode (see https://mathiasbynens.be/notes/javascript-unicode and http://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode). The Unicode characters that range from `0x0000` to `0xFFFF` contain all the standard printed characters (in various languages) that you're likely to have seen or interacted with. This group of characters is called the *Basic Multilingual Plane (BMP)*. The BMP even contains fun symbols like this cool snowman: ☃ (U+2603). There are lots of other extended Unicode characters beyond this BMP set, which range up to `0x10FFFF`. These symbols are often referred to as *astral* symbols, as that's the name given to the set of 16 *planes* (e.g., layers/groupings) of characters beyond the BMP. Examples of astral symbols include 𝄞 (U+1D11E) and 💩 (U+1F4A9). Prior to ES6, JavaScript strings could specify Unicode characters using Unicode escaping, such as: ```js var snowman = "\u2603"; console.log( snowman ); // "☃" ``` However, the `\uXXXX` Unicode escaping only supports four hexadecimal characters, so you can only represent the BMP set of characters in this way. To represent an astral character using Unicode escaping prior to ES6, you need to use a *surrogate pair* -- basically two specially calculated Unicode-escaped characters side by side, which JS interprets together as a single astral character: ```js var gclef = "\uD834\uDD1E"; console.log( gclef ); // "𝄞" ``` As of ES6, we now have a new form for Unicode escaping (in strings and regular expressions), called Unicode *code point escaping*: ```js var gclef = "\u{1D11E}"; console.log( gclef ); // "𝄞" ``` As you can see, the difference is the presence of the `{ }` in the escape sequence, which allows it to contain any number of hexadecimal characters. Because you only need six to represent the highest possible code point value in Unicode (i.e., 0x10FFFF), this is sufficient. ### Unicode-Aware String Operations By default, JavaScript string operations and methods are not sensitive to astral symbols in string values. So, they treat each BMP character individually, even the two surrogate halves that make up an otherwise single astral character. Consider: ```js var snowman = "☃"; snowman.length; // 1 var gclef = "𝄞"; gclef.length; // 2 ``` So, how do we accurately calculate the length of such a string? In this scenario, the following trick will work: ```js var gclef = "𝄞"; [...gclef].length; // 1 Array.from( gclef ).length; // 1 ``` Recall from the "`for..of` Loops" section earlier in this chapter that ES6 strings have built-in iterators. This iterator happens to be Unicode-aware, meaning it will automatically output an astral symbol as a single value. We take advantage of that using the `...` spread operator in an array literal, which creates an array of the string's symbols. Then we just inspect the length of that resultant array. ES6's `Array.from(..)` does basically the same thing as `[...XYZ]`, but we'll cover that utility in detail in Chapter 6. **Warning:** It should be noted that constructing and exhausting an iterator just to get the length of a string is quite expensive on performance, relatively speaking, compared to what a theoretically optimized native utility/property would do. Unfortunately, the full answer is not as simple or straightforward. In addition to the surrogate pairs (which the string iterator takes care of), there are special Unicode code points that behave in other special ways, which is much harder to account for. For example, there's a set of code points that modify the previous adjacent character, known as *Combining Diacritical Marks*. Consider these two string outputs: ```js console.log( s1 ); // "é" console.log( s2 ); // "é" ``` They look the same, but they're not! Here's how we created `s1` and `s2`: ```js var s1 = "\xE9", s2 = "e\u0301"; ``` As you can probably guess, our previous `length` trick doesn't work with `s2`: ```js [...s1].length; // 1 [...s2].length; // 2 ``` So what can we do? In this case, we can perform a *Unicode normalization* on the value before inquiring about its length, using the ES6 `String#normalize(..)` utility (which we'll cover more in Chapter 6): ```js var s1 = "\xE9", s2 = "e\u0301"; s1.normalize().length; // 1 s2.normalize().length; // 1 s1 === s2; // false s1 === s2.normalize(); // true ``` Essentially, `normalize(..)` takes a sequence like `"e\u0301"` and normalizes it to `"\xE9"`. Normalization can even combine multiple adjacent combining marks if there's a suitable Unicode character they combine to: ```js var s1 = "o\u0302\u0300", s2 = s1.normalize(), s3 = "ồ"; s1.length; // 3 s2.length; // 1 s3.length; // 1 s2 === s3; // true ``` Unfortunately, normalization isn't fully perfect here, either. If you have multiple combining marks modifying a single character, you may not get the length count you'd expect, because there may not be a single defined normalized character that represents the combination of all the marks. For example: ```js var s1 = "e\u0301\u0330"; console.log( s1 ); // "ḛ́" s1.normalize().length; // 2 ``` The further you go down this rabbit hole, the more you realize that it's difficult to get one precise definition for "length." What we see visually rendered as a single character -- more precisely called a *grapheme* -- doesn't always strictly relate to a single "character" in the program processing sense. **Tip:** If you want to see just how deep this rabbit hole goes, check out the "Grapheme Cluster Boundaries" algorithm (http://www.Unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries). ### Character Positioning Similar to length complications, what does it actually mean to ask, "what is the character at position 2?" The naive pre-ES6 answer comes from `charAt(..)`, which will not respect the atomicity of an astral character, nor will it take into account combining marks. Consider: ```js var s1 = "abc\u0301d", s2 = "ab\u0107d", s3 = "ab\u{1d49e}d"; console.log( s1 ); // "abćd" console.log( s2 ); // "abćd" console.log( s3 ); // "ab𝒞d" s1.charAt( 2 ); // "c" s2.charAt( 2 ); // "ć" s3.charAt( 2 ); // "" <-- unprintable surrogate s3.charAt( 3 ); // "" <-- unprintable surrogate ``` So, is ES6 giving us a Unicode-aware version of `charAt(..)`? Unfortunately, no. At the time of this writing, there's a proposal for such a utility that's under consideration for post-ES6. But with what we explored in the previous section (and of course with the limitations noted thereof!), we can hack an ES6 answer: ```js var s1 = "abc\u0301d", s2 = "ab\u0107d", s3 = "ab\u{1d49e}d"; [...s1.normalize()][2]; // "ć" [...s2.normalize()][2]; // "ć" [...s3.normalize()][2]; // "𝒞" ``` **Warning:** Reminder of an earlier warning: constructing and exhausting an iterator each time you want to get at a single character is... very not ideal, performance wise. Let's hope we get a built-in and optimized utility for this soon, post-ES6. What about a Unicode-aware version of the `charCodeAt(..)` utility? ES6 gives us `codePointAt(..)`: ```js var s1 = "abc\u0301d", s2 = "ab\u0107d", s3 = "ab\u{1d49e}d"; s1.normalize().codePointAt( 2 ).toString( 16 ); // "107" s2.normalize().codePointAt( 2 ).toString( 16 ); // "107" s3.normalize().codePointAt( 2 ).toString( 16 ); // "1d49e" ``` What about the other direction? A Unicode-aware version of `String.fromCharCode(..)` is ES6's `String.fromCodePoint(..)`: ```js String.fromCodePoint( 0x107 ); // "ć" String.fromCodePoint( 0x1d49e ); // "𝒞" ``` So wait, can we just combine `String.fromCodePoint(..)` and `codePointAt(..)` to get a better version of a Unicode-aware `charAt(..)` from earlier? Yep! ```js var s1 = "abc\u0301d", s2 = "ab\u0107d", s3 = "ab\u{1d49e}d"; String.fromCodePoint( s1.normalize().codePointAt( 2 ) ); // "ć" String.fromCodePoint( s2.normalize().codePointAt( 2 ) ); // "ć" String.fromCodePoint( s3.normalize().codePointAt( 2 ) ); // "𝒞" ``` There's quite a few other string methods we haven't addressed here, including `toUpperCase()`, `toLowerCase()`, `substring(..)`, `indexOf(..)`, `slice(..)`, and a dozen others. None of these have been changed or augmented for full Unicode awareness, so you should be very careful -- probably just avoid them! -- when working with strings containing astral symbols. There are also several string methods that use regular expressions for their behavior, like `replace(..)` and `match(..)`. Thankfully, ES6 brings Unicode awareness to regular expressions, as we covered in "Unicode Flag" earlier in this chapter. OK, there we have it! JavaScript's Unicode string support is significantly better over pre-ES6 (though still not perfect) with the various additions we've just covered. ### Unicode Identifier Names Unicode can also be used in identifier names (variables, properties, etc.). Prior to ES6, you could do this with Unicode-escapes, like: ```js var \u03A9 = 42; // same as: var Ω = 42; ``` As of ES6, you can also use the earlier explained code point escape syntax: ```js var \u{2B400} = 42; // same as: var 𫐀 = 42; ``` There's a complex set of rules around exactly which Unicode characters are allowed. Furthermore, some are allowed only if they're not the first character of the identifier name. **Note:** Mathias Bynens has a great post (https://mathiasbynens.be/notes/javascript-identifiers-es6) on all the nitty-gritty details. The reasons for using such unusual characters in identifier names are rather rare and academic. You typically won't be best served by writing code that relies on these esoteric capabilities. ## Symbols With ES6, for the first time in quite a while, a new primitive type has been added to JavaScript: the `symbol`. Unlike the other primitive types, however, symbols don't have a literal form. Here's how you create a symbol: ```js var sym = Symbol( "some optional description" ); typeof sym; // "symbol" ``` Some things to note: * You cannot and should not use `new` with `Symbol(..)`. It's not a constructor, nor are you producing an object. * The parameter passed to `Symbol(..)` is optional. If passed, it should be a string that gives a friendly description for the symbol's purpose. * The `typeof` output is a new value (`"symbol"`) that is the primary way to identify a symbol. The description, if provided, is solely used for the stringification representation of the symbol: ```js sym.toString(); // "Symbol(some optional description)" ``` Similar to how primitive string values are not instances of `String`, symbols are also not instances of `Symbol`. If, for some reason, you want to construct a boxed wrapper object form of a symbol value, you can do the following: ```js sym instanceof Symbol; // false var symObj = Object( sym ); symObj instanceof Symbol; // true symObj.valueOf() === sym; // true ``` **Note:** `symObj` in this snippet is interchangeable with `sym`; either form can be used in all places symbols are utilized. There's not much reason to use the boxed wrapper object form (`symObj`) instead of the primitive form (`sym`). Keeping with similar advice for other primitives, it's probably best to prefer `sym` over `symObj`. The internal value of a symbol itself -- referred to as its `name` -- is hidden from the code and cannot be obtained. You can think of this symbol value as an automatically generated, unique (within your application) string value. But if the value is hidden and unobtainable, what's the point of having a symbol at all? The main point of a symbol is to create a string-like value that can't collide with any other value. So, for example, consider using a symbol as a constant representing an event name: ```js const EVT_LOGIN = Symbol( "event.login" ); ``` You'd then use `EVT_LOGIN` in place of a generic string literal like `"event.login"`: ```js evthub.listen( EVT_LOGIN, function(data){ // .. } ); ``` The benefit here is that `EVT_LOGIN` holds a value that cannot be duplicated (accidentally or otherwise) by any other value, so it is impossible for there to be any confusion of which event is being dispatched or handled. **Note:** Under the covers, the `evthub` utility assumed in the previous snippet would almost certainly be using the symbol value from the `EVT_LOGIN` argument directly as the property/key in some internal object (hash) that tracks event handlers. If `evthub` instead needed to use the symbol value as a real string, it would need to explicitly coerce with `String(..)` or `toString()`, as implicit string coercion of symbols is not allowed. You may use a symbol directly as a property name/key in an object, such as a special property that you want to treat as hidden or meta in usage. It's important to know that although you intend to treat it as such, it is not *actually* a hidden or untouchable property. Consider this module that implements the *singleton* pattern behavior -- that is, it only allows itself to be created once: ```js const INSTANCE = Symbol( "instance" ); function HappyFace() { if (HappyFace[INSTANCE]) return HappyFace[INSTANCE]; function smile() { .. } return HappyFace[INSTANCE] = { smile: smile }; } var me = HappyFace(), you = HappyFace(); me === you; // true ``` The `INSTANCE` symbol value here is a special, almost hidden, meta-like property stored statically on the `HappyFace()` function object. It could alternatively have been a plain old property like `__instance`, and the behavior would have been identical. The usage of a symbol simply improves the metaprogramming style, keeping this `INSTANCE` property set apart from any other normal properties. ### Symbol Registry One mild downside to using symbols as in the last few examples is that the `EVT_LOGIN` and `INSTANCE` variables had to be stored in an outer scope (perhaps even the global scope), or otherwise somehow stored in a publicly available location, so that all parts of the code that need to use the symbols can access them. To aid in organizing code with access to these symbols, you can create symbol values with the *global symbol registry*. For example: ```js const EVT_LOGIN = Symbol.for( "event.login" ); console.log( EVT_LOGIN ); // Symbol(event.login) ``` And: ```js function HappyFace() { const INSTANCE = Symbol.for( "instance" ); if (HappyFace[INSTANCE]) return HappyFace[INSTANCE]; // .. return HappyFace[INSTANCE] = { .. }; } ``` `Symbol.for(..)` looks in the global symbol registry to see if a symbol is already stored with the provided description text, and returns it if so. If not, it creates one to return. In other words, the global symbol registry treats symbol values, by description text, as singletons themselves. But that also means that any part of your application can retrieve the symbol from the registry using `Symbol.for(..)`, as long as the matching description name is used. Ironically, symbols are basically intended to replace the use of *magic strings* (arbitrary string values given special meaning) in your application. But you precisely use *magic* description string values to uniquely identify/locate them in the global symbol registry! To avoid accidental collisions, you'll probably want to make your symbol descriptions quite unique. One easy way of doing that is to include prefix/context/namespacing information in them. For example, consider a utility such as the following: ```js function extractValues(str) { var key = Symbol.for( "extractValues.parse" ), re = extractValues[key] || /[^=&]+?=([^&]+?)(?=&|$)/g, values = [], match; while (match = re.exec( str )) { values.push( match[1] ); } return values; } ``` We use the magic string value `"extractValues.parse"` because it's quite unlikely that any other symbol in the registry would ever collide with that description. If a user of this utility wants to override the parsing regular expression, they can also use the symbol registry: ```js extractValues[Symbol.for( "extractValues.parse" )] = /..some pattern../g; extractValues( "..some string.." ); ``` Aside from the assistance the symbol registry provides in globally storing these values, everything we're seeing here could have been done by just actually using the magic string `"extractValues.parse"` as the key, rather than the symbol. The improvements exist at the metaprogramming level more than the functional level. You may have occasion to use a symbol value that has been stored in the registry to look up what description text (key) it's stored under. For example, you may need to signal to another part of your application how to locate a symbol in the registry because you cannot pass the symbol value itself. You can retrieve a registered symbol's description text (key) using `Symbol.keyFor(..)`: ```js var s = Symbol.for( "something cool" ); var desc = Symbol.keyFor( s ); console.log( desc ); // "something cool" // get the symbol from the registry again var s2 = Symbol.for( desc ); s2 === s; // true ``` ### Symbols as Object Properties If a symbol is used as a property/key of an object, it's stored in a special way so that the property will not show up in a normal enumeration of the object's properties: ```js var o = { foo: 42, [ Symbol( "bar" ) ]: "hello world", baz: true }; Object.getOwnPropertyNames( o ); // [ "foo","baz" ] ``` To retrieve an object's symbol properties: ```js Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ] ``` This makes it clear that a property symbol is not actually hidden or inaccessible, as you can always see it in the `Object.getOwnPropertySymbols(..)` list. #### Built-In Symbols ES6 comes with a number of predefined built-in symbols that expose various meta behaviors on JavaScript object values. However, these symbols are *not* registered in the global symbol registry, as one might expect. Instead, they're stored as properties on the `Symbol` function object. For example, in the "`for..of`" section earlier in this chapter, we introduced the `Symbol.iterator` value: ```js var a = [1,2,3]; a[Symbol.iterator]; // native function ``` The specification uses the `@@` prefix notation to refer to the built-in symbols, the most common ones being: `@@iterator`, `@@toStringTag`, `@@toPrimitive`. Several others are defined as well, though they probably won't be used as often. **Note:** See "Well Known Symbols" in Chapter 7 for detailed information about how these built-in symbols are used for meta programming purposes. ## Review ES6 adds a heap of new syntax forms to JavaScript, so there's plenty to learn! Most of these are designed to ease the pain points of common programming idioms, such as setting default values to function parameters and gathering the "rest" of the parameters into an array. Destructuring is a powerful tool for more concisely expressing assignments of values from arrays and nested objects. While features like `=>` arrow functions appear to also be all about shorter and nicer-looking syntax, they actually have very specific behaviors that you should intentionally use only in appropriate situations. Expanded Unicode support, new tricks for regular expressions, and even a new primitive `symbol` type round out the syntactic evolution of ES6. ================================================ FILE: es6 & beyond/ch3.md ================================================ # You Don't Know JS: ES6 & Beyond # Chapter 3: Organization It's one thing to write JS code, but it's another to properly organize it. Utilizing common patterns for organization and reuse goes a long way to improving the readability and understandability of your code. Remember: code is at least as much about communicating to other developers as it is about feeding the computer instructions. ES6 has several important features that help significantly improve these patterns, including: iterators, generators, modules, and classes. ## Iterators An *iterator* is a structured pattern for pulling information from a source in one-at-a-time fashion. This pattern has been around programming for a long time. And to be sure, JS developers have been ad hoc designing and implementing iterators in JS programs since before anyone can remember, so it's not at all a new topic. What ES6 has done is introduce an implicit standardized interface for iterators. Many of the built-in data structures in JavaScript will now expose an iterator implementing this standard. And you can also construct your own iterators adhering to the same standard, for maximal interoperability. Iterators are a way of organizing ordered, sequential, pull-based consumption of data. For example, you may implement a utility that produces a new unique identifier each time it's requested. Or you may produce an infinite series of values that rotate through a fixed list, in round-robin fashion. Or you could attach an iterator to a database query result to pull out new rows one at a time. Although they have not commonly been used in JS in such a manner, iterators can also be thought of as controlling behavior one step at a time. This can be illustrated quite clearly when considering generators (see "Generators" later in this chapter), though you can certainly do the same without generators. ### Interfaces At the time of this writing, ES6 section 25.1.1.2 (https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface) details the `Iterator` interface as having the following requirement: ``` Iterator [required] next() {method}: retrieves next IteratorResult ``` There are two optional members that some iterators are extended with: ``` Iterator [optional] return() {method}: stops iterator and returns IteratorResult throw() {method}: signals error and returns IteratorResult ``` The `IteratorResult` interface is specified as: ``` IteratorResult value {property}: current iteration value or final return value (optional if `undefined`) done {property}: boolean, indicates completion status ``` **Note:** I call these interfaces implicit not because they're not explicitly called out in the specification -- they are! -- but because they're not exposed as direct objects accessible to code. JavaScript does not, in ES6, support any notion of "interfaces," so adherence for your own code is purely conventional. However, wherever JS expects an iterator -- a `for..of` loop, for instance -- what you provide must adhere to these interfaces or the code will fail. There's also an `Iterable` interface, which describes objects that must be able to produce iterators: ``` Iterable @@iterator() {method}: produces an Iterator ``` If you recall from "Built-In Symbols" in Chapter 2, `@@iterator` is the special built-in symbol representing the method that can produce iterator(s) for the object. #### IteratorResult The `IteratorResult` interface specifies that the return value from any iterator operation will be an object of the form: ```js { value: .. , done: true / false } ``` Built-in iterators will always return values of this form, but more properties are, of course, allowed to be present on the return value, as necessary. For example, a custom iterator may add additional metadata to the result object (e.g., where the data came from, how long it took to retrieve, cache expiration length, frequency for the appropriate next request, etc.). **Note:** Technically, `value` is optional if it would otherwise be considered absent or unset, such as in the case of the value `undefined`. Because accessing `res.value` will produce `undefined` whether it's present with that value or absent entirely, the presence/absence of the property is more an implementation detail or an optimization (or both), rather than a functional issue. ### `next()` Iteration Let's look at an array, which is an iterable, and the iterator it can produce to consume its values: ```js var arr = [1,2,3]; var it = arr[Symbol.iterator](); it.next(); // { value: 1, done: false } it.next(); // { value: 2, done: false } it.next(); // { value: 3, done: false } it.next(); // { value: undefined, done: true } ``` Each time the method located at `Symbol.iterator` (see Chapters 2 and 7) is invoked on this `arr` value, it will produce a new fresh iterator. Most structures will do the same, including all the built-in data structures in JS. However, a structure like an event queue consumer might only ever produce a single iterator (singleton pattern). Or a structure might only allow one unique iterator at a time, requiring the current one to be completed before a new one can be created. The `it` iterator in the previous snippet doesn't report `done: true` when you receive the `3` value. You have to call `next()` again, in essence going beyond the end of the array's values, to get the complete signal `done: true`. It may not be clear why until later in this section, but that design decision will typically be considered a best practice. Primitive string values are also iterables by default: ```js var greeting = "hello world"; var it = greeting[Symbol.iterator](); it.next(); // { value: "h", done: false } it.next(); // { value: "e", done: false } .. ``` **Note:** Technically, the primitive value itself isn't iterable, but thanks to "boxing", `"hello world"` is coerced/converted to its `String` object wrapper form, which *is* an iterable. See the *Types & Grammar* title of this series for more information. ES6 also includes several new data structures, called collections (see Chapter 5). These collections are not only iterables themselves, but they also provide API method(s) to generate an iterator, such as: ```js var m = new Map(); m.set( "foo", 42 ); m.set( { cool: true }, "hello world" ); var it1 = m[Symbol.iterator](); var it2 = m.entries(); it1.next(); // { value: [ "foo", 42 ], done: false } it2.next(); // { value: [ "foo", 42 ], done: false } .. ``` The `next(..)` method of an iterator can optionally take one or more arguments. The built-in iterators mostly do not exercise this capability, though a generator's iterator definitely does (see "Generators" later in this chapter). By general convention, including all the built-in iterators, calling `next(..)` on an iterator that's already been exhausted is not an error, but will simply continue to return the result `{ value: undefined, done: true }`. ### Optional: `return(..)` and `throw(..)` The optional methods on the iterator interface -- `return(..)` and `throw(..)` -- are not implemented on most of the built-in iterators. However, they definitely do mean something in the context of generators, so see "Generators" for more specific information. `return(..)` is defined as sending a signal to an iterator that the consuming code is complete and will not be pulling any more values from it. This signal can be used to notify the producer (the iterator responding to `next(..)` calls) to perform any cleanup it may need to do, such as releasing/closing network, database, or file handle resources. If an iterator has a `return(..)` present and any condition occurs that can automatically be interpreted as abnormal or early termination of consuming the iterator, `return(..)` will automatically be called. You can call `return(..)` manually as well. `return(..)` will return an `IteratorResult` object just like `next(..)` does. In general, the optional value you send to `return(..)` would be sent back as `value` in this `IteratorResult`, though there are nuanced cases where that might not be true. `throw(..)` is used to signal an exception/error to an iterator, which possibly may be used differently by the iterator than the completion signal implied by `return(..)`. It does not necessarily imply a complete stop of the iterator as `return(..)` generally does. For example, with generator iterators, `throw(..)` actually injects a thrown exception into the generator's paused execution context, which can be caught with a `try..catch`. An uncaught `throw(..)` exception would end up abnormally aborting the generator's iterator. **Note:** By general convention, an iterator should not produce any more results after having called `return(..)` or `throw(..)`. ### Iterator Loop As we covered in the "`for..of`" section in Chapter 2, the ES6 `for..of` loop directly consumes a conforming iterable. If an iterator is also an iterable, it can be used directly with the `for..of` loop. You make an iterator an iterable by giving it a `Symbol.iterator` method that simply returns the iterator itself: ```js var it = { // make the `it` iterator an iterable [Symbol.iterator]() { return this; }, next() { .. }, .. }; it[Symbol.iterator]() === it; // true ``` Now we can consume the `it` iterator with a `for..of` loop: ```js for (var v of it) { console.log( v ); } ``` To fully understand how such a loop works, recall the `for` equivalent of a `for..of` loop from Chapter 2: ```js for (var v, res; (res = it.next()) && !res.done; ) { v = res.value; console.log( v ); } ``` If you look closely, you'll see that `it.next()` is called before each iteration, and then `res.done` is consulted. If `res.done` is `true`, the expression evaluates to `false` and the iteration doesn't occur. Recall earlier that we suggested iterators should in general not return `done: true` along with the final intended value from the iterator. Now you can see why. If an iterator returned `{ done: true, value: 42 }`, the `for..of` loop would completely discard the `42` value and it'd be lost. For this reason, assuming that your iterator may be consumed by patterns like the `for..of` loop or its manual `for` equivalent, you should probably wait to return `done: true` for signaling completion until after you've already returned all relevant iteration values. **Warning:** You can, of course, intentionally design your iterator to return some relevant `value` at the same time as returning `done: true`. But don't do this unless you've documented that as the case, and thus implicitly forced consumers of your iterator to use a different pattern for iteration than is implied by `for..of` or its manual equivalent we depicted. ### Custom Iterators In addition to the standard built-in iterators, you can make your own! All it takes to make them interoperate with ES6's consumption facilities (e.g., the `for..of` loop and the `...` operator) is to adhere to the proper interface(s). Let's try constructing an iterator that produces the infinite series of numbers in the Fibonacci sequence: ```js var Fib = { [Symbol.iterator]() { var n1 = 1, n2 = 1; return { // make the iterator an iterable [Symbol.iterator]() { return this; }, next() { var current = n2; n2 = n1; n1 = n1 + current; return { value: current, done: false }; }, return(v) { console.log( "Fibonacci sequence abandoned." ); return { value: v, done: true }; } }; } }; for (var v of Fib) { console.log( v ); if (v > 50) break; } // 1 1 2 3 5 8 13 21 34 55 // Fibonacci sequence abandoned. ``` **Warning:** If we hadn't inserted the `break` condition, this `for..of` loop would have run forever, which is probably not the desired result in terms of breaking your program! The `Fib[Symbol.iterator]()` method when called returns the iterator object with `next()` and `return(..)` methods on it. State is maintained via `n1` and `n2` variables, which are kept by the closure. Let's *next* consider an iterator that is designed to run through a series (aka a queue) of actions, one item at a time: ```js var tasks = { [Symbol.iterator]() { var steps = this.actions.slice(); return { // make the iterator an iterable [Symbol.iterator]() { return this; }, next(...args) { if (steps.length > 0) { let res = steps.shift()( ...args ); return { value: res, done: false }; } else { return { done: true } } }, return(v) { steps.length = 0; return { value: v, done: true }; } }; }, actions: [] }; ``` The iterator on `tasks` steps through functions found in the `actions` array property, if any, and executes them one at a time, passing in whatever arguments you pass to `next(..)`, and returning any return value to you in the standard `IteratorResult` object. Here's how we could use this `tasks` queue: ```js tasks.actions.push( function step1(x){ console.log( "step 1:", x ); return x * 2; }, function step2(x,y){ console.log( "step 2:", x, y ); return x + (y * 2); }, function step3(x,y,z){ console.log( "step 3:", x, y, z ); return (x * y) + z; } ); var it = tasks[Symbol.iterator](); it.next( 10 ); // step 1: 10 // { value: 20, done: false } it.next( 20, 50 ); // step 2: 20 50 // { value: 120, done: false } it.next( 20, 50, 120 ); // step 3: 20 50 120 // { value: 1120, done: false } it.next(); // { done: true } ``` This particular usage reinforces that iterators can be a pattern for organizing functionality, not just data. It's also reminiscent of what we'll see with generators in the next section. You could even get creative and define an iterator that represents meta operations on a single piece of data. For example, we could define an iterator for numbers that by default ranges from `0` up to (or down to, for negative numbers) the number in question. Consider: ```js if (!Number.prototype[Symbol.iterator]) { Object.defineProperty( Number.prototype, Symbol.iterator, { writable: true, configurable: true, enumerable: false, value: function iterator(){ var i, inc, done = false, top = +this; // iterate positively or negatively? inc = 1 * (top < 0 ? -1 : 1); return { // make the iterator itself an iterable! [Symbol.iterator](){ return this; }, next() { if (!done) { // initial iteration always 0 if (i == null) { i = 0; } // iterating positively else if (top >= 0) { i = Math.min(top,i + inc); } // iterating negatively else { i = Math.max(top,i + inc); } // done after this iteration? if (i == top) done = true; return { value: i, done: false }; } else { return { done: true }; } } }; } } ); } ``` Now, what tricks does this creativity afford us? ```js for (var i of 3) { console.log( i ); } // 0 1 2 3 [...-3]; // [0,-1,-2,-3] ``` Those are some fun tricks, though the practical utility is somewhat debatable. But then again, one might wonder why ES6 didn't just ship with such a minor but delightful feature easter egg!? I'd be remiss if I didn't at least remind you that extending native prototypes as I'm doing in the previous snippet is something you should only do with caution and awareness of potential hazards. In this case, the chances that you'll have a collision with other code or even a future JS feature is probably exceedingly low. But just beware of the slight possibility. And document what you're doing verbosely for posterity's sake. **Note:** I've expounded on this particular technique in this blog post (http://blog.getify.com/iterating-es6-numbers/) if you want more details. And this comment (http://blog.getify.com/iterating-es6-numbers/comment-page-1/#comment-535294) even suggests a similar trick but for making string character ranges. ### Iterator Consumption We've already shown consuming an iterator item by item with the `for..of` loop. But there are other ES6 structures that can consume iterators. Let's consider the iterator attached to this array (though any iterator we choose would have the following behaviors): ```js var a = [1,2,3,4,5]; ``` The `...` spread operator fully exhausts an iterator. Consider: ```js function foo(x,y,z,w,p) { console.log( x + y + z + w + p ); } foo( ...a ); // 15 ``` `...` can also spread an iterator inside an array: ```js var b = [ 0, ...a, 6 ]; b; // [0,1,2,3,4,5,6] ``` Array destructuring (see "Destructuring" in Chapter 2) can partially or completely (if paired with a `...` rest/gather operator) consume an iterator: ```js var it = a[Symbol.iterator](); var [x,y] = it; // take just the first two elements from `it` var [z, ...w] = it; // take the third, then the rest all at once // is `it` fully exhausted? Yep. it.next(); // { value: undefined, done: true } x; // 1 y; // 2 z; // 3 w; // [4,5] ``` ## Generators All functions run to completion, right? In other words, once a function starts running, it finishes before anything else can interrupt. At least that's how it's been for the whole history of JavaScript up to this point. As of ES6, a new somewhat exotic form of function is being introduced, called a generator. A generator can pause itself in mid-execution, and can be resumed either right away or at a later time. So it clearly does not hold the run-to-completion guarantee that normal functions do. Moreover, each pause/resume cycle in mid-execution is an opportunity for two-way message passing, where the generator can return a value, and the controlling code that resumes it can send a value back in. As with iterators in the previous section, there are multiple ways to think about what a generator is, or rather what it's most useful for. There's no one right answer, but we'll try to consider several angles. **Note:** See the *Async & Performance* title of this series for more information about generators, and also see Chapter 4 of this current title. ### Syntax The generator function is declared with this new syntax: ```js function *foo() { // .. } ``` The position of the `*` is not functionally relevant. The same declaration could be written as any of the following: ```js function *foo() { .. } function* foo() { .. } function * foo() { .. } function*foo() { .. } .. ``` The *only* difference here is stylistic preference. Most other literature seems to prefer `function* foo(..) { .. }`. I prefer `function *foo(..) { .. }`, so that's how I'll present them for the rest of this title. My reason is purely didactic in nature. In this text, when referring to a generator function, I will use `*foo(..)`, as opposed to `foo(..)` for a normal function. I observe that `*foo(..)` more closely matches the `*` positioning of `function *foo(..) { .. }`. Moreover, as we saw in Chapter 2 with concise methods, there's a concise generator form in object literals: ```js var a = { *foo() { .. } }; ``` I would say that with concise generators, `*foo() { .. }` is rather more natural than `* foo() { .. }`. So that further argues for matching the consistency with `*foo()`. Consistency eases understanding and learning. #### Executing a Generator Though a generator is declared with `*`, you still execute it like a normal function: ```js foo(); ``` You can still pass it arguments, as in: ```js function *foo(x,y) { // .. } foo( 5, 10 ); ``` The major difference is that executing a generator, like `foo(5,10)` doesn't actually run the code in the generator. Instead, it produces an iterator that will control the generator to execute its code. We'll come back to this later in "Iterator Control," but briefly: ```js function *foo() { // .. } var it = foo(); // to start/advanced `*foo()`, call // `it.next(..)` ``` #### `yield` Generators also have a new keyword you can use inside them, to signal the pause point: `yield`. Consider: ```js function *foo() { var x = 10; var y = 20; yield; var z = x + y; } ``` In this `*foo()` generator, the operations on the first two lines would run at the beginning, then `yield` would pause the generator. If and when resumed, the last line of `*foo()` would run. `yield` can appear any number of times (or not at all, technically!) in a generator. You can even put `yield` inside a loop, and it can represent a repeated pause point. In fact, a loop that never completes just means a generator that never completes, which is completely valid, and sometimes entirely what you need. `yield` is not just a pause point. It's an expression that sends out a value when pausing the generator. Here's a `while..true` loop in a generator that for each iteration `yield`s a new random number: ```js function *foo() { while (true) { yield Math.random(); } } ``` The `yield ..` expression not only sends a value -- `yield` without a value is the same as `yield undefined` -- but also receives (e.g., is replaced by) the eventual resumption value. Consider: ```js function *foo() { var x = yield 10; console.log( x ); } ``` This generator will first `yield` out the value `10` when pausing itself. When you resume the generator -- using the `it.next(..)` we referred to earlier -- whatever value (if any) you resume with will replace/complete the whole `yield 10` expression, meaning that value will be assigned to the `x` variable. A `yield ..` expression can appear anywhere a normal expression can. For example: ```js function *foo() { var arr = [ yield 1, yield 2, yield 3 ]; console.log( arr, yield 4 ); } ``` `*foo()` here has four `yield ..` expressions. Each `yield` results in the generator pausing to wait for a resumption value that's then used in the various expression contexts. `yield` is not technically an operator, though when used like `yield 1` it sure looks like it. Because `yield` can be used all by itself as in `var x = yield;`, thinking of it as an operator can sometimes be confusing. Technically, `yield ..` is of the same "expression precedence" -- similar conceptually to operator precedence -- as an assignment expression like `a = 3`. That means `yield ..` can basically appear anywhere `a = 3` can validly appear. Let's illustrate the symmetry: ```js var a, b; a = 3; // valid b = 2 + a = 3; // invalid b = 2 + (a = 3); // valid yield 3; // valid a = 2 + yield 3; // invalid a = 2 + (yield 3); // valid ``` **Note:** If you think about it, it makes a sort of conceptual sense that a `yield ..` expression would behave similar to an assignment expression. When a paused `yield` expression is resumed, it's completed/replaced by the resumption value in a way that's not terribly dissimilar from being "assigned" that value. The takeaway: if you need `yield ..` to appear in a position where an assignment like `a = 3` would not itself be allowed, it needs to be wrapped in a `( )`. Because of the low precedence of the `yield` keyword, almost any expression after a `yield ..` will be computed first before being sent with `yield`. Only the `...` spread operator and the `,` comma operator have lower precedence, meaning they'd bind after the `yield` has been evaluated. So just like with multiple operators in normal statements, another case where `( )` might be needed is to override (elevate) the low precedence of `yield`, such as the difference between these expressions: ```js yield 2 + 3; // same as `yield (2 + 3)` (yield 2) + 3; // `yield 2` first, then `+ 3` ``` Just like `=` assignment, `yield` is also "right-associative," which means that multiple `yield` expressions in succession are treated as having been `( .. )` grouped from right to left. So, `yield yield yield 3` is treated as `yield (yield (yield 3))`. A "left-associative" interpretation like `((yield) yield) yield 3` would make no sense. Just like with operators, it's a good idea to use `( .. )` grouping, even if not strictly required, to disambiguate your intent if `yield` is combined with other operators or `yield`s. **Note:** See the *Types & Grammar* title of this series for more information about operator precedence and associativity. #### `yield *` In the same way that the `*` makes a `function` declaration into `function *` generator declaration, a `*` makes `yield` into `yield *`, which is a very different mechanism, called *yield delegation*. Grammatically, `yield *..` will behave the same as a `yield ..`, as discussed in the previous section. `yield * ..` requires an iterable; it then invokes that iterable's iterator, and delegates its own host generator's control to that iterator until it's exhausted. Consider: ```js function *foo() { yield *[1,2,3]; } ``` **Note:** As with the `*` position in a generator's declaration (discussed earlier), the `*` positioning in `yield *` expressions is stylistically up to you. Most other literature prefers `yield* ..`, but I prefer `yield *..`, for very symmetrical reasons as already discussed. The `[1,2,3]` value produces an iterator that will step through its values, so the `*foo()` generator will yield those values out as it's consumed. Another way to illustrate the behavior is in yield delegating to another generator: ```js function *foo() { yield 1; yield 2; yield 3; } function *bar() { yield *foo(); } ``` The iterator produced when `*bar()` calls `*foo()` is delegated to via `yield *`, meaning whatever value(s) `*foo()` produces will be produced by `*bar()`. Whereas with `yield ..` the completion value of the expression comes from resuming the generator with `it.next(..)`, the completion value of the `yield *..` expression comes from the return value (if any) from the delegated-to iterator. Built-in iterators generally don't have return values, as we covered at the end of the "Iterator Loop" section earlier in this chapter. But if you define your own custom iterator (or generator), you can design it to `return` a value, which `yield *..` would capture: ```js function *foo() { yield 1; yield 2; yield 3; return 4; } function *bar() { var x = yield *foo(); console.log( "x:", x ); } for (var v of bar()) { console.log( v ); } // 1 2 3 // x: { value: 4, done: true } ``` While the `1`, `2`, and `3` values are `yield`ed out of `*foo()` and then out of `*bar()`, the `4` value returned from `*foo()` is the completion value of the `yield *foo()` expression, which then gets assigned to `x`. Because `yield *` can call another generator (by way of delegating to its iterator), it can also perform a sort of generator recursion by calling itself: ```js function *foo(x) { if (x < 3) { x = yield *foo( x + 1 ); } return x * 2; } foo( 1 ); ``` The result from `foo(1)` and then calling the iterator's `next()` to run it through its recursive steps will be `24`. The first `*foo(..)` run has `x` at value `1`, which is `x < 3`. `x + 1` is passed recursively to `*foo(..)`, so `x` is then `2`. One more recursive call results in `x` of `3`. Now, because `x < 3` fails, the recursion stops, and `return 3 * 2` gives `6` back to the previous call's `yield *..` expression, which is then assigned to `x`. Another `return 6 * 2` returns `12` back to the previous call's `x`. Finally `12 * 2`, or `24`, is returned from the completed run of the `*foo(..)` generator. ### Iterator Control Earlier, we briefly introduced the concept that generators are controlled by iterators. Let's fully dig into that now. Recall the recursive `*foo(..)` from the previous section. Here's how we'd run it: ```js function *foo(x) { if (x < 3) { x = yield *foo( x + 1 ); } return x * 2; } var it = foo( 1 ); it.next(); // { value: 24, done: true } ``` In this case, the generator doesn't really ever pause, as there's no `yield ..` expression. Instead, `yield *` just keeps the current iteration step going via the recursive call. So, just one call to the iterator's `next()` function fully runs the generator. Now let's consider a generator that will have multiple steps and thus multiple produced values: ```js function *foo() { yield 1; yield 2; yield 3; } ``` We already know we can consume an iterator, even one attached to a generator like `*foo()`, with a `for..of` loop: ```js for (var v of foo()) { console.log( v ); } // 1 2 3 ``` **Note:** The `for..of` loop requires an iterable. A generator function reference (like `foo`) by itself is not an iterable; you must execute it with `foo()` to get the iterator (which is also an iterable, as we explained earlier in this chapter). You could theoretically extend the `GeneratorPrototype` (the prototype of all generator functions) with a `Symbol.iterator` function that essentially just does `return this()`. That would make the `foo` reference itself an iterable, which means `for (var v of foo) { .. }` (notice no `()` on `foo`) will work. Let's instead iterate the generator manually: ```js function *foo() { yield 1; yield 2; yield 3; } var it = foo(); it.next(); // { value: 1, done: false } it.next(); // { value: 2, done: false } it.next(); // { value: 3, done: false } it.next(); // { value: undefined, done: true } ``` If you look closely, there are three `yield` statements and four `next()` calls. That may seem like a strange mismatch. In fact, there will always be one more `next()` call than `yield` expression, assuming all are evaluated and the generator is fully run to completion. But if you look at it from the opposite perspective (inside-out instead of outside-in), the matching between `yield` and `next()` makes more sense. Recall that the `yield ..` expression will be completed by the value you resume the generator with. That means the argument you pass to `next(..)` completes whatever `yield ..` expression is currently paused waiting for a completion. Let's illustrate this perspective this way: ```js function *foo() { var x = yield 1; var y = yield 2; var z = yield 3; console.log( x, y, z ); } ``` In this snippet, each `yield ..` is sending a value out (`1`, `2`, `3`), but more directly, it's pausing the generator to wait for a value. In other words, it's almost like asking the question, "What value should I use here? I'll wait to hear back." Now, here's how we control `*foo()` to start it up: ```js var it = foo(); it.next(); // { value: 1, done: false } ``` That first `next()` call is starting up the generator from its initial paused state, and running it to the first `yield`. At the moment you call that first `next()`, there's no `yield ..` expression waiting for a completion. If you passed a value to that first `next()` call, it would currently just be thrown away, because no `yield` is waiting to receive such a value. **Note:** An early proposal for the "beyond ES6" timeframe *would* let you access a value passed to an initial `next(..)` call via a separate meta property (see Chapter 7) inside the generator. Now, let's answer the currently pending question, "What value should I assign to `x`?" We'll answer it by sending a value to the *next* `next(..)` call: ```js it.next( "foo" ); // { value: 2, done: false } ``` Now, the `x` will have the value `"foo"`, but we've also asked a new question, "What value should I assign to `y`?" And we answer: ```js it.next( "bar" ); // { value: 3, done: false } ``` Answer given, another question asked. Final answer: ```js it.next( "baz" ); // "foo" "bar" "baz" // { value: undefined, done: true } ``` Now it should be clearer how each `yield ..` "question" is answered by the *next* `next(..)` call, and so the "extra" `next()` call we observed is always just the initial one that starts everything going. Let's put all those steps together: ```js var it = foo(); // start up the generator it.next(); // { value: 1, done: false } // answer first question it.next( "foo" ); // { value: 2, done: false } // answer second question it.next( "bar" ); // { value: 3, done: false } // answer third question it.next( "baz" ); // "foo" "bar" "baz" // { value: undefined, done: true } ``` You can think of a generator as a producer of values, in which case each iteration is simply producing a value to be consumed. But in a more general sense, perhaps it's appropriate to think of generators as controlled, progressive code execution, much like the `tasks` queue example from the earlier "Custom Iterators" section. **Note:** That perspective is exactly the motivation for how we'll revisit generators in Chapter 4. Specifically, there's no reason that `next(..)` has to be called right away after the previous `next(..)` finishes. While the generator's inner execution context is paused, the rest of the program continues unblocked, including the ability for asynchronous actions to control when the generator is resumed. ### Early Completion As we covered earlier in this chapter, the iterator attached to a generator supports the optional `return(..)` and `throw(..)` methods. Both of them have the effect of aborting a paused generator immediately. Consider: ```js function *foo() { yield 1; yield 2; yield 3; } var it = foo(); it.next(); // { value: 1, done: false } it.return( 42 ); // { value: 42, done: true } it.next(); // { value: undefined, done: true } ``` `return(x)` is kind of like forcing a `return x` to be processed at exactly that moment, such that you get the specified value right back. Once a generator is completed, either normally or early as shown, it no longer processes any code or returns any values. In addition to `return(..)` being callable manually, it's also called automatically at the end of iteration by any of the ES6 constructs that consume iterators, such as the `for..of` loop and the `...` spread operator. The purpose for this capability is so the generator can be notified if the controlling code is no longer going to iterate over it anymore, so that it can perhaps do any cleanup tasks (freeing up resources, resetting status, etc.). Identical to a normal function cleanup pattern, the main way to accomplish this is to use a `finally` clause: ```js function *foo() { try { yield 1; yield 2; yield 3; } finally { console.log( "cleanup!" ); } } for (var v of foo()) { console.log( v ); } // 1 2 3 // cleanup! var it = foo(); it.next(); // { value: 1, done: false } it.return( 42 ); // cleanup! // { value: 42, done: true } ``` **Warning:** Do not put a `yield` statement inside the `finally` clause! It's valid and legal, but it's a really terrible idea. It acts in a sense as deferring the completion of the `return(..)` call you made, as any `yield ..` expressions in the `finally` clause are respected to pause and send messages; you don't immediately get a completed generator as expected. There's basically no good reason to opt in to that crazy *bad part*, so avoid doing so! In addition to the previous snippet showing how `return(..)` aborts the generator while still triggering the `finally` clause, it also demonstrates that a generator produces a whole new iterator each time it's called. In fact, you can use multiple iterators attached to the same generator concurrently: ```js function *foo() { yield 1; yield 2; yield 3; } var it1 = foo(); it1.next(); // { value: 1, done: false } it1.next(); // { value: 2, done: false } var it2 = foo(); it2.next(); // { value: 1, done: false } it1.next(); // { value: 3, done: false } it2.next(); // { value: 2, done: false } it2.next(); // { value: 3, done: false } it2.next(); // { value: undefined, done: true } it1.next(); // { value: undefined, done: true } ``` #### Early Abort Instead of calling `return(..)`, you can call `throw(..)`. Just like `return(x)` is essentially injecting a `return x` into the generator at its current pause point, calling `throw(x)` is essentially like injecting a `throw x` at the pause point. Other than the exception behavior (we cover what that means to `try` clauses in the next section), `throw(..)` produces the same sort of early completion that aborts the generator's run at its current pause point. For example: ```js function *foo() { yield 1; yield 2; yield 3; } var it = foo(); it.next(); // { value: 1, done: false } try { it.throw( "Oops!" ); } catch (err) { console.log( err ); // Exception: Oops! } it.next(); // { value: undefined, done: true } ``` Because `throw(..)` basically injects a `throw ..` in replacement of the `yield 1` line of the generator, and nothing handles this exception, it immediately propagates back out to the calling code, which handles it with a `try..catch`. Unlike `return(..)`, the iterator's `throw(..)` method is never called automatically. Of course, though not shown in the previous snippet, if a `try..finally` clause was waiting inside the generator when you call `throw(..)`, the `finally` clause would be given a chance to complete before the exception is propagated back to the calling code. ### Error Handling As we've already hinted, error handling with generators can be expressed with `try..catch`, which works in both inbound and outbound directions: ```js function *foo() { try { yield 1; } catch (err) { console.log( err ); } yield 2; throw "Hello!"; } var it = foo(); it.next(); // { value: 1, done: false } try { it.throw( "Hi!" ); // Hi! // { value: 2, done: false } it.next(); console.log( "never gets here" ); } catch (err) { console.log( err ); // Hello! } ``` Errors can also propagate in both directions through `yield *` delegation: ```js function *foo() { try { yield 1; } catch (err) { console.log( err ); } yield 2; throw "foo: e2"; } function *bar() { try { yield *foo(); console.log( "never gets here" ); } catch (err) { console.log( err ); } } var it = bar(); try { it.next(); // { value: 1, done: false } it.throw( "e1" ); // e1 // { value: 2, done: false } it.next(); // foo: e2 // { value: undefined, done: true } } catch (err) { console.log( "never gets here" ); } it.next(); // { value: undefined, done: true } ``` When `*foo()` calls `yield 1`, the `1` value passes through `*bar()` untouched, as we've already seen. But what's most interesting about this snippet is that when `*foo()` calls `throw "foo: e2"`, this error propagates to `*bar()` and is immediately caught by `*bar()`'s `try..catch` block. The error doesn't pass through `*bar()` like the `1` value did. `*bar()`'s `catch` then does a normal output of `err` (`"foo: e2"`) and then `*bar()` finishes normally, which is why the `{ value: undefined, done: true }` iterator result comes back from `it.next()`. If `*bar()` didn't have a `try..catch` around the `yield *..` expression, the error would of course propagate all the way out, and on the way through it still would complete (abort) `*bar()`. ### Transpiling a Generator Is it possible to represent a generator's capabilities prior to ES6? It turns out it is, and there are several great tools that do so, including most notably Facebook's Regenerator tool (https://facebook.github.io/regenerator/). But just to better understand generators, let's try our hand at manually converting. Basically, we're going to create a simple closure-based state machine. We'll keep our source generator really simple: ```js function *foo() { var x = yield 42; console.log( x ); } ``` To start, we'll need a function called `foo()` that we can execute, which needs to return an iterator: ```js function foo() { // .. return { next: function(v) { // .. } // we'll skip `return(..)` and `throw(..)` }; } ``` Now, we need some inner variable to keep track of where we are in the steps of our "generator"'s logic. We'll call it `state`. There will be three states: `0` initially, `1` while waiting to fulfill the `yield` expression, and `2` once the generator is complete. Each time `next(..)` is called, we need to process the next step, and then increment `state`. For convenience, we'll put each step into a `case` clause of a `switch` statement, and we'll hold that in an inner function called `nextState(..)` that `next(..)` can call. Also, because `x` is a variable across the overall scope of the "generator," it needs to live outside the `nextState(..)` function. Here it is all together (obviously somewhat simplified, to keep the conceptual illustration clearer): ```js function foo() { function nextState(v) { switch (state) { case 0: state++; // the `yield` expression return 42; case 1: state++; // `yield` expression fulfilled x = v; console.log( x ); // the implicit `return` return undefined; // no need to handle state `2` } } var state = 0, x; return { next: function(v) { var ret = nextState( v ); return { value: ret, done: (state == 2) }; } // we'll skip `return(..)` and `throw(..)` }; } ``` And finally, let's test our pre-ES6 "generator": ```js var it = foo(); it.next(); // { value: 42, done: false } it.next( 10 ); // 10 // { value: undefined, done: true } ``` Not bad, huh? Hopefully this exercise solidifies in your mind that generators are actually just simple syntax for state machine logic. That makes them widely applicable. ### Generator Uses So, now that we much more deeply understand how generators work, what are they useful for? We've seen two major patterns: * *Producing a series of values:* This usage can be simple (e.g., random strings or incremented numbers), or it can represent more structured data access (e.g., iterating over rows returned from a database query). Either way, we use the iterator to control a generator so that some logic can be invoked for each call to `next(..)`. Normal iterators on data structures merely pull values without any controlling logic. * *Queue of tasks to perform serially:* This usage often represents flow control for the steps in an algorithm, where each step requires retrieval of data from some external source. The fulfillment of each piece of data may be immediate, or may be asynchronously delayed. From the perspective of the code inside the generator, the details of sync or async at a `yield` point are entirely opaque. Moreover, these details are intentionally abstracted away, such as not to obscure the natural sequential expression of steps with such implementation complications. Abstraction also means the implementations can be swapped/refactored often without touching the code in the generator at all. When generators are viewed in light of these uses, they become a lot more than just a different or nicer syntax for a manual state machine. They are a powerful abstraction tool for organizing and controlling orderly production and consumption of data. ## Modules I don't think it's an exaggeration to suggest that the single most important code organization pattern in all of JavaScript is, and always has been, the module. For myself, and I think for a large cross-section of the community, the module pattern drives the vast majority of code. ### The Old Way The traditional module pattern is based on an outer function with inner variables and functions, and a returned "public API" with methods that have closure over the inner data and capabilities. It's often expressed like this: ```js function Hello(name) { function greeting() { console.log( "Hello " + name + "!" ); } // public API return { greeting: greeting }; } var me = Hello( "Kyle" ); me.greeting(); // Hello Kyle! ``` This `Hello(..)` module can produce multiple instances by being called subsequent times. Sometimes, a module is only called for as a singleton (i.e., it just needs one instance), in which case a slight variation on the previous snippet, using an IIFE, is common: ```js var me = (function Hello(name){ function greeting() { console.log( "Hello " + name + "!" ); } // public API return { greeting: greeting }; })( "Kyle" ); me.greeting(); // Hello Kyle! ``` This pattern is tried and tested. It's also flexible enough to have a wide assortment of variations for a number of different scenarios. One of the most common is the Asynchronous Module Definition (AMD), and another is the Universal Module Definition (UMD). We won't cover the particulars of these patterns and techniques here, but they're explained extensively in many places online. ### Moving Forward As of ES6, we no longer need to rely on the enclosing function and closure to provide us with module support. ES6 modules have first class syntactic and functional support. Before we get into the specific syntax, it's important to understand some fairly significant conceptual differences with ES6 modules compared to how you may have dealt with modules in the past: * ES6 uses file-based modules, meaning one module per file. At this time, there is no standardized way of combining multiple modules into a single file. That means that if you are going to load ES6 modules directly into a browser web application, you will be loading them individually, not as a large bundle in a single file as has been common in performance optimization efforts. It's expected that the contemporaneous advent of HTTP/2 will significantly mitigate any such performance concerns, as it operates on a persistent socket connection and thus can very efficiently load many smaller files in parallel and interleaved with one another. * The API of an ES6 module is static. That is, you define statically what all the top-level exports are on your module's public API, and those cannot be amended later. Some uses are accustomed to being able to provide dynamic API definitions, where methods can be added/removed/replaced in response to runtime conditions. Either these uses will have to change to fit with ES6 static APIs, or they will have to restrain the dynamic changes to properties/methods of a second-level object. * ES6 modules are singletons. That is, there's only one instance of the module, which maintains its state. Every time you import that module into another module, you get a reference to the one centralized instance. If you want to be able to produce multiple module instances, your module will need to provide some sort of factory to do it. * The properties and methods you expose on a module's public API are not just normal assignments of values or references. They are actual bindings (almost like pointers) to the identifiers in your inner module definition. In pre-ES6 modules, if you put a property on your public API that holds a primitive value like a number or string, that property assignment was by value-copy, and any internal update of a corresponding variable would be separate and not affect the public copy on the API object. With ES6, exporting a local private variable, even if it currently holds a primitive string/number/etc, exports a binding to the variable. If the module changes the variable's value, the external import binding now resolves to that new value. * Importing a module is the same thing as statically requesting it to load (if it hasn't already). If you're in a browser, that implies a blocking load over the network. If you're on a server (i.e., Node.js), it's a blocking load from the filesystem. However, don't panic about the performance implications. Because ES6 modules have static definitions, the import requirements can be statically scanned, and loads will happen preemptively, even before you've used the module. ES6 doesn't actually specify or handle the mechanics of how these load requests work. There's a separate notion of a Module Loader, where each hosting environment (browser, Node.js, etc.) provides a default Loader appropriate to the environment. The importing of a module uses a string value to represent where to get the module (URL, file path, etc.), but this value is opaque in your program and only meaningful to the Loader itself. You can define your own custom Loader if you want more fine-grained control than the default Loader affords -- which is basically none, as it's totally hidden from your program's code. As you can see, ES6 modules will serve the overall use case of organizing code with encapsulation, controlling public APIs, and referencing dependency imports. But they have a very particular way of doing so, and that may or may not fit very closely with how you've already been doing modules for years. #### CommonJS There's a similar, but not fully compatible, module syntax called CommonJS, which is familiar to those in the Node.js ecosystem. For lack of a more tactful way to say this, in the long run, ES6 modules essentially are bound to supersede all previous formats and standards for modules, even CommonJS, as they are built on syntactic support in the language. This will, in time, inevitably win out as the superior approach, if for no other reason than ubiquity. We face a fairly long road to get to that point, though. There are literally hundreds of thousands of CommonJS style modules in the server-side JavaScript world, and 10 times that many modules of varying format standards (UMD, AMD, ad hoc) in the browser world. It will take many years for the transitions to make any significant progress. In the interim, module transpilers/converters will be an absolute necessity. You might as well just get used to that new reality. Whether you author in regular modules, AMD, UMD, CommonJS, or ES6, these tools will have to parse and convert to a format that is suitable for whatever environment your code will run in. For Node.js, that probably means (for now) that the target is CommonJS. For the browser, it's probably UMD or AMD. Expect lots of flux on this over the next few years as these tools mature and best practices emerge. From here on out, my best advice on modules is this: whatever format you've been religiously attached to with strong affinity, also develop an appreciation for and understanding of ES6 modules, such as they are, and let your other module tendencies fade. They *are* the future of modules in JS, even if that reality is a bit of a ways off. ### The New Way The two main new keywords that enable ES6 modules are `import` and `export`. There's lots of nuance to the syntax, so let's take a deeper look. **Warning:** An important detail that's easy to overlook: both `import` and `export` must always appear in the top-level scope of their respective usage. For example, you cannot put either an `import` or `export` inside an `if` conditional; they must appear outside of all blocks and functions. #### `export`ing API Members The `export` keyword is either put in front of a declaration, or used as an operator (of sorts) with a special list of bindings to export. Consider: ```js export function foo() { // .. } export var awesome = 42; var bar = [1,2,3]; export { bar }; ``` Another way of expressing the same exports: ```js function foo() { // .. } var awesome = 42; var bar = [1,2,3]; export { foo, awesome, bar }; ``` These are all called *named exports*, as you are in effect exporting the name bindings of the variables/functions/etc. Anything you don't *label* with `export` stays private inside the scope of the module. That is, although something like `var bar = ..` looks like it's declaring at the top-level global scope, the top-level scope is actually the module itself; there is no global scope in modules. **Note:** Modules *do* still have access to `window` and all the "globals" that hang off it, just not as lexical top-level scope. However, you really should stay away from the globals in your modules if at all possible. You can also "rename" (aka alias) a module member during named export: ```js function foo() { .. } export { foo as bar }; ``` When this module is imported, only the `bar` member name is available to import; `foo` stays hidden inside the module. Module exports are not just normal assignments of values or references, as you're accustomed to with the `=` assignment operator. Actually, when you export something, you're exporting a binding (kinda like a pointer) to that thing (variable, etc.). Within your module, if you change the value of a variable you already exported a binding to, even if it's already been imported (see the next section), the imported binding will resolve to the current (updated) value. Consider: ```js var awesome = 42; export { awesome }; // later awesome = 100; ``` When this module is imported, regardless of whether that's before or after the `awesome = 100` setting, once that assignment has happened, the imported binding resolves to the `100` value, not `42`. That's because the binding is, in essence, a reference to, or a pointer to, the `awesome` variable itself, rather than a copy of its value. This is a mostly unprecedented concept for JS introduced with ES6 module bindings. Though you can clearly use `export` multiple times inside a module's definition, ES6 definitely prefers the approach that a module has a single export, which is known as a *default export*. In the words of some members of the TC39 committee, you're "rewarded with simpler `import` syntax" if you follow that pattern, and conversely "penalized" with more verbose syntax if you don't. A default export sets a particular exported binding to be the default when importing the module. The name of the binding is literally `default`. As you'll see later, when importing module bindings you can also rename them, as you commonly will with a default export. There can only be one `default` per module definition. We'll cover `import` in the next section, and you'll see how the `import` syntax is more concise if the module has a default export. There's a subtle nuance to default export syntax that you should pay close attention to. Compare these two snippets: ```js function foo(..) { // .. } export default foo; ``` And this one: ```js function foo(..) { // .. } export { foo as default }; ``` In the first snippet, you are exporting a binding to the function expression value at that moment, *not* to the identifier `foo`. In other words, `export default ..` takes an expression. If you later assign `foo` to a different value inside your module, the module import still reveals the function originally exported, not the new value. By the way, the first snippet could also have been written as: ```js export default function foo(..) { // .. } ``` **Warning:** Although the `function foo..` part here is technically a function expression, for the purposes of the internal scope of the module, it's treated like a function declaration, in that the `foo` name is bound in the module's top-level scope (often called "hoisting"). The same is true for `export default class Foo..`. However, while you *can* do `export var foo = ..`, you currently cannot do `export default var foo = ..` (or `let` or `const`), in a frustrating case of inconsistency. At the time of this writing, there's already discussion of adding that capability in soon, post-ES6, for consistency sake. Recall the second snippet again: ```js function foo(..) { // .. } export { foo as default }; ``` In this version of the module export, the default export binding is actually to the `foo` identifier rather than its value, so you get the previously described binding behavior (i.e., if you later change `foo`'s value, the value seen on the import side will also be updated). Be very careful of this subtle gotcha in default export syntax, especially if your logic calls for export values to be updated. If you never plan to update a default export's value, `export default ..` is fine. If you do plan to update the value, you must use `export { .. as default }`. Either way, make sure to comment your code to explain your intent! Because there can only be one `default` per module, you may be tempted to design your module with one default export of an object with all your API methods on it, such as: ```js export default { foo() { .. }, bar() { .. }, .. }; ``` That pattern seems to map closely to how a lot of developers have already structured their pre-ES6 modules, so it seems like a natural approach. Unfortunately, it has some downsides and is officially discouraged. In particular, the JS engine cannot statically analyze the contents of a plain object, which means it cannot do some optimizations for static `import` performance. The advantage of having each member individually and explicitly exported is that the engine *can* do the static analysis and optimization. If your API has more than one member already, it seems like these principles -- one default export per module, and all API members as named exports -- are in conflict, doesn't it? But you *can* have a single default export as well as other named exports; they are not mutually exclusive. So, instead of this (discouraged) pattern: ```js export default function foo() { .. } foo.bar = function() { .. }; foo.baz = function() { .. }; ``` You can do: ```js export default function foo() { .. } export function bar() { .. } export function baz() { .. } ``` **Note:** In this previous snippet, I used the name `foo` for the function that `default` labels. That `foo` name, however, is ignored for the purposes of export -- `default` is actually the exported name. When you import this default binding, you can give it whatever name you want, as you'll see in the next section. Alternatively, some will prefer: ```js function foo() { .. } function bar() { .. } function baz() { .. } export { foo as default, bar, baz, .. }; ``` The effects of mixing default and named exports will be more clear when we cover `import` shortly. But essentially it means that the most concise default import form would only retrieve the `foo()` function. The user could additionally manually list `bar` and `baz` as named imports, if they want them. You can probably imagine how tedious that's going to be for consumers of your module if you have lots of named export bindings. There is a wildcard import form where you import all of a module's exports within a single namespace object, but there's no way to wildcard import to top-level bindings. Again, the ES6 module mechanism is intentionally designed to discourage modules with lots of exports; relatively speaking, it's desired that such approaches be a little more difficult, as a sort of social engineering to encourage simple module design in favor of large/complex module design. I would probably recommend you not mix default export with named exports, especially if you have a large API and refactoring to separate modules isn't practical or desired. In that case, just use all named exports, and document that consumers of your module should probably use the `import * as ..` (namespace import, discussed in the next section) approach to bring the whole API in at once on a single namespace. We mentioned this earlier, but let's come back to it in more detail. Other than the `export default ...` form that exports an expression value binding, all other export forms are exporting bindings to local identifiers. For those bindings, if you change the value of a variable inside a module after exporting, the external imported binding will access the updated value: ```js var foo = 42; export { foo as default }; export var bar = "hello world"; foo = 10; bar = "cool"; ``` When you import this module, the `default` and `bar` exports will be bound to the local variables `foo` and `bar`, meaning they will reveal the updated `10` and `"cool"` values. The values at time of export are irrelevant. The values at time of import are irrelevant. The bindings are live links, so all that matters is what the current value is when you access the binding. **Warning:** Two-way bindings are not allowed. If you import a `foo` from a module, and try to change the value of your imported `foo` variable, an error will be thrown! We'll revisit that in the next section. You can also re-export another module's exports, such as: ```js export { foo, bar } from "baz"; export { foo as FOO, bar as BAR } from "baz"; export * from "baz"; ``` Those forms are similar to just first importing from the `"baz"` module then listing its members explicitly for export from your module. However, in these forms, the members of the `"baz"` module are never imported to your module's local scope; they sort of pass through untouched. #### `import`ing API Members To import a module, unsurprisingly you use the `import` statement. Just as `export` has several nuanced variations, so does `import`, so spend plenty of time considering the following issues and experimenting with your options. If you want to import certain specific named members of a module's API into your top-level scope, you use this syntax: ```js import { foo, bar, baz } from "foo"; ``` **Warning:** The `{ .. }` syntax here may look like an object literal, or even an object destructuring syntax. However, its form is special just for modules, so be careful not to confuse it with other `{ .. }` patterns elsewhere. The `"foo"` string is called a *module specifier*. Because the whole goal is statically analyzable syntax, the module specifier must be a string literal; it cannot be a variable holding the string value. From the perspective of your ES6 code and the JS engine itself, the contents of this string literal are completely opaque and meaningless. The module loader will interpret this string as an instruction of where to find the desired module, either as a URL path or a local filesystem path. The `foo`, `bar`, and `baz` identifiers listed must match named exports on the module's API (static analysis and error assertion apply). They are bound as top-level identifiers in your current scope: ```js import { foo } from "foo"; foo(); ``` You can rename the bound identifiers imported, as: ```js import { foo as theFooFunc } from "foo"; theFooFunc(); ``` If the module has just a default export that you want to import and bind to an identifier, you can opt to skip the `{ .. }` surrounding syntax for that binding. The `import` in this preferred case gets the nicest and most concise of the `import` syntax forms: ```js import foo from "foo"; // or: import { default as foo } from "foo"; ``` **Note:** As explained in the previous section, the `default` keyword in a module's `export` specifies a named export where the name is actually `default`, as is illustrated by the second more verbose syntax option. The renaming from `default` to, in this case, `foo`, is explicit in the latter syntax and is identical yet implicit in the former syntax. You can also import a default export along with other named exports, if the module has such a definition. Recall this module definition from earlier: ```js export default function foo() { .. } export function bar() { .. } export function baz() { .. } ``` To import that module's default export and its two named exports: ```js import FOOFN, { bar, baz as BAZ } from "foo"; FOOFN(); bar(); BAZ(); ``` The strongly suggested approach from ES6's module philosophy is that you only import the specific bindings from a module that you need. If a module provides 10 API methods, but you only need two of them, some believe it wasteful to bring in the entire set of API bindings. One benefit, besides code being more explicit, is that narrow imports make static analysis and error detection (accidentally using the wrong binding name, for instance) more robust. Of course, that's just the standard position influenced by ES6 design philosophy; there's nothing that requires adherence to that approach. Many developers would be quick to point out that such approaches can be more tedious, requiring you to regularly revisit and update your `import` statement(s) each time you realize you need something else from a module. The trade-off is in exchange for convenience. In that light, the preference might be to import everything from the module into a single namespace, rather than importing individual members, each directly into the scope. Fortunately, the `import` statement has a syntax variation that can support this style of module consumption, called *namespace import*. Consider a `"foo"` module exported as: ```js export function bar() { .. } export var x = 42; export function baz() { .. } ``` You can import that entire API to a single module namespace binding: ```js import * as foo from "foo"; foo.bar(); foo.x; // 42 foo.baz(); ``` **Note:** The `* as ..` clause requires the `*` wildcard. In other words, you cannot do something like `import { bar, x } as foo from "foo"` to bring in only part of the API but still bind to the `foo` namespace. I would have liked something like that, but for ES6 it's all or nothing with the namespace import. If the module you're importing with `* as ..` has a default export, it is named `default` in the namespace specified. You can additionally name the default import outside of the namespace binding, as a top-level identifier. Consider a `"world"` module exported as: ```js export default function foo() { .. } export function bar() { .. } export function baz() { .. } ``` And this `import`: ```js import foofn, * as hello from "world"; foofn(); hello.default(); hello.bar(); hello.baz(); ``` While this syntax is valid, it can be rather confusing that one method of the module (the default export) is bound at the top-level of your scope, whereas the rest of the named exports (and one called `default`) are bound as properties on a differently named (`hello`) identifier namespace. As I mentioned earlier, my suggestion would be to avoid designing your module exports in this way, to reduce the chances that your module's users will suffer these strange quirks. All imported bindings are immutable and/or read-only. Consider the previous import; all of these subsequent assignment attempts will throw `TypeError`s: ```js import foofn, * as hello from "world"; foofn = 42; // (runtime) TypeError! hello.default = 42; // (runtime) TypeError! hello.bar = 42; // (runtime) TypeError! hello.baz = 42; // (runtime) TypeError! ``` Recall earlier in the "`export`ing API Members" section that we talked about how the `bar` and `baz` bindings are bound to the actual identifiers inside the `"world"` module. That means if the module changes those values, `hello.bar` and `hello.baz` now reference the updated values. But the immutable/read-only nature of your local imported bindings enforces that you cannot change them from the imported bindings, hence the `TypeError`s. That's pretty important, because without those protections, your changes would end up affecting all other consumers of the module (remember: singleton), which could create some very surprising side effects! Moreover, though a module *can* change its API members from the inside, you should be very cautious of intentionally designing your modules in that fashion. ES6 modules are *intended* to be static, so deviations from that principle should be rare and should be carefully and verbosely documented. **Warning:** There are module design philosophies where you actually intend to let a consumer change the value of a property on your API, or module APIs are designed to be "extended" by having other "plug-ins" add to the API namespace. As we just asserted, ES6 module APIs should be thought of and designed as static and unchangeable, which strongly restricts and discourages these alternative module design patterns. You can get around these limitations by exporting a plain object, which of course can then be changed at will. But be careful and think twice before going down that road. Declarations that occur as a result of an `import` are "hoisted" (see the *Scope & Closures* title of this series). Consider: ```js foo(); import { foo } from "foo"; ``` `foo()` can run because not only did the static resolution of the `import ..` statement figure out what `foo` is during compilation, but it also "hoisted" the declaration to the top of the module's scope, thus making it available throughout the module. Finally, the most basic form of the `import` looks like this: ```js import "foo"; ``` This form does not actually import any of the module's bindings into your scope. It loads (if not already loaded), compiles (if not already compiled), and evaluates (if not already run) the `"foo"` module. In general, that sort of import is probably not going to be terribly useful. There may be niche cases where a module's definition has side effects (such as assigning things to the `window`/global object). You could also envision using `import "foo"` as a sort of preload for a module that may be needed later. ### Circular Module Dependency A imports B. B imports A. How does this actually work? I'll state off the bat that designing systems with intentional circular dependency is generally something I try to avoid. That having been said, I recognize there are reasons people do this and it can solve some sticky design situations. Let's consider how ES6 handles this. First, module `"A"`: ```js import bar from "B"; export default function foo(x) { if (x > 10) return bar( x - 1 ); return x * 2; } ``` Now, module `"B"`: ```js import foo from "A"; export default function bar(y) { if (y > 5) return foo( y / 2 ); return y * 3; } ``` These two functions, `foo(..)` and `bar(..)`, would work as standard function declarations if they were in the same scope, because the declarations are "hoisted" to the whole scope and thus available to each other regardless of authoring order. With modules, you have declarations in entirely different scopes, so ES6 has to do extra work to help make these circular references work. In a rough conceptual sense, this is how circular `import` dependencies are validated and resolved: * If the `"A"` module is loaded first, the first step is to scan the file and analyze all the exports, so it can register all those bindings available for import. Then it processes the `import .. from "B"`, which signals that it needs to go fetch `"B"`. * Once the engine loads `"B"`, it does the same analysis of its export bindings. When it sees the `import .. from "A"`, it knows the API of `"A"` already, so it can verify the `import` is valid. Now that it knows the `"B"` API, it can also validate the `import .. from "B"` in the waiting `"A"` module. In essence, the mutual imports, along with the static verification that's done to validate both `import` statements, virtually composes the two separate module scopes (via the bindings), such that `foo(..)` can call `bar(..)` and vice versa. This is symmetric to if they had originally been declared in the same scope. Now let's try using the two modules together. First, we'll try `foo(..)`: ```js import foo from "foo"; foo( 25 ); // 11 ``` Or we can try `bar(..)`: ```js import bar from "bar"; bar( 25 ); // 11.5 ``` By the time either the `foo(25)` or `bar(25)` calls are executed, all the analysis/compilation of all modules has completed. That means `foo(..)` internally knows directly about `bar(..)` and `bar(..)` internally knows directly about `foo(..)`. If all we need is to interact with `foo(..)`, then we only need to import the `"foo"` module. Likewise with `bar(..)` and the `"bar"` module. Of course, we *can* import and use both of them if we want to: ```js import foo from "foo"; import bar from "bar"; foo( 25 ); // 11 bar( 25 ); // 11.5 ``` The static loading semantics of the `import` statement mean that a `"foo"` and `"bar"` that mutually depend on each other via `import` will ensure that both are loaded, parsed, and compiled before either of them runs. So their circular dependency is statically resolved and this works as you'd expect. ### Module Loading We asserted at the beginning of this "Modules" section that the `import` statement uses a separate mechanism, provided by the hosting environment (browser, Node.js, etc.), to actually resolve the module specifier string into some useful instruction for finding and loading the desired module. That mechanism is the system *Module Loader*. The default module loader provided by the environment will interpret a module specifier as a URL if in the browser, and (generally) as a local filesystem path if on a server such as Node.js. The default behavior is to assume the loaded file is authored in the ES6 standard module format. Moreover, you will be able to load a module into the browser via an HTML tag, similar to how current script programs are loaded. At the time of this writing, it's not fully clear if this tag will be `` elements in the page that load these files separately, and even a few inline-code `` elements as well. But do these separate files/code snippets constitute separate programs or are they collectively one JS program? The (perhaps surprising) reality is they act more like independent JS programs in most, but not all, respects. The one thing they *share* is the single `global` object (`window` in the browser), which means multiple files can append their code to that shared namespace and they can all interact. So, if one `script` element defines a global function `foo()`, when a second `script` later runs, it can access and call `foo()` just as if it had defined the function itself. But global variable scope *hoisting* (see the *Scope & Closures* title of this series) does not occur across these boundaries, so the following code would not work (because `foo()`'s declaration isn't yet declared), regardless of if they are (as shown) inline `` elements or externally loaded `` files: ```html ``` But either of these *would* work instead: ```html ``` Or: ```html ``` Also, if an error occurs in a `script` element (inline or external), as a separate standalone JS program it will fail and stop, but any subsequent `script`s will run (still with the shared `global`) unimpeded. You can create `script` elements dynamically from your code, and inject them into the DOM of the page, and the code in them will behave basically as if loaded normally in a separate file: ```js var greeting = "Hello World"; var el = document.createElement( "script" ); el.text = "function foo(){ alert( greeting );\ } setTimeout( foo, 1000 );"; document.body.appendChild( el ); ``` **Note:** Of course, if you tried the above snippet but set `el.src` to some file URL instead of setting `el.text` to the code contents, you'd be dynamically creating an externally loaded `` element. One difference between code in an inline code block and that same code in an external file is that in the inline code block, the sequence of characters `` cannot appear together, as (regardless of where it appears) it would be interpreted as the end of the code block. So, beware of code like: ```html "; ``` It looks harmless, but the `` appearing inside the `string` literal will terminate the script block abnormally, causing an error. The most common workaround is: ```js ""; ``` Also, beware that code inside an external file will be interpreted in the character set (UTF-8, ISO-8859-8, etc.) the file is served with (or the default), but that same code in an inline `script` element in your HTML page will be interpreted by the character set of the page (or its default). **Warning:** The `charset` attribute will not work on inline script elements. Another deprecated practice with inline `script` elements is including HTML-style or X(HT)ML-style comments around inline code, like: ```html ``` Both of these are totally unnecessary now, so if you're still doing that, stop it! **Note:** Both `` (HTML-style comments) are actually specified as valid single-line comment delimiters (`var x = 2; another valid line comment`) in JavaScript (see the "Web ECMAScript" section earlier), purely because of this old technique. But never use them. ## Reserved Words The ES5 spec defines a set of "reserved words" in Section 7.6.1 that cannot be used as standalone variable names. Technically, there are four categories: "keywords", "future reserved words", the `null` literal, and the `true` / `false` boolean literals. Keywords are the obvious ones like `function` and `switch`. Future reserved words include things like `enum`, though many of the rest of them (`class`, `extends`, etc.) are all now actually used by ES6; there are other strict-mode only reserved words like `interface`. StackOverflow user "art4theSould" creatively worked all these reserved words into a fun little poem (http://stackoverflow.com/questions/26255/reserved-keywords-in-javascript/12114140#12114140): > Let this long package float, > Goto private class if short. > While protected with debugger case, > Continue volatile interface. > Instanceof super synchronized throw, > Extends final export throws. > > Try import double enum? > - False, boolean, abstract function, > Implements typeof transient break! > Void static, default do, > Switch int native new. > Else, delete null public var > In return for const, true, char > …Finally catch byte. **Note:** This poem includes words that were reserved in ES3 (`byte`, `long`, etc.) that are no longer reserved as of ES5. Prior to ES5, the reserved words also could not be property names or keys in object literals, but that restriction no longer exists. So, this is not allowed: ```js var import = "42"; ``` But this is allowed: ```js var obj = { import: "42" }; console.log( obj.import ); ``` You should be aware though that some older browser versions (mainly older IE) weren't completely consistent on applying these rules, so there are places where using reserved words in object property name locations can still cause issues. Carefully test all supported browser environments. ## Implementation Limits The JavaScript spec does not place arbitrary limits on things such as the number of arguments to a function or the length of a string literal, but these limits exist nonetheless, because of implementation details in different engines. For example: ```js function addAll() { var sum = 0; for (var i=0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } var nums = []; for (var i=1; i < 100000; i++) { nums.push(i); } addAll( 2, 4, 6 ); // 12 addAll.apply( null, nums ); // should be: 499950000 ``` In some JS engines, you'll get the correct `499950000` answer, but in others (like Safari 6.x), you'll get the error: "RangeError: Maximum call stack size exceeded." Examples of other limits known to exist: * maximum number of characters allowed in a string literal (not just a string value) * size (bytes) of data that can be sent in arguments to a function call (aka stack size) * number of parameters in a function declaration * maximum depth of non-optimized call stack (i.e., with recursion): how long a chain of function calls from one to the other can be * number of seconds a JS program can run continuously blocking the browser * maximum length allowed for a variable name * ... It's not very common at all to run into these limits, but you should be aware that limits can and do exist, and importantly that they vary between engines. ## Review We know and can rely upon the fact that the JS language itself has one standard and is predictably implemented by all the modern browsers/engines. This is a very good thing! But JavaScript rarely runs in isolation. It runs in an environment mixed in with code from third-party libraries, and sometimes it even runs in engines/environments that differ from those found in browsers. Paying close attention to these issues improves the reliability and robustness of your code. ================================================ FILE: types & grammar/apB.md ================================================ # Вы не знаете JS: Типы и грамматика # Приложение B: Благодарности Есть множество людей, которых нужно поблагодарить за то, что появилась на свет эта книга и вся серия. Во-первых, я должен поблагодарить мою жену Кристен Симпсон (Christen Simpson) и двух моих детей Итана (Ethan) и Эмили (Emily), за то, что мирились с тем, что их папу вечно надо было отрывать от компьютера. Даже когда я не писал книги, моя одержимость JavaScript приклеивала мой взгляд к экрану больше, чем следовало. То время, которое я занял у моей семьи, и есть причина, по которой эти книги могут так глубоко и полностью объяснить JavaScript для вас, читатель. Я в большом долгу перед своей семьей. Хочу поблагодарить моих редакторов в O'Reilly, а именно Simon St.Laurent и Brian MacDonald, как и остальных в команде редакторов и маркетинга. Работать с ними — одно удовольствие, и хочется особенно поблагодарить их за создание подходящих условий во время этого эксперимента с написанием "open source"-книги, за редактирование и выпуск. Спасибо всем тем, кто участвовал в улучшении этой серии книг присылая предложения по редактированию и корректировке, включая Shelley Powers, Tim Ferro, Evan Borden, Forrest L. Norvell, Jennifer Davis, Jesse Harlin и многих других. Большое спасибо Shane Hudson за написание предисловия к этой серии книг. Спасибо бесчисленным участникам сообщества, включая членов комитета TC39, кто поделился столько многим с нами и особенно за терпеливое отношение к моим бесконечным вопросам и изысканиям. John-David Dalton, Juriy "kangax" Zaytsev, Mathias Bynens, Axel Rauschmayer, Nicholas Zakas, Angus Croll, Reginald Braithwaite, Dave Herman, Brendan Eich, Allen Wirfs-Brock, Bradley Meck, Domenic Denicola, David Walsh, Tim Disney, Peter van der Zee, Andrea Giammarchi, Kit Cambridge, Eric Elliott и многие другие, я упомянул лишь малую часть. Серия книг *Вы не знаете JS* родилась на Kickstarter, поэтому я также хочу поблагодарить всех моих (почти) 500 щедрых инвесторов, без которых эта серия книг не появилась бы: > Jan Szpila, nokiko, Murali Krishnamoorthy, Ryan Joy, Craig Patchett, pdqtrader, Dale Fukami, ray hatfield, R0drigo Perez [Mx], Dan Petitt, Jack Franklin, Andrew Berry, Brian Grinstead, Rob Sutherland, Sergi Meseguer, Phillip Gourley, Mark Watson, Jeff Carouth, Alfredo Sumaran, Martin Sachse, Marcio Barrios, Dan, AimelyneM, Matt Sullivan, Delnatte Pierre-Antoine, Jake Smith, Eugen Tudorancea, Iris, David Trinh, simonstl, Ray Daly, Uros Gruber, Justin Myers, Shai Zonis, Mom & Dad, Devin Clark, Dennis Palmer, Brian Panahi Johnson, Josh Marshall, Marshall, Dennis Kerr, Matt Steele, Erik Slagter, Sacah, Justin Rainbow, Christian Nilsson, Delapouite, D.Pereira, Nicolas Hoizey, George V. Reilly, Dan Reeves, Bruno Laturner, Chad Jennings, Shane King, Jeremiah Lee Cohick, od3n, Stan Yamane, Marko Vucinic, Jim B, Stephen Collins, Ægir Þorsteinsson, Eric Pederson, Owain, Nathan Smith, Jeanetteurphy, Alexandre ELISÉ, Chris Peterson, Rik Watson, Luke Matthews, Justin Lowery, Morten Nielsen, Vernon Kesner, Chetan Shenoy, Paul Tregoing, Marc Grabanski, Dion Almaer, Andrew Sullivan, Keith Elsass, Tom Burke, Brian Ashenfelter, David Stuart, Karl Swedberg, Graeme, Brandon Hays, John Christopher, Gior, manoj reddy, Chad Smith, Jared Harbour, Minoru TODA, Chris Wigley, Daniel Mee, Mike, Handyface, Alex Jahraus, Carl Furrow, Rob Foulkrod, Max Shishkin, Leigh Penny Jr., Robert Ferguson, Mike van Hoenselaar, Hasse Schougaard, rajan venkataguru, Jeff Adams, Trae Robbins, Rolf Langenhuijzen, Jorge Antunes, Alex Koloskov, Hugh Greenish, Tim Jones, Jose Ochoa, Michael Brennan-White, Naga Harish Muvva, Barkóczi Dávid, Kitt Hodsden, Paul McGraw, Sascha Goldhofer, Andrew Metcalf, Markus Krogh, Michael Mathews, Matt Jared, Juanfran, Georgie Kirschner, Kenny Lee, Ted Zhang, Amit Pahwa, Inbal Sinai, Dan Raine, Schabse Laks, Michael Tervoort, Alexandre Abreu, Alan Joseph Williams, NicolasD, Cindy Wong, Reg Braithwaite, LocalPCGuy, Jon Friskics, Chris Merriman, John Pena, Jacob Katz, Sue Lockwood, Magnus Johansson, Jeremy Crapsey, Grzegorz Pawłowski, nico nuzzaci, Christine Wilks, Hans Bergren, charles montgomery, Ariel בר-לבב Fogel, Ivan Kolev, Daniel Campos, Hugh Wood, Christian Bradford, Frédéric Harper, Ionuţ Dan Popa, Jeff Trimble, Rupert Wood, Trey Carrico, Pancho Lopez, Joël kuijten, Tom A Marra, Jeff Jewiss, Jacob Rios, Paolo Di Stefano, Soledad Penades, Chris Gerber, Andrey Dolganov, Wil Moore III, Thomas Martineau, Kareem, Ben Thouret, Udi Nir, Morgan Laupies, jory carson-burson, Nathan L Smith, Eric Damon Walters, Derry Lozano-Hoyland, Geoffrey Wiseman, mkeehner, KatieK, Scott MacFarlane, Brian LaShomb, Adrien Mas, christopher ross, Ian Littman, Dan Atkinson, Elliot Jobe, Nick Dozier, Peter Wooley, John Hoover, dan, Martin A. Jackson, Héctor Fernando Hurtado, andy ennamorato, Paul Seltmann, Melissa Gore, Dave Pollard, Jack Smith, Philip Da Silva, Guy Israeli, @megalithic, Damian Crawford, Felix Gliesche, April Carter Grant, Heidi, jim tierney, Andrea Giammarchi, Nico Vignola, Don Jones, Chris Hartjes, Alex Howes, john gibbon, David J. Groom, BBox, Yu 'Dilys' Sun, Nate Steiner, Brandon Satrom, Brian Wyant, Wesley Hales, Ian Pouncey, Timothy Kevin Oxley, George Terezakis, sanjay raj, Jordan Harband, Marko McLion, Wolfgang Kaufmann, Pascal Peuckert, Dave Nugent, Markus Liebelt, Welling Guzman, Nick Cooley, Daniel Mesquita, Robert Syvarth, Chris Coyier, Rémy Bach, Adam Dougal, Alistair Duggin, David Loidolt, Ed Richer, Brian Chenault, GoldFire Studios, Carles Andrés, Carlos Cabo, Yuya Saito, roberto ricardo, Barnett Klane, Mike Moore, Kevin Marx, Justin Love, Joe Taylor, Paul Dijou, Michael Kohler, Rob Cassie, Mike Tierney, Cody Leroy Lindley, tofuji, Shimon Schwartz, Raymond, Luc De Brouwer, David Hayes, Rhys Brett-Bowen, Dmitry, Aziz Khoury, Dean, Scott Tolinski - Level Up, Clement Boirie, Djordje Lukic, Anton Kotenko, Rafael Corral, Philip Hurwitz, Jonathan Pidgeon, Jason Campbell, Joseph C., SwiftOne, Jan Hohner, Derick Bailey, getify, Daniel Cousineau, Chris Charlton, Eric Turner, David Turner, Joël Galeran, Dharma Vagabond, adam, Dirk van Bergen, dave ♥♫★ furf, Vedran Zakanj, Ryan McAllen, Natalie Patrice Tucker, Eric J. Bivona, Adam Spooner, Aaron Cavano, Kelly Packer, Eric J, Martin Drenovac, Emilis, Michael Pelikan, Scott F. Walter, Josh Freeman, Brandon Hudgeons, vijay chennupati, Bill Glennon, Robin R., Troy Forster, otaku_coder, Brad, Scott, Frederick Ostrander, Adam Brill, Seb Flippence, Michael Anderson, Jacob, Adam Randlett, Standard, Joshua Clanton, Sebastian Kouba, Chris Deck, SwordFire, Hannes Papenberg, Richard Woeber, hnzz, Rob Crowther, Jedidiah Broadbent, Sergey Chernyshev, Jay-Ar Jamon, Ben Combee, luciano bonachela, Mark Tomlinson, Kit Cambridge, Michael Melgares, Jacob Adams, Adrian Bruinhout, Bev Wieber, Scott Puleo, Thomas Herzog, April Leone, Daniel Mizieliński, Kees van Ginkel, Jon Abrams, Erwin Heiser, Avi Laviad, David newell, Jean-Francois Turcot, Niko Roberts, Erik Dana, Charles Neill, Aaron Holmes, Grzegorz Ziółkowski, Nathan Youngman, Timothy, Jacob Mather, Michael Allan, Mohit Seth, Ryan Ewing, Benjamin Van Treese, Marcelo Santos, Denis Wolf, Phil Keys, Chris Yung, Timo Tijhof, Martin Lekvall, Agendine, Greg Whitworth, Helen Humphrey, Dougal Campbell, Johannes Harth, Bruno Girin, Brian Hough, Darren Newton, Craig McPheat, Olivier Tille, Dennis Roethig, Mathias Bynens, Brendan Stromberger, sundeep, John Meyer, Ron Male, John F Croston III, gigante, Carl Bergenhem, B.J. May, Rebekah Tyler, Ted Foxberry, Jordan Reese, Terry Suitor, afeliz, Tom Kiefer, Darragh Duffy, Kevin Vanderbeken, Andy Pearson, Simon Mac Donald, Abid Din, Chris Joel, Tomas Theunissen, David Dick, Paul Grock, Brandon Wood, John Weis, dgrebb, Nick Jenkins, Chuck Lane, Johnny Megahan, marzsman, Tatu Tamminen, Geoffrey Knauth, Alexander Tarmolov, Jeremy Tymes, Chad Auld, Sean Parmelee, Rob Staenke, Dan Bender, Yannick derwa, Joshua Jones, Geert Plaisier, Tom LeZotte, Christen Simpson, Stefan Bruvik, Justin Falcone, Carlos Santana, Michael Weiss, Pablo Villoslada, Peter deHaan, Dimitris Iliopoulos, seyDoggy, Adam Jordens, Noah Kantrowitz, Amol M, Matthew Winnard, Dirk Ginader, Phinam Bui, David Rapson, Andrew Baxter, Florian Bougel, Michael George, Alban Escalier, Daniel Sellers, Sasha Rudan, John Green, Robert Kowalski, David I. Teixeira (@ditma, Charles Carpenter, Justin Yost, Sam S, Denis Ciccale, Kevin Sheurs, Yannick Croissant, Pau Fracés, Stephen McGowan, Shawn Searcy, Chris Ruppel, Kevin Lamping, Jessica Campbell, Christopher Schmitt, Sablons, Jonathan Reisdorf, Bunni Gek, Teddy Huff, Michael Mullany, Michael Fürstenberg, Carl Henderson, Rick Yoesting, Scott Nichols, Hernán Ciudad, Andrew Maier, Mike Stapp, Jesse Shawl, Sérgio Lopes, jsulak, Shawn Price, Joel Clermont, Chris Ridmann, Sean Timm, Jason Finch, Aiden Montgomery, Elijah Manor, Derek Gathright, Jesse Harlin, Dillon Curry, Courtney Myers, Diego Cadenas, Arne de Bree, João Paulo Dubas, James Taylor, Philipp Kraeutli, Mihai Păun, Sam Gharegozlou, joshjs, Matt Murchison, Eric Windham, Timo Behrmann, Andrew Hall, joshua price, Théophile Villard Эта серия книг выпускается в стиле "open source", включая редактирование и выпуск. Мы отдаем дань благодарности GitHub за предоставление такой возможности для сообщества! Еще раз спасибо всех бесчисленным людям, которых я не перечислил по имени, но кого я тем не менее должен поблагодарить. Пусть эта серия книг будет "принадлежать" всем нам и служить вкладом в увеличение информированности и понимании языка JavaScript, на благо все нынешних и будущих вкладчиков в общее дело сообщества. ================================================ FILE: types & grammar/ch1.md ================================================ # Вы не знаете JS: Типы и грамматика # Глава 1: Типы Большинство разработчиков скажут, что динамический язык (как JS) не имеет *типов*. Посмотрим, что спецификация ES5.1 (http://www.ecma-international.org/ecma-262/5.1/) говорит об этом: > Алгоритмы в этой спецификации управляют значениями, каждое из которых имеет связанный с ним тип. Возможные типы значений — это именно те, которые определены в этом разделе. Далее типы подразделяются на типы языка ECMAScript и типы спецификации. > > Тип языка ECMAScript соответствует значениям, которые непосредственно обрабатываются программистом ECMAScript с использованием языка ECMAScript. Типы языка ECMAScript это: Undefined, Null, Boolean, String, Number и Object. Если вы поклонник строго типизированных (статически типизированных) языков, вы можете возражать против этого использования слова «тип». В этих языках «тип» означает гораздо *больше*, чем здесь, в JS. Некоторые люди говорят, что JS не должен претендовать на то, что имеет «типы», и их следует вместо этого называть «тегами» или, допустим, «подтипами». Фух! Мы будем использовать вот это грубое определение (то же самое, что, похоже, подразумевает формулировка из спецификации): *тип* — это встроенный набор характеристик, который однозначно идентифицирует поведение конкретного значения и отличает его от других значений, как для движка, **так и для разработчика**. Другими словами, если и движок, и разработчик обрабатывают значение `42` (число) иначе, чем они обрабатывают значение `"42"` (строка), то эти два значения имеют разные *типы* — `number` и `string`, соответственно. Когда вы используете `42`, вы *собираетесь* делать что-то числовое, например, математику. Но когда вы используете `"42"`, вы *собираетесь* делать что-то строковое, например, выводить на страницу и т.д. **Эти два значения имеют разные типы.** Это далеко не идеальное определение. Но оно достаточно хорошее для обсуждения. И согласуется с тем, как JS описывает себя. # Тип, как бы он ни назывался... За пределами академических разногласий в определениях, почему важно, имеет JavaScript *типы* или нет? Наличие правильного понимания каждого *типа* и его внутреннего поведения абсолютно необходимо для понимания того, как правильно и точно преобразовывать значения в разные типы (см. «Приведение типов», глава 4). Почти каждая программа на JS, когда-либо написанная, должна будет обрабатывать приведение типов значений в каком-то виде или форме, поэтому важно, чтобы вы делали это ответственно и уверенно. Если у вас есть значение `42` типа `number`, но вы хотите обрабатывать его как `string`, например, вытаскивая `"2"` в качестве символа в позиции `1`, вы, очевидно, должны сначала преобразовать (привести) значение из `number` к `string`. Это кажется довольно простым. Но существует много разных способов такого приведения. Некоторые из них ясны, понятны и надёжны. Но если вы не будете осторожны, приведение может произойти очень странным и непредсказуемым образом. Путаница с приведением, возможно, является одним из самых глубоких разочарований для разработчиков JavaScript. Его часто критикуют за то, что оно настолько *опасно*, что считается недостатком в устройстве языка, которого следует сторониться и избегать. Вооружившись полным пониманием типов JavaScript, мы попытаемся проиллюстрировать, почему *плохая репутация* приведения в значительной степени раздута и несколько незаслуженна — чтобы взглянуть на вещи с другой точки зрения, увидеть силу и полезность приведения. Но во-первых, мы должны получше разобраться со значениями и типами. ## Встроенные типы В JavaScript определены семь встроенных типов: * `null` * `undefined` * `boolean` * `number` * `string` * `object` * `symbol` — добавлен в ES6! **Примечание:** Все эти типы, кроме `object`, называют "примитивами". Оператор `typeof` проверяет тип заданного значения и всегда возвращает одно из семи строковых значений — и как ни странно, они не имеют точного соответствия с семью встроенными типами, которые мы только что перечислили. ```js typeof undefined === "undefined"; // true typeof true === "boolean"; // true typeof 42 === "number"; // true typeof "42" === "string"; // true typeof { life: 42 } === "object"; // true // добавлен в ES6! typeof Symbol() === "symbol"; // true ``` Эти шесть перечисленных типов имеют значения соответствующего типа и возвращают строковое значение с таким именем, как показано в примере. `Symbol` — это новый тип данных из ES6, он будет рассмотрен в главе 3. Как вы могли заметить, я исключил `null` из приведённого выше списка. Он *особенный* — особенный в том смысле, что в сочетании с оператором `typeof` он глючит: ```js typeof null === "object"; // true ``` Было бы неплохо (и правильно!), если бы вернулось `"null"`, но эта оригинальная ошибка в JS сохраняется почти два десятилетия и, скорее всего, никогда не будет исправлена, потому что существует настолько много кода в вебе, который полагается на это глючное поведение, что «исправление» ошибки *создало бы* больше «ошибок» и сломало бы много веб-программ. Если вы хотите проверить значение `null`, используя его тип, вам потребуется составное условие: ```js var a = null; (!a && typeof a === "object"); // true ``` `null` — единственное примитивное значение, которое является «ложным» (или «подобным ложному», см. главу 4), но также возвращает «объект» из проверки типа. Так каково же седьмое строковое значение, возвращаемое `typeof`? ```js typeof function a(){ /* .. */ } === "function"; // true ``` Легко решить, что `function` это встроенный тип верхнего уровня в JS, особенно учитывая такое поведение оператора `typeof`. Однако, если вы прочитаете спецификацию, вы увидите, что это фактически «подтип» объекта. В частности, функцию называют «вызываемым объектом» — объектом, который имеет внутреннее свойство `[[Call]]`, которое позволяет ему вызываться. Тот факт, что функции фактически являются объектами, весьма полезен. Самое главное, они могут иметь свойства. Например: ```js function a(b,c) { /* .. */ } ``` Объект функции имеет свойство `length`, определяемое числом формальных параметров, с которыми он был объявлен. ```js a.length; // 2 ``` Поскольку вы объявили функцию с двумя формальными именованными параметрами (`b` и `c`), «длина функции» равна `2`. Что насчёт массивов? В JS они нативные, так особый ли это тип? ```js typeof [1,2,3] === "object"; // true ``` Нет, это просто объекты. Наиболее целесообразно думать о них также как о «подтипе» объектов (см. Главу 3), в данном случае — с дополнительными характеристиками численного индексирования (в отличие от просто строковых ключей, как у обычных объектов) и поддержкой автоматически обновляемого свойства `.length`. ## Значения как типы В JavaScript переменные не имеют типов — **значения имеют типы**. Переменные могут хранить любое значение в любой момент времени. Другими словами, JS не имеет «принудительного применения типа», поскольку движок не настаивает на том, чтобы *переменная* всегда содержала значение *того же начального типа*, с которого инициализировалась. Переменная может в одном операторе присваивания содержать `string`, а в следующем — `number` и т.д. *Значение* `42` имеет встроенный тип `number`, и этот *тип* не может быть изменён. Другое значение, например, `"42"` с типом `string`, может быть получено *из* значения `42` типа `number` с помощью **приведения типов** (см. Главу 4). Если вы используете `typeof` для переменной, он не спрашивает: «Какой тип у этой переменной?», как может показаться, поскольку переменные в JS не имеют типов. Вместо этого он спрашивает: «Какой тип значения *в* этой переменной?». ```js var a = 42; typeof a; // "number" a = true; typeof a; // "boolean" ``` Оператор `typeof` всегда возвращает строку. Поэтому: ```js typeof typeof 42; // "string" ``` Сначала `typeof 42` возвращает `"number"`, и `typeof "number"` это `"string"`. ### Неопределённые и необъявленные переменные Переменные, которые не имеют значения *в данный момент*, фактически имеют значение `undefined`. Вызов `typeof` для таких переменных возвращает `"undefined"`: ```js var a; typeof a; // "undefined" var b = 42; var c; // далее b = c; typeof b; // "undefined" typeof c; // "undefined" ``` Для большинства разработчиков удобно думать о слове «undefined» («неопределённый») как о синониме слова «undeclared» («необъявленный»). Однако в JS две эти концепции — совершенно разные. «Неопределённая» переменная — это та, которая была объявлена в доступной области видимости, но *в данный момент* не имеющая значения. В отличие от этого, «необъявленная» переменная — это та, которая не была объявлена в доступной области видимости. Посмотрите внимательно: ```js var a; a; // undefined b; // ReferenceError: b is not defined ``` Досадную путаницу создаёт сообщение об ошибке, которое браузеры назначают этой ситуации. Как вы можете видеть, сообщение — «b is not defined» («b не определено»), которое, конечно, очень легко и логично спутать с «b is undefined» («b неопределено»). Ещё раз: «неопределено» и «не определено» — это совершенно разные вещи. Было бы неплохо, если бы браузеры писали что-то вроде «b не найдено» или «b не объявлено», чтобы уменьшить путаницу! Существует также особое поведение у `typeof`, связанное с необъявленными переменными, которое ещё больше усиливает путаницу. Смотрите: ```js var a; typeof a; // "undefined" typeof b; // "undefined" ``` Оператор `typeof` возвращает `"undefined"` даже для «необъявленных» (или «не определённых») переменных. Обратите внимание, что при выполнении `typeof b` ошибки не возникло, хотя `b` является необъявленной переменной. Это особая мера безопасности в поведении `typeof`. Как и в примере выше, было бы неплохо, если бы `typeof`, используемый с необъявленной переменной, возвращал «undeclared» («необъявлено») вместо объединения результата с другой ситуацией, когда переменная «undefined» («неопределена»). ### `typeof` для необъявленных переменных Тем не менее, эта мера безопасности является полезной функцией при работе с JavaScript в браузере, где несколько файлов скриптов могут загружать переменные в общее глобальное пространство имён. **Примечание:** Многие разработчики считают, что в глобальном пространстве имён никогда не должно быть никаких переменных, и что всё должно содержаться в модулях и закрытых/отдельных пространствах имён. Это прекрасно в теории, но практически невозможно на практике; хотя всё же это хорошая цель, к которой нужно стремиться! К счастью, ES6 добавил первоклассную поддержку модулей, что в конечном итоге сделает это гораздо более удобным. В качестве простого примера представьте, что в вашей программе есть «режим отладки», который управляется глобальной переменной (флагом) под названием `DEBUG`. Вы хотите проверить, была ли объявлена эта переменная перед выполнением задачи отладки, например, вывода сообщения в консоль. Глобальное объявление `var DEBUG = true` верхнего уровня будет включено только в файл «debug.js», который вы загружаете в браузер только тогда, когда находитесь в режиме разработки/тестирования, но не для продакшна. Однако вы должны позаботиться о том, как вы проверяете глобальную переменную `DEBUG` в остальной части кода своего приложения, чтобы не получить `ReferenceError`. В этом случае безопасность в `typeof` станет вашим другом. ```js // упс, здесь будет ошибка! if (DEBUG) { console.log( "Debugging is starting" ); } // это безопасная проверка на наличие if (typeof DEBUG !== "undefined") { console.log( "Debugging is starting" ); } ``` Этот вид проверки полезен, даже если вы работаете не с переменными, определяемыми пользователем (как `DEBUG`). Если вы выполняете проверку функционала для встроенного API, для вас также может быть полезна проверка без выброса ошибки: ```js if (typeof atob === "undefined") { atob = function() { /*..*/ }; } ``` **Примечание:** Если вы определяете полифилл для функционала, которого ещё не существует, вы, вероятно, захотите избежать использования `var`, чтобы объявить `atob`. Если вы объявляете `var atob` внутри оператора `if`, это объявление всплывёт (см. книгу *Область видимости и замыкания* из этой серии) в верхнюю часть области видимости, даже если условие `if` не выполнится (потому что глобальная `atob` уже существует!). В некоторых браузерах и для некоторых специальных типов глобальных встроенных переменных (часто называемых «host-объектами») это дублирующее объявление может вызвать ошибку. Опущение `var` предотвращает это объявление от всплытия. Другой способ выполнения этих проверок глобальных переменных, но без защиты `typeof` — это увидеть, что все глобальные переменные также являются свойствами глобального объекта, который в браузере обычно является объектом `window`. Таким образом, вышеуказанные проверки могли быть выполнены (совершенно безопасно) вот так: ```js if (window.DEBUG) { // .. } if (!window.atob) { // .. } ``` В отличие от ссылок на необъявленные переменные, при попытке доступа к свойству объекта (даже глобального объекта `window`), ошибки `ReferenceError` выброшено не будет. С другой стороны, ручное обращение к глобальной переменной с помощью `window` — это то, чего некоторые разработчики предпочитают избегать, особенно если ваш код должен запускаться в нескольких JS-средах (не только в браузерах, но и в серверном node.js, например), где глобальная переменная не всегда называется `window`. Технически, эта защита в `typeof` полезна, даже если вы не используете глобальные переменные, хотя такие условия более редки, и некоторые разработчики могут найти этот подход к стилю кода менее желательным. Представьте себе функцию-утилиту, которая предназначена вами для копирования и вставки в чужие программы или модули, в которой вы хотите проверить, определила ли основная программа какую-то переменную (чтобы вы могли её использовать) или нет: ```js function doSomethingCool() { var helper = (typeof FeatureXYZ !== "undefined") ? FeatureXYZ : function() { /*.. функция по умолчанию ..*/ }; var val = helper(); // .. } ``` `doSomethingCool()` проверяет переменную с именем `FeatureXYZ`, и если она найдена, использует её, но если нет, то использует свою собственную. Теперь, если кто-то включает эту утилиту в свой модуль/программу, она надёжно проверяет, определена `FeatureXYZ` или нет: ```js // IIFE (см. обсуждение "Immediately Invoked Function Expressions" // ("Немедленно вызываемых функций") в книге *Область видимости и замыкания* из этой серии) (function(){ function FeatureXYZ() { /*.. моя функция XYZ ..*/ } // подключаем `doSomethingCool(..)` function doSomethingCool() { var helper = (typeof FeatureXYZ !== "undefined") ? FeatureXYZ : function() { /*.. функция по умолчанию ..*/ }; var val = helper(); // .. } doSomethingCool(); })(); ``` Здесь `FeatureXYZ` — вовсе не глобальная переменная, но мы по-прежнему используем защиту `typeof`, чтобы сделать проверку безопасной. И что важно, здесь *нет* объекта, который мы можем использовать (как мы делали для глобальных переменных с `window.___`), чтобы сделать проверку, поэтому `typeof` весьма полезен. Другие разработчики предпочли бы шаблон проектирования, называемый «внедрение зависимости», где вместо неявной проверки в `doSomethingCool()`, объявлена ли `FeatureXYZ` вне её, нужно явно передать зависимость, например: ```js function doSomethingCool(FeatureXYZ) { var helper = FeatureXYZ || function() { /*.. функция по умолчанию ..*/ }; var val = helper(); // .. } ``` Существует множество вариантов реализации такого функционала. Ни один шаблон здесь не является «правильным» или «неправильным» — существуют разные нюансы у каждого подхода. Но в целом, приятно, что защита `typeof` для необъявленных переменных даёт нам больше возможностей. ## Обзор В JavaScript есть семь встроенных *типов*: `null`, `undefined`, `boolean`, `number`, `string`, `object`, `symbol`. Их можно определить с помощью оператора `typeof`. Переменные не имеют типов, но их имеют значения переменных. Эти типы определяют внутреннее поведение значений. Многие разработчики полагают, что «неопределённый» и «необъявленный» — это примерно одно и то же, но в JavaScript это совершенно разные вещи. `undefined` («неопределённый») — это значение, которое может содержать объявленная переменная. «Undeclared» («необъявленный») означает, что переменная не была объявлена. JavaScript, к сожалению, в некотором роде отождествляет эти два термина, не только в сообщениях об ошибках («ReferenceError: a is not defined»), но также и в значении, возвращаемом `typeof`, являющемся `"undefined"` для обоих случаев. Однако защита (предотвращение ошибки) в `typeof` при использовании с необъявленной переменной может быть полезна в некоторых случаях. ================================================ FILE: types & grammar/ch2.md ================================================ # Вы не знаете JS: Типы и грамматика # Глава 2: Типы `array`, `string`, и `number` являются основными составными элементами любой программы, но в JavaScript, при работе с этими типами данных, есть несколько особенностей, которые могут смутить или запутать вас. Давайте посмотрим на несколько встроенных типов JS, и разберемся как мы можем полностью понять и корректно использовать их поведение. ## Массивы Если сравнивать с другими строго типизированными языками, в JavaScript массивы - всего лишь контейнеры для любых типов значений, начиная от `string` до `number`, `object` и даже других `array` (с помощью которых можно создавать многомерные массивы). ```js var a = [ 1, "2", [3] ]; a.length; // 3 a[0] === 1; // true a[2][0] === 3; // true ``` Вам не нужно предварительно устанавливать размер `array` (подробнее в "Массивы" Глава 3), вы можете просто объявить их и добавлять значения когда вам нужно: ```js var a = [ ]; a.length; // 0 a[0] = 1; a[1] = "2"; a[2] = [ 3 ]; a.length; // 3 ``` **Предупреждение:** Используя `delete` для значения `array` будет удалена ячейка `array` с этим значением, но даже если вы удалите последний элемент таким способом, это **НЕ** обновит свойство `length`, так что будьте осторожны! Работа оператора `delete` более детально будет рассмотрена в Главе 5. Будьте осторожны при создании "разрозненных" массивов (оставляя или создавая пустые/пропущенные ячейки): ```js var a = [ ]; a[0] = 1; // ячейка `a[1]` отсутствует a[2] = [ 3 ]; a[1]; // undefined a.length; // 3 ``` Такой код может привести к странному поведению "пустых ячеек" оставленных между элементами массива. Пустой слот со значением `undefined` внутри, ведет себя не так же как явно объявленный элемент массива (`a[1] = undefined`). Подробнее в главе 3 "Массивы". Массивы `array`s проиндексированы числами (как и ожидается), но хитрость в том, что они также являются объектами, которые могут иметь `string` ключи/свойства, добавленные к ним (но такие свойства не будут посчитаны в длине массива `length`): ```js var a = [ ]; a[0] = 1; a["foobar"] = 2; a.length; // 1 a["foobar"]; // 2 a.foobar; // 2 ``` Как бы там ни было, нужно быть осторожнее при использовании индексов массива в виде `string`, т.к. это значение может быть преобразовано в тип `number`, потому что использование индекса `number` для массива предпочтительнее чем `string`! ```js var a = [ ]; a["13"] = 42; a.length; // 14 ``` В общем, это не самая лучшая идея использовать пару `string` ключ/свойство как элемент массива `array`. Используйте `object` для хранения пар ключ/свойство, а массивы `array`s приберегите для хранения значений в ячейках с числовыми индексами. ### Массивоподобные Бывают случаи когда нужно преобразовать массивоподобное значение (пронумерованную коллекцию значений) в настоящий массив `array`, обычно таким образом вы сможете применить методы массива (такие как `indexOf(..)`, `concat(..)`, `forEach(..)`, etc.) к коллекции значений. Например, различные DOM запросы возвращают список DOM элементов который не является настоящим массивом `array`, но, при этом он достаточно похож на массив для преобразования. Другой общеизвестный пример - когда функция предоставляет свои аргументы `arguments` в виде массивоподобного объекта (в ES6, считается устаревшим), чтобы получить доступ к списку аргументов. Один из самых распространенных способов осуществить такое преобразование одолжить метод `slice(..)` для значения: ```js function foo() { var arr = Array.prototype.slice.call( arguments ); arr.push( "bam" ); console.log( arr ); } foo( "bar", "baz" ); // ["bar","baz","bam"] ``` Если `slice()` вызван без каких-либо параметров, как в примере выше, стандартные значения его параметров позволят продублировать массив `array` (а в нашем случае, массивоподобное значение). В ES6, есть встроенный метод `Array.from(..)` который при вызове выполнит то же самое: ```js ... var arr = Array.from( arguments ); ... ``` **Примечание:** `Array.from(..)` имеет несколько мощных возможностей, детально о них рассказано в книге *ES6 и не только* данной серии. ## Строки Есть общее мнение, что строки `string` являются всего лишь массивами `array` из символов. Пока мы решаем можно или нельзя использовать `array`, важно осознавать что JavaScript `string` на самом деле не то же самое что массивы `array` символов. Это сходство по большей части поверхностное. Например, давайте сравним два значения: ```js var a = "foo"; var b = ["f","o","o"]; ``` Строки имеют поверхностные сходства по отношению к массивам -- и массивоподобным, как выше -- например, оба из них имеют свойство `length`, метод `indexOf(..)` (`array` только в ES5), и метод `concat(..)`: ```js a.length; // 3 b.length; // 3 a.indexOf( "o" ); // 1 b.indexOf( "o" ); // 1 var c = a.concat( "bar" ); // "foobar" var d = b.concat( ["b","a","r"] ); // ["f","o","o","b","a","r"] a === c; // false b === d; // false a; // "foo" b; // ["f","o","o"] ``` Итак, строки по большей части это "массивы символов", верно? **НЕ совсем**: ```js a[1] = "O"; b[1] = "O"; a; // "foo" b; // ["f","O","o"] ``` В JavaScript строки `string` неизменяемы, тогда как массивы `array` достаточно изменяемы. Более того форма доступа к символу строки вида `a[1]` не совсем правильный JavaScript. Старые версии IE не разрешают такой синтаксис (в новых версиях IE это работает). Вместо него нужно использовать *корректный* способ - `a.charAt(1)`. Еще одним последствием неизменяемости строк `string` является то что ни один метод строки `string` меняющий ее содержимое не может делать это по месту, скорее метод создаст и вернет новые строки. И напротив, большинство методов изменяющих содержимое массива `array` действительно *делают* изменения по месту. ```js c = a.toUpperCase(); a === c; // false a; // "foo" c; // "FOO" b.push( "!" ); b; // ["f","O","o","!"] ``` Также многие из методов массива `array`, которые могут быть полезны при работе со строками `string` вообще для них недоступны, но мы можем "одолжить" не изменяющие методы массива `array` для нашей строки `string`: ```js a.join; // undefined a.map; // undefined var c = Array.prototype.join.call( a, "-" ); var d = Array.prototype.map.call( a, function(v){ return v.toUpperCase() + "."; } ).join( "" ); c; // "f-o-o" d; // "F.O.O." ``` Давайте возьмем другой пример: реверсируем строку `string` (кстати, это довольно тривиальный общий вопрос на JavaScript собеседованиях!). У массивов `array` есть метод `reverse()` осуществляющий изменение по месту, но для строки `string` такого метода нет: ```js a.reverse; // undefined b.reverse(); // ["!","o","O","f"] b; // ["!","o","O","f"] ``` К несчастью, это "одалживание" не сработает с методами изменяющими массив `array`, потому что строки `string` неизменяемы и поэтому не могут быть изменены по месту: ```js Array.prototype.reverse.call( a ); // все еще возвращаем объект-обертку String (подробнее в Главе 3) // для "foo" :( ``` Другое временное решение (хак) отконвертировать строку `string` в массив `array`, выполнить желаемое действие, и затем отконвертировать обратно в строку `string`. ```js var c = a // разбиваем `a` на массив символов .split( "" ) // реверсируем массив символов .reverse() // объединяем массив символов обратно в строку .join( "" ); c; // "oof" ``` Если кажется, что это выглядит безобразно, так и есть. Тем не менее, *это работает* для простых строк `string`, так что, если вам нужно "склепать" что-нибудь по-быстрому, часто такой подход позволит выполнить работу. **Предупреждение:** Будьте осторожны! Этот подход **не работает** для строк `string` со сложными (unicode) символами в них (astral symbols, multibyte characters, etc.). Вам потребуются более сложные библиотеки которые распознают unicode символы для правильного выполнения подобных операций. Подробнее можно посмотреть в работе Mathias Bynens': *Esrever* (https://github.com/mathiasbynens/esrever). Хотя с другой стороны: если вы чаще работаете с вашими "строками", интерпретируя их как *массивы символов*, возможно лучше просто записывать их в массив `array` вместо строк `string`. Возможно вы избавите себя от хлопот при переводе строки `string` в массив `array` каждый раз. Вы всегда можете вызвать `join("")` для массива `array` *символов* когда вам понадобится представление в виде строки `string`. ## Числа В JavaScript есть один числовой тип: `number`. Этот тип включает в себя как "целые" ("integer") значения так и десятичные дробные числа. Я заключил "целые" ("integer") в кавычки, потому что в JS это понятие подвергается критике, поскольку здесь нет реально целых значений, как в других языках программирования. Возможно в будущем это изменится, но сейчас, у нас просто есть тип `number` для всего. Итак, в JS, "целое" ("integer") это просто числовое значение, которое не имеет десятичной составляющей после запятой . Так например, `42.0` более может считаться "целым" ("integer"), чем `42`. Как и в большинстве современных языков, включая практически все скриптовые языки, реализация чисел `number` в JavaScript основана на стандарте "IEEE 754", который часто называют "числа с плавающей точкой" ("floating-point"). JavaScript особенно использует формат "двойной степени точности" (как "64-битные в бинарном формате") этого стандарта. В интернете есть множество статей о подробных деталях того, как бинарные числа с плавающей точкой записываются в память, и последствия выбора таких чисел. Т.к. понимание того как работает запись в память не строго необходимо для того чтобы корректно использовать числа `number` в JS, мы оставим это упражнение для заинтересованного читателя, если вы захотите более детально разобраться со стандартом IEEE 754. ### Числовой синтаксис Числовые литералы в JavaScript в большинстве представлены как литералы десятичных дробей. Например: ```js var a = 42; var b = 42.3; ``` Если целая часть дробного числа - `0`, можно ее опустить: ```js var a = 0.42; var b = .42; ``` Аналогично, если дробная часть после точки `.`, - `0`, можно ее опустить: ```js var a = 42.0; var b = 42.; ``` **Предупреждение:** `42.` выглядит достаточно необычно, и возможно это не лучшая идея если вы хотите избежать недопонимания со стороны других людей при работе с вашим кодом. Но, в любом случае, это корректная запись. По умолчанию, большинство чисел `number` выводятся как десятичные дроби, с удаленными нулями `0` в конце дробной части. Так: ```js var a = 42.300; var b = 42.0; a; // 42.3 b; // 42 ``` Очень большие или очень маленькие числа `number` по умолчанию выводятся в экспоненциальной форме, также как и результат метода `toExponential()`, например: ```js var a = 5E10; a; // 50000000000 a.toExponential(); // "5e+10" var b = a * a; b; // 2.5e+21 var c = 1 / a; c; // 2e-11 ``` Т.к. числовые значения `number` могут быть помещены в объект - обертку `Number` (подробнее Глава 3), числовые значения `number` могут получать методы встроенные в `Number.prototype` (подробнее Глава 3). Например, метод `toFixed(..)` позволяет вам определить с точностью до скольких знаков после запятой вывести дробную часть: ```js var a = 42.59; a.toFixed( 0 ); // "43" a.toFixed( 1 ); // "42.6" a.toFixed( 2 ); // "42.59" a.toFixed( 3 ); // "42.590" a.toFixed( 4 ); // "42.5900" ``` Заметьте что результат - строковое `string` представление числа `number`, и таким образом `0`- будет добавлено справа если вам понадобится больше знаков после запятой, чем есть сейчас. `toPrecision(..)` похожий метод, но он определяет сколько *цифровых знаков* должно использоваться в выводимом значении: ```js var a = 42.59; a.toPrecision( 1 ); // "4e+1" a.toPrecision( 2 ); // "43" a.toPrecision( 3 ); // "42.6" a.toPrecision( 4 ); // "42.59" a.toPrecision( 5 ); // "42.590" a.toPrecision( 6 ); // "42.5900" ``` Вам не обязательно использовать переменные для хранения чисел, чтобы применить эти методы; вы можете применять методы прямо к числовым литералам `number`. Но, будьте осторожны с оператором `.`. Т.к. `.` это еще и числовой оператор, и, если есть такая возможность, он в первую очередь будет интерпретирован как часть числового литерала `number`, вместо того чтобы получать доступ к свойству. ```js // неправильный синтаксис: 42.toFixed( 3 ); // SyntaxError // это корректное обращение к методам: (42).toFixed( 3 ); // "42.000" 0.42.toFixed( 3 ); // "0.420" 42..toFixed( 3 ); // "42.000" ``` `42.toFixed(3)` неверный синтаксис, потому что `.` станет частью числового литерала `42.` (такая запись корректна -- смотрите выше!), и тогда оператор `.`, который должен получить доступ к методу `.toFixed`, отсутствует. `42..toFixed(3)` работает т.к. первый оператор `.` часть числового литерала `number` вторая `.` оператор доступа к свойству. Но, возможно это выглядит странно, и на самом деле очень редко можно увидеть что-то подобное в реальном JavaScript коде. Фактически, это нестандартно -- применять методы прямо к примитивным значениям. Нестандартно не значит *плохо* или *неправильно*. **Примечание:** Есть библиотеки расширяющие встроенные методы `Number.prototype` (подробнее Глава 3) для поддержки операций над/с числами `number`, и в этих случаях, совершенно правильно использовать `10..makeItRain()` чтобы отключить 10-секундную анимацию денежного дождя, или еще что-нибудь такое же глупое. Также технически корректной будет такая запись (заметьте пробел): ```js 42 .toFixed(3); // "42.000" ``` Тем не менее, с числовыми литералами `number` особенно, **это чрезвычайно запутанный стиль кода** и он не преследует иных целей кроме как запутать разработчиков при работе с кодом (в том числе и вас в будущем). Избегайте этого. Числа `number` также могут быть представлены в экспоненциальной форме, которую обычно используют для представления больших чисел `number` таких, как: ```js var onethousand = 1E3; // means 1 * 10^3 var onemilliononehundredthousand = 1.1E6; // means 1.1 * 10^6 ``` Числовые литералы `number` могут быть также выражены в других формах, таких как, двоичная, восьмеричная, и шестнадцатеричная. Эти форматы работают в текущей версии JavaScript: ```js 0xf3; // шестнадцатиричная для: 243 0Xf3; // то же самое 0363; // восьмеричная для: 243 ``` **Примечание:** Начиная с ES6 с включенным `strict` режимом, восьмеричная форма `0363` больше не разрешена (смотрите ниже новую форму). Форма `0363` все еще разрешена в `non-strict` режиме, но в любом случае нужно прекратить ее использовать, чтобы использовать современный подход (и потому что пора бы использовать `strict` режим уже сейчас!). Для ES6, доступны новые формы записи: ```js 0o363; // восьмеричная для: 243 0O363; // то же самое 0b11110011; // двоичная для: 243 0B11110011; // то же самое ``` И, пожалуйста, окажите вашим коллегам - разработчикам услугу: никогда не используйте форму вида `0O363`. `0` перед заглавной `O` может лишь вызвать затруднение при чтении кода. Всегда используйте нижний регистр в подобных формах: `0x`, `0b`, и `0o`. ### Маленькие дробные числа Самый известный побочный эффект от использования бинарной формы чисел с плавающей точкой (которая, как мы помним, справедлива для **всех** языков использующих стандарт IEEE 754 -- не *только* JavaScript как многие привыкли предполагать) это: ```js 0.1 + 0.2 === 0.3; // false ``` Математически, что результатом выражения должно быть `true`. Почему же в результате получается `false`? Если по-простому, представления чисел `0.1` и `0.2` в бинарном виде с плавающей точкой не совсем точные, поэтому когда мы их складываем, результат не совсем `0.3`. Это **действительно** близко: `0.30000000000000004`, но если сравнение не прошло, "близко" уже не имеет значения. **Примечание:** Должен ли JavaScript перейти на другую реализацию числового типа `number` которая имеет точные представления для всех значений? Некоторые так думают. За все годы появлялось много альтернатив. Никакие из них до сих пор не были утверждены, и возможно никогда не будут. Кажется что это также легко, как просто поднять руку и сказать "Да исправьте вы уже этот баг!", но это вовсе не так. Если бы это было легко, это определенно было бы исправлено намного раньше. Сейчас, вопрос в том, что если есть числа `number` для которых нельзя быть *уверенным* в их точности, может нам совсем не стоит использовать числа `number`? **Конечно нет.** Есть несколько случаев применения чисел, где нужно быть осторожными, особенно имея дело с дробными числами. Также есть достаточно (возможно большинство?) случаев когда мы имеем дело только с целыми числами ("integers"), и более того, работаем только с числами максимум до миллиона или триллиона. Такие случаи применения чисел всегда были, и будут, **превосходно безопасными** для проведения числовых операций в JS. А что если нам *было* нужно сравнить два числа `number` таких, как `0.1 + 0.2` и `0.3`, зная что обычный тест на равенство не сработает? Самая общепринятая практика использование миниатюрной "ошибки округления" как *допуска* для сравнения. Это малюсенькое значение часто называют "машинной эпсилон," которое составляет `2^-52` (`2.220446049250313e-16`) для числового типа `number` в JavaScript. В ES6, `Number.EPSILON` определено заранее этим пороговым значением, так что если вы хотите его использовать, нужно применить полифилл для определения порогового значения для стандартов до-ES6: ```js if (!Number.EPSILON) { Number.EPSILON = Math.pow(2,-52); } ``` Мы можем использовать это значение `Number.EPSILON` для проверки двух чисел `number` на "равенство" (с учетом допуска ошибки округления): ```js function numbersCloseEnoughToEqual(n1,n2) { return Math.abs( n1 - n2 ) < Number.EPSILON; } var a = 0.1 + 0.2; var b = 0.3; numbersCloseEnoughToEqual( a, b ); // true numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false ``` Максимальное значение числа с плавающей точкой приблизительно `1.798e+308` (реально огромное число!), определено как `Number.MAX_VALUE`. Минимальное значение, `Number.MIN_VALUE` приблизительно `5e-324`, оно положительное, но очень близко к нулю! ### Безопасные диапазоны целых чисел Из-за представления чисел `number` в JS, существует диапазон "безопасных" значений для всех чисел `number` "integers", и он существенно меньше значения `Number.MAX_VALUE`. Максимальное целое число, которое может быть "безопасно" представлено (это означает гарантию того, что запрашиваемое значение будет представлено совершенно определенно) это `2^53 - 1`, что составляет `9007199254740991`. Если вы добавите запятые, то увидите что это немного больше 9 квадриллионов. Так что это чертовски много для верхнего диапазона чисел `number`. Это значение автоматически предопределено в ES6, как `Number.MAX_SAFE_INTEGER`. Ожидаемо, минимальное значение, `-9007199254740991`, соответственно предопределено в ES6 как `Number.MIN_SAFE_INTEGER`. Чаще всего JS программы могут столкнуться с такими большими числами, когда имеют дело с 64-битными ID баз данных, и т.п. 64-битные не могут быть точно представлены типом `number`, так что они должны быть записаны (и переданы в/из) JavaScript с помощью строкового `string` представления. Математические операции с ID `number` значениями (кроме сравнения, которое отлично пройдет со строками `string`) обычно не выполняются, к счастью. Но если вам *необходимо* выполнить математическую операцию с очень большими числами, сейчас вы можете использовать утилиту *big number*. Поддержка больших чисел может быть реализована в будущих стандартах JavaScript. ### Проверяем является ли число целым Чтобы проверить, является ли число целым, вы можете использовать специальный ES6-метод `Number.isInteger(..)`: ```js Number.isInteger( 42 ); // true Number.isInteger( 42.000 ); // true Number.isInteger( 42.3 ); // false ``` Полифилл для `Number.isInteger(..)` для стандартов до-ES6: ```js if (!Number.isInteger) { Number.isInteger = function(num) { return typeof num == "number" && num % 1 == 0; }; } ``` Для проверки на нахождение числа в безопасном диапазоне *safe integer*, используется ES6-метод `Number.isSafeInteger(..)`: ```js Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true Number.isSafeInteger( Math.pow( 2, 53 ) ); // false Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true ``` Полифилл для `Number.isSafeInteger(..)` для стандартов до-ES6: ```js if (!Number.isSafeInteger) { Number.isSafeInteger = function(num) { return Number.isInteger( num ) && Math.abs( num ) <= Number.MAX_SAFE_INTEGER; }; } ``` ### 32-битные целые числа (со знаком) Пока целые числа могут быть приблизительно до 9 квадриллионов (53 бита), есть несколько числовых операторов (например побитовые операторы), которые определены для 32-битных чисел `number`, так "безопасный диапазон" для чисел `number` используемый в таких случаях намного меньше. Диапазоном является от `Math.pow(-2,31)` (`-2147483648`, около -2.1 миллиардов) до `Math.pow(2,31)-1` (`2147483647`, около +2.1 миллиардов). Чтобы записать число `number` из переменной `a` в 32-битное целое число, используем `a | 0`. Это сработает т.к. `|` побитовый оператор и работает только с 32-битными целыми числами (это означает что он будет работать только с 32 битами, а остальные биты будут утеряны). Ну, а "ИЛИ" с нулем побитовый оператор, который не проводит операций с битами. **Примечание:** Определенные специальные значения (о которых будет рассказано далее) такие как `NaN` и `Infinity` не являются "32-битными безопасными значениями" и в случае передачи этих значений побитовому оператору, будет применен абстрактный оператор `ToInt32` (смотрите главу 4) результатом которого будет значение`+0` для последующего применения побитового оператора. ## Специальные значения Есть несколько специальных значений, которые распространяются на все типы, и с которыми *внимательный* JS разработчик должен быть осторожен, и использовать их по назначению. ### Отсутствие значения Для типа `undefined`, есть только одно значение: `undefined`. Для типа `null`, есть только одно значение: `null`. Итак, для них обоих, есть свой тип и свое значение. И `undefined` и `null` часто считаются взаимозаменяемыми, как либо "пустое" значение, либо его "отсутствие". Другие разработчики различают их в соответствии с их особенностями. Например: * `null` пустое значение * `undefined` отсутствующее значение Или: * `undefined` значение пока не присвоено * `null` значение есть и там ничего не содержится Независимо от того, как вы "определяете" и используете эти два значения, `null` это специальное ключевое слово, не является идентификатором, и таким образом нельзя его использовать для назначения переменной (зачем вообще это делать!?). Как бы там ни было, `undefined` *является* (к несчастью) идентификатором. Увы и ах. ### Undefined В нестрогом режиме non-`strict`, действительно есть возможность (хоть это и чрезвычайно плохая идея!) присваивать значение глобальному идентификатору `undefined` : ```js function foo() { undefined = 2; // очень плохая идея! } foo(); ``` ```js function foo() { "use strict"; undefined = 2; // TypeError! } foo(); ``` Как в нестрогом non-`strict` так и в строгом `strict` режимах, тем не менее, вы можете создать локальную переменную `undefined`. Но, еще раз, это ужасная идея! ```js function foo() { "use strict"; var undefined = 2; console.log( undefined ); // 2 } foo(); ``` **Настоящие друзья никогда не позволят друзьям переназначить `undefined`.** Никогда. #### Оператор `void` Пока `undefined` является встроенным идентификатором который содержит (если только кто-нибудь это не изменил -- см. выше!) встроенное значение `undefined`, другой способ получить это значение - оператор `void`. Выражение `void ___` "аннулирует" любое значение, так что результатом выражения всегда будет являться значение `undefined`. Это выражение не изменяет действующее значение; оно просто дает нам уверенность в том, что мы не получим назад другого значения после применения оператора. ```js var a = 42; console.log( void a, a ); // undefined 42 ``` По соглашению (большей частью из C-языка программирования), для получения только самого значения `undefined` вместо использования `void`, вы можете использовать `void 0` (хотя и понятно что даже `void true` или любое другое `void` выражение выполнит то же самое). На практике нет никакой разницы между `void 0`, `void 1`, и `undefined`. Но, оператор `void` может быть полезен в некоторых других обстоятельствах, например, если нужно быть уверенным, что выражение не вернет никакого результата (даже если оно имеет побочный эффект). Например: ```js function doSomething() { // примечание: `APP.ready` поддерживается нашим приложением if (!APP.ready) { // попробуйте еще раз позже return void setTimeout( doSomething, 100 ); } var result; // делаем что - нибудь другое return result; } // есть возможность выполнить задачу прямо сейчас? if (doSomething()) { // выполняем следующие задания немедленно right away } ``` Здесь, функция `setTimeout(..)` возвращает числовое значение (уникальный идентификатор интервала таймера, если вы захотите его отменить), но нам нужно применить оператор `void` чтобы значение, которое вернет функция не было ложноположительным с инструкцией `if`. Многие разработчики предпочитают выполнять действия по отдельности, что в результате работает так же, но не требует применения оператора `void`: ```js if (!APP.ready) { // попробуйте еще раз позже setTimeout( doSomething, 100 ); return; } ``` Итак, если есть место где существует значение (как результат выражения), и вы находите полезным получить вместо него `undefined`, используйте оператор `void`. Возможно это не должно часто встречаться в ваших программах, но в редких случаях, когда это понадобится, это может быть довольно полезным. ### Специальные числа Тип `number` включает в себя несколько специальных значений. Рассмотрим каждое более подробно. #### НЕ Число, Число Любая математическая операция, которую выполняют с операндами не являющимися числами `number` (или значениями которые могут быть интерпретированы как числа `number` в десятичной или шестнадцатеричной форме) приведет к ошибке при попытке получить значение числового типа `number`, в этом случае вы получите значение `NaN`. `NaN` буквально означает "not a `number`" ("НЕ число"), хотя это название/описание довольно скудное и обманчивое, как мы скоро увидим. Было бы правильнее думать о `NaN` как о "неправильном числе", "ошибочном числе", или даже "плохом числе", чем думать о нем как о "НЕ числе". Например: ```js var a = 2 / "foo"; // NaN typeof a === "number"; // true ``` Другими словами: "Типом НЕ-числа является число 'number'!" Ура запутывающим именам и семантике. `NaN` что-то вроде "сторожевого значения" (другими словами нормальное значение, которое несет специальный смысл), которое определяет сбой при проведении операции назначения числа `number`. Эта ошибка, по сути означает следующее: "Я попробовал выполнить математическую операцию и произошла ошибка, поэтому, вместо результата, здесь ошибочное число `number`." Итак, если у вас есть значение в какой-нибудь переменной, и вы хотите проверить, не является ли оно ошибочным числом `NaN`, вы должно быть думаете что можно просто его сравнить прямо с `NaN`, как с любым другим значением, например `null` или `undefined`. Неа. ```js var a = 2 / "foo"; a == NaN; // false a === NaN; // false ``` `NaN` очень особенное значение и оно никогда не будет равно другому значению `NaN` (т.е., оно не равно самому себе). Фактически, это всего лишь значение, которое не рефлексивно (без возможности идентификации `x === x`). Итак, `NaN !== NaN`. Немного странно, да? Так как мы *можем* его проверить, если нельзя сравнить с `NaN` (т.к. сравнение не сработает)? ```js var a = 2 / "foo"; isNaN( a ); // true ``` Достаточно просто, верно? Мы использовали встроенную глобальную функцию, которая называется `isNaN(..)` и она сообщила нам является значение `NaN` или нет. Проблема решена! Не так быстро. У функции `isNaN(..)` есть большой недостаток. Он появляется при попытках воспринимать значение `NaN` ("НЕ-Число") слишком буквально -- вот, вкратце, как это работает: "проверяем то, что нам передали -- либо это не является числом `number`, либо -- это число `number`." Но это не совсем правильно. ```js var a = 2 / "foo"; var b = "foo"; a; // NaN b; // "foo" window.isNaN( a ); // true window.isNaN( b ); // true -- упс! ``` Понятно, `"foo"` буквально *НЕ-Число*, но и определенно не является значением `NaN`! Этот баг был в JS с самого начала (более 19 лет *упс*). В ES6, наконец была представлена функция: `Number.isNaN(..)`. Простым полифиллом, чтобы вы могли проверить на значение `NaN` *прямо сейчас*, даже в браузерах не поддерживающих-ES6, будет: ```js if (!Number.isNaN) { Number.isNaN = function(n) { return ( typeof n === "number" && window.isNaN( n ) ); }; } var a = 2 / "foo"; var b = "foo"; Number.isNaN( a ); // true Number.isNaN( b ); // false -- фуух! ``` Вообще, мы можем реализовать полифилл `Number.isNaN(..)` даже проще, если воспользоваться специфической особенностью `NaN`, которое не равно самому себе. `NaN` *единственное* для которого это справедливо; любое другое значение всегда **равно самому себе**. Итак: ```js if (!Number.isNaN) { Number.isNaN = function(n) { return n !== n; }; } ``` Странно, правда? Но это работает! `NaN`могут появляться во многих действующих JS программах, намеренно или случайно. Это действительно хорошая идея проводить надежную проверку, например `Number.isNaN(..)` если это поддерживается (или полифилл), чтобы распознать их должным образом. Если вы все еще используете `isNaN(..)` в своей программе, плохая новость: в вашей программе *есть баг*, даже если вы с ним еще не столкнулись! #### Бесконечности Разработчики пришедшие из традиционных компилируемых языков вроде C, возможно, привыкли видеть ошибку компилирования или выполнения, например "деление на ноль," для подобных операций: ```js var a = 1 / 0; ``` Как бы там ни было, в JS, эта операция четко определена, и ее результатом будет являться -- бесконечность `Infinity` (ну или `Number.POSITIVE_INFINITY`). Как и ожидается: ```js var a = 1 / 0; // Infinity var b = -1 / 0; // -Infinity ``` Как вы видите, `-Infinity` (или `Number.NEGATIVE_INFINITY`) получается при делении на ноль где один из операторов (но не оба!) является отрицательным. JS использует вещественное представление чисел (IEEE 754 числа с плавающей точкой, о котором было рассказано ранее), вразрез с чистой математикой, похоже что *есть* возможность переполнения при выполнении таких операций как сложение или вычитание, и в этом случае результатом будет `Infinity` или `-Infinity`. Например: ```js var a = Number.MAX_VALUE; // 1.7976931348623157e+308 a + a; // Infinity a + Math.pow( 2, 970 ); // Infinity a + Math.pow( 2, 969 ); // 1.7976931348623157e+308 ``` Согласно спецификации, если, в результате операции вроде сложения, получается число, превышающее максимальное число, которое может быть представлено, функция IEEE 754 "округления-до-ближайшего" определит, каким должен быть результат. Итак, если проще, `Number.MAX_VALUE + Math.pow( 2, 969 )` ближе к `Number.MAX_VALUE` чем к бесконечности `Infinity`, так что его "округляем вниз," тогда как `Number.MAX_VALUE + Math.pow( 2, 970 )` ближе к бесконечности `Infinity`, поэтому его "округляем вверх". Если слишком много об этом думать, то у вас так скоро голова заболит. Не нужно. Серьезно, перестаньте! Если однажды вы перешагнете одну из *бесконечностей*, в любом случае, назад пути уже не будет. Другими словами, в почти литературной форме, вы можете прийти из действительности в бесконечность, но не из бесконечности в действительность. Это фактически философский вопрос: "Что если бесконечность разделить на бесконечность". Наш наивный мозг скажет что-нибудь вроде "1", или, может, "бесконечность." Но ни то, ни другое, не будет верным. И в математике, и в JavaScript, операция `Infinity / Infinity` не определена. В JS, результатом будет `NaN`. Но, что если любое вещественное положительное число `number`, разделить на бесконечность `Infinity`? Это легко! `0`. А что если вещественное отрицательное число `number`, разделить на бесконечность `Infinity`? Об этом в следующей серии, продолжайте читать! #### Нули Это может смутить математически-думающего читателя, но в JavaScript есть два значения `0`: нормальный ноль (также известный как положительный ноль `+0`) *и* отрицательный ноль `-0`. Прежде чем объяснять почему существует `-0`, мы должны посмотреть как это работает в JS, потому что это может сбить с толку. Кроме того что значение `-0` может быть буквально присвоено, отрицательный ноль может быть результатом математических операций. Например: ```js var a = 0 / -3; // -0 var b = 0 * -3; // -0 ``` Отрицательный ноль не может быть получен в результате сложения или вычитания. Отрицательный ноль при выводе в консоль разработчика обычно покажет `-0`, хотя до недавнего времени это не было общепринятым, вы можете узнать что некоторые старые браузеры до сих пор выводят `0`. Как бы там ни было, при попытке преобразования отрицательного нуля в строку, всегда будет выведено `"0"`, согласно спецификации. ```js var a = 0 / -3; // (некоторые браузеры) выводят в консоль правильное значение a; // -0 // но спецификация лжет вам на каждом шагу! a.toString(); // "0" a + ""; // "0" String( a ); // "0" // странно, даже JSON введен в заблуждение JSON.stringify( a ); // "0" ``` Интересно, что обратная операция (преобразование из строки `string` в число `number`) не врет: ```js +"-0"; // -0 Number( "-0" ); // -0 JSON.parse( "-0" ); // -0 ``` **Предупреждение:** Поведение `JSON.stringify( -0 )` по отношению к `"0"` странное лишь частично, если вы заметите то обратная операция: `JSON.parse( "-0" )` выведет `-0` как вы и ожидаете. В дополнение к тому что преобразование в строку скрывает реальное значение отрицательного нуля, операторы сравнения также (намеренно) настроены *лгать*. ```js var a = 0; var b = 0 / -3; a == b; // true -0 == 0; // true a === b; // true -0 === 0; // true 0 > -0; // false a > b; // false ``` Очевидно, если вы хотите различать `-0` от `0` в вашем коде, вы не можете просто полагаться на то, что выведет консоль разработчика, так что придется поступить немного хитрее: ```js function isNegZero(n) { n = Number( n ); return (n === 0) && (1 / n === -Infinity); } isNegZero( -0 ); // true isNegZero( 0 / -3 ); // true isNegZero( 0 ); // false ``` Итак, зачем нам нужен отрицательный ноль, вместо обычного значения? Есть определенные случаи где разработчики используют величину значения для определения одних данных (например скорость перемещения анимации в кадре) а знак этого числа `number` для представления других данных (например направление перемещения). В этих случаях, как в примере выше, если переменная достигнет нуля и потеряет знак, тогда, вы потеряете информацию о том, откуда она пришла, до того как достигла нулевого значения. Сохранение знака нуля предупреждает потерю этой информации. ### Специальное равенство Как мы заметили ранее, значения `NaN` и `-0` имеют особенное поведение в случае сравнения. `NaN` никогда не равно самому себе, поэтому необходимо использовать функцию из ES6 `Number.isNaN(..)` (или же полифил). Аналогично, `-0` *лжет* и притворяется равным (даже `===` строго равным -- см. Главу 4) положительному `0`, поэтому необходимо использовать хитрый способ `isNegZero(..)`, который мы предложили выше. В ES6 появилась новая возможность, которую мы можем использовать для тестирования двух значений на абсолютное равенство без вышеперечисленных исключений. Она называется `Object.is(..)`: ```js var a = 2 / "foo"; var b = -3 * 0; Object.is( a, NaN ); // true Object.is( b, -0 ); // true Object.is( b, 0 ); // false ``` Есть достаточно простой полифил для `Object.is(..)` для браузеров, не поддерживающих ES6: ```js if (!Object.is) { Object.is = function(v1, v2) { // test for `-0` if (v1 === 0 && v2 === 0) { return 1 / v1 === 1 / v2; } // test for `NaN` if (v1 !== v1) { return v2 !== v2; } // everything else return v1 === v2; }; } ``` `Object.is(..)`, вероятно, не должен использоваться в случаях, где `==` и `===` достоверно *безопасны* (см. Главу 4 "Coercion"), поскольку данные операторы скорее более эффективны и определенно более идиоматичны/распространены. `Object.is(..)` необходим для особенных случаев равенства. ## Значения и ссылки Во многих языках значения могут присваиваться/передаваться по значению или по ссылке в зависимости от используемого синтаксиса. Например, если в С++ вы хотите обновить значение числовой переменной, передаваемой функции, то можете объявить параметр функции вида `int& myNum`. Тогда при передаче переменной (например `x`) `myNum` будет содержать **ссылку на `x`**; ссылки работают как специальная разновиднось указателей (то есть фактически *синонимы* для других переменных). Если же не объявить ссылочный параметр, то передаваемое значение *всегда* будет копироваться, даже если это сложный объект. В JavaScript указатели не существуют, а ссылки работают несколько иначе. Одна переменная JS не может хранить ссылку на другую переменную -- это попросту невозможно. Ссылка в JS указывает на (общее) **значение**, так что, если вы создаете 10 разных ссылок, они всегда будут указывать на одно общее значение; **они никогда не ссылаются/не указывают друг на друга.** Более того, в JavaScript не существует синтаксических подсказок, управляющие способом передачи (по ссылке или по значению). Вместо этого *тип* значения управляет только тем, как будет выполняться присваивание -- копированием значения или копированние ссылки. Пример: ```js var a = 2; var b = a; // `b` всегда содержит копию значения из `a` b++; a; // 2 b; // 3 var c = [1,2,3]; var d = c; // `d` - ссылка на общее значение `[1,2,3]` d.push( 4 ); c; // [1,2,3,4] d; // [1,2,3,4] ``` Простые значения (то есть скалярные примитивы) *всегда* присваиваются/передаются копированием значения: это `null`, `undefined`, `string`, `number`, `boolean` и `symbol` из ES6. Составные значения -- `объекты` (включая `массивы` и все объектные обертки -- см. главу 3) и `функции` -- при присваивании или передаче *всегда* создают копию ссылки. В приведенном примере, поскольку `2` является скалярным примитивом, `a` содержит одну исходную копию значения, а `b` присваивается другая *копия* значения. Но **`c` и `d`** представляют собой разные ссылки на одно общее значение `[1,2,3]`, которое является составным. Важно заметить, что значение `[1, 2, 3]` не "принадлежит" ни `c`, ни `d` -- это просто равноправные ссылки на значение. Итак, при использовании любой ссылки для изменения общего `массива` ((`.push(4)`)) изменения распространяются на единственное общее значение, а обе ссылки будут указывать на измененное значение `[1, 2, 3, 4]`. Так как ссылки указывают на сами значения, а не на переменные, одна ссылка не может использоваться для изменения того, на что ссылается другая ссылка: ```js var a = [1,2,3]; var b = a; a; // [1,2,3] b; // [1,2,3] // позднее b = [4,5,6]; a; // [1,2,3] b; // [4,5,6] ``` Выполняя присваивание `b = [4,5,6]`, мы абсолютно ничего не делаем для изменения того, на что сейчас ссылается `a` (`[1,2,3]`). Чтобы это произошло, переменная `b` должна быть указателем, а не ссылкой на массив, но в JS такой возможности нет. Чаще всего такие недоразумения происходят с параметрами функций: ```js function foo(x) { x.push( 4 ); x; // [1,2,3,4] // позднее x = [4,5,6]; x.push( 7 ); x; // [4,5,6,7] } var a = [1,2,3]; foo( a ); a; // [1,2,3,4], а не [4,5,6,7] ``` При передаче аргумента `a` копия ссылки `a` присваивается `x`. `x` и `a` -- разные ссылки, указывающие на одно значение `[1,2,3]`. Теперь внутри функции можно использовать эту ссылку для изменения самого значения (`push(4)`). Но когда мы выполняем присваивание `x = [4,5,6]`, оно никак не влияет на то, на что указывает исходная ссылка, она все еще указывает на (уже измененный) `[1,2,3,4]`. Ссылка `x` не может использоваться для изменения того, на что указывает `a`. Можно только изменить содержимое общего значения, на которое указывает как `a`, так и `x`. Чтобы переменная `a` изменилась и содержала `[4,5,6,7]`, вам не удастся создать новый `массив` и выполнить присваивание, необходимо изменить существующее значение `массива`: ```js function foo(x) { x.push( 4 ); x; // [1,2,3,4] // позднее x.length = 0; // очистить существующий массив на месте x.push( 4, 5, 6, 7 ); x; // [4,5,6,7] } var a = [1,2,3]; foo( a ); a; // [4,5,6,7], а не [1,2,3,4] ``` Как видите, `x.length = 0` и `x.push(4,5,6,7)` не создает новый `массив`, а изменяет существующий общий `массив`. И конечно, `a` ссылается на новое содержимое `[4,5,6,7]`. Помните: вы не можете напрямую контролировать/переопределять используемый механизм передачи (копирование значения или копирование ссылки) -- эта семантика определяется исключительно типом используемого значения. Чтобы передать составное значение (например, `массив`) посредством копирования значения, необходимо создать его копию вручную, чтобы переданная ссылка не продолжала указывать на оригинал. Пример: ```js foo( a.slice() ); ``` `slice(..)` без параметров по умолчанию создает совершенно новую (поверхностную) копию `массива`. Таким образом, передается ссылка только на скопированный `массив`, а значит `foo(..)` не сможет повлиять на содержимое `a`. Чтобы решить обратную задачу -- передать скалярное примитивное значение так, чтобы его значение обновлялось как ссылка -- необходимо "завернуть" значение в другое составное значение (`объект`, `массив`, и т. д.), которое *может* быть передано копированием ссылки: ```js function foo(wrapper) { wrapper.a = 42; } var obj = { a: 2 }; foo( obj ); obj.a; // 42 ``` Здесь `obj` является оберткой для скалярного примитивного свойства `a`. При передаче `foo(..)` передается копия ссылки `obj`, которая присваивается параметру `wrapper`. После этого ссылка `wrapper` может использоваться для обращения к общему объекту и обновлению его свойства. После завершения функции `obj.a` "увидит" обновленное значение `42`. И тут возникает другая мысль: если вы хотите передать ссылку на скалярное примитивное значение (например, `2`), нельзя ли упаковать значение в объектную обертку `Number` (см. главу 3)? Действительно, функции *будет* передана копия ссылки на объект `Number`, но, к сожалению, наличие ссылки на общий объект не даст вам возможности изменить общее примитивное значение, как можно было бы ожидать: ```js function foo(x) { x = x + 1; x; // 3 } var a = 2; var b = new Number( a ); // эквивалентно `Object(a)` foo( b ); console.log( b ); // 2, не 3 ``` Проблема в том, что нижележащее скалярное примитивное значение является *неизменяемым* (то же относится к `String` и `Boolean`). Если объект `Number` содержит скалярное примитивное значение `2`, этот конкретный объект `Number` не удастся изменить так, чтобы он содержал другое значение, можно только создать совершенно новый объект `Number` с другим значением. При использовании `x` в выражении `x + 1` нижележащее скалярное примитивное значение `2` распаковывается (извлекается) из объекта `Number` автоматически, так что строка `x = x + 1` совершенно незаметно превращает `x` из общей ссылки на объект `Number` в простое хранилище для скалярного примитивного значения `3` в результате операции сложения `2 + 1`. Следовательно, `b` снаружи продолжает ссылаться на исходный неизмененный/неизменяемый объект `Number`, содержащий значение `2`. К объекту `Number` *можно* добавлять новые свойства (только не изменяя его внутреннее примитивное значение), так что вы сможете организовать непрямую передачу информации через эти дополнительные свойства. Впрочем, такие ситуации встречаются довольно редко. Пожалуй, мало кто из разработчиков сочтет такой трюк хорошей практикой программирования. Вместо того чтобы использовать объектную обертку `Number` подобным образом, лучше использовать "ручное" решение с объектной оберткой (`obj`) из предыдущего фрагмента. Это вовсе не означает, что для таких объектных оберток, как `Number`, нельзя найти творческое применение. Это значит, что в большинстве случаев лучше использовать форму со скалярным примитивным значением. Ссылки обладают мощными возможностями, но иногда они начинают мешать, а иногда они нужны там, где их нет. Управлять поведением ссылок (выбирать между копированием значений и копированием ссылок) можно только при помощи типа самого значения, так что вам придется косвенно влиять на поведение присваивания/передачи выбором типов используемых значений. ## Обзор В JavaScript массивы `array` являются численно индексированными коллекциями для любых типов значений. Строки `string` несколько "массивоподобны", но обладают отличительными особенностями, и нужно быть аккуратным, если вы хотите рассматривать их в роли массивов `array`. Значения чисел в JavaScript включают "целые" ("integers") и с плавающей точкой. В примитивах определено несколько специальных значений. Тип `null` имеет одно значение: `null`, аналогично тип `undefined` имеет лишь значение `undefined`. `undefined` в целом является значением по умолчанию для любой переменной или свойства, если не указано иного значения. Оператор `void` позволяет вернуть значение `undefined` из любого другого значения. Числа `number` включают несколько специальных значений, например `NaN` (условно "НЕ число", но на самом деле скорее "неправильное число"); `+Infinity` и `-Infinity`; и `-0`. Простые скалярные примитивы (строки `string`, числа `number`, и т.д.) присваиваются/передаются копированием значения, однако сложные значения (объекты `object` и т.д.) присваиваются/передаются копированием ссылки. Ссылки не похожи на ссылки/указатели в других языках -- они никогда не указывают на другие переменные/ссылки, а только на основные значения. ================================================ FILE: types & grammar/ch3.md ================================================ # Вы не знаете JS: Типы и грамматика # Глава 3: Стандартные встроенные объекты Несколько раз в первой и второй главах этой книги мы упоминали различные нативные объекты, такие как `String` и `Number`. Давай рассмотрим их детальнее. Вот список часто используемых стандартных объектов: * `String()` * `Number()` * `Boolean()` * `Array()` * `Object()` * `Function()` * `RegExp()` * `Date()` * `Error()` * `Symbol()` -- добавлено в ES6! Как вы можете заметить, на деле все они являются встроенными функциями. Если вы пришли в JS из языков подобных Java, то, наверное, заметили, что выражение `String()` схоже с конструктором `String(..)`, используемым в Java для создания строк. Скоро станет понятно, что можно делать подобные вещи: ```js var s = new String( "Hello World!" ); console.log( s.toString() ); // "Hello World!" ``` На самом деле каждый из этих встроенных объектов может быть использован в качестве конструктора. Но продукт его вызова может быть не таким, как вам кажется. ```js var a = new String( "abc" ); typeof a; // "object" ... не "String" a instanceof String; // true Object.prototype.toString.call( a ); // "[object String]" ``` Результатом создания значений с помощью вызова конструктора (`new String("abc")`) является объект-обертка над примитивным (`"abc"`) значением. Вызов `typeof` показывает, что эти объекты не имеют какой-то уникальный `тип`, точнее определить их подтипами типа `object`. Эту обертку над значением можно наблюдать с помощью: ```js console.log( a ); ``` Результаты вывода могут отличаться в разных браузерах, так как консоли разработчика вправе выбирать как сериализировать объекты для инспектирования их разработчиками. **Замечание:** В момент написания этой книги последняя версия Chrome печатает что-то вроде этого: `String {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}`. Но старые версии Chrome печатали просто строку: `String {0: "a", 1: "b", 2: "c"}`. Последний Firefox на данный момент выводит `String ["a", "b", "c"]`, но до этого печатал курсивом `"abc"`, на который можно было кликнуть и открыть окно инспектора. Конечно, все это быстро меняется, и ваш опыт может сильно разниться с приведенным в книге. Цель создания обертки над строчкой `"abc"` с помощью `new String("abc")` - не только примитивное значение `"abc"`. ## Внутреннее свойство `[[Class]]` Значения, для которых `typeof` возвращает `"object"` (как, например, массив), дополнительно помечаются внутренним свойством `[[Class]]` (думайте об этом больше как о внутренней *класс*ификации, нежели о классах из традиционного ООП). Это свойство не имеет прямого доступа, но его его можно обнаружить, одолжив стандартный метод `Object.prototype.toString()` и вызвав его с желаемым значением. К примеру: ```js Object.prototype.toString.call( [1,2,3] ); // "[object Array]" Object.prototype.toString.call( /regex-literal/i ); // "[object RegExp]" ``` Видно, что для массива в этом примере, внутренний `[[Class]]` имеет значение `"Array"`, а для регулярного выражения `"RegExp"`. В большинстве случаев, значение внутреннего свойства `[[Class]]` соответствует стандартному встроенному конструктору (см. ниже), связанного со значением, хотя это не всегда так. А что насчет примитивных значений? Рассмотрим, `null` и `undefined`: ```js Object.prototype.toString.call( null ); // "[object Null]" Object.prototype.toString.call( undefined ); // "[object Undefined]" ``` Вы заметите, что нет собственных конструкторов Null() или Undefined(), но Null и Undefined, тем не менее, являются внутренними общедоступными значениями `[[Class]]`. Но для других простых примитивов, таких как `string`, `number` и `boolean`, на самом деле срабатывает другое поведение, которое обычно называется "упаковкой" (см. раздел "Обертки упаковки" далее): ```js Object.prototype.toString.call( "abc" ); // "[object String]" Object.prototype.toString.call( 42 ); // "[object Number]" Object.prototype.toString.call( true ); // "[object Boolean]" ``` В этом фрагменте каждый из простых примитивов автоматически упаковывается соответствующими объектными оболочками, поэтому `"String"`, `"Number"` и `"Boolean"` раскрываются как соответствующие внутренние `[[Class]]` значения. **Примечание:** Поведение `toString()` и `[[Class]]`, как показано здесь, немного изменилось с ES5 на ES6, но мы рассмотрим эти детали в заголовке *ES6 и далее* этой серии. . ## Обертки Эти оболочки объектов служат очень важной цели. Примитивные значения не имеют свойств или методов, поэтому для доступа к `.length` или `.toString()` вам нужна оболочка объекта вокруг значения. К счастью, JS автоматически *упаковывает* (или обертывает) примитивное значение для выполнения таких обращений. ```js var a = "abc"; a.length; // 3 a.toUpperCase(); // "ABC" ``` Итак, если вы собираетесь регулярно обращаться к этим свойствам/методам строковых значений, например, к условию `i < a.length` в цикле `for`, может показаться, что имеет смысл просто иметь объектную форму значения с самого начала, поэтому движку JS не нужно неявно создавать его для вас. Но оказывается, это плохая идея. Браузеры уже давно оптимизировали производительность в таких распространенных случаях, как `.length`, что означает, что ваша программа *фактически будет работать медленнее*, если вы попытаетесь "предварительно оптимизировать" непосредственно с помощью формы объекта (которая не есть путём к оптимизации). В общем, нет никакой причины использовать форму объекта напрямую. Лучше просто позволить боксу происходить неявно там, где это необходимо. Другими словами, никогда не делайте такие вещи, как `new String("abc")`, `new Number(42)` и т. д. Всегда предпочитайте использовать литеральные примитивные значения `"abc"` и `42`. ### Ошибки объектной оболочки Есть некоторые подводные камни, связанные с непосредственным использованием объектов-оболочек, о которых вам следует знать, если вы *решите* когда-либо их использовать. Например, рассмотрите `Boolean` обёртку: ```js var a = new Boolean( false ); if (!a) { console.log( "Oops" ); // никогда не запустится } ``` Проблема в том, что вы создали объектную оболочку вокруг значения `false`, но сами объекты являются "истинными" (см. главу 4), поэтому использование объекта ведет себя противоположно использованию самого лежащего в основе значения `false`, что весьма вопреки обычному ожиданию. Если вы хотите вручную упаковать примитивное значение, вы можете использовать функцию `Object(..)` (без ключевого слова `new`): ```js var a = "abc"; var b = new String( a ); var c = Object( a ); typeof a; // "string" typeof b; // "object" typeof c; // "object" b instanceof String; // true c instanceof String; // true Object.prototype.toString.call( b ); // "[object String]" Object.prototype.toString.call( c ); // "[object String]" ``` Опять же, прямое использование упакованных объектов-оболочек (например, `b` и `c` выше) обычно не рекомендуется, но могут быть некоторые редкие случаи, когда вы столкнетесь с тем, что они могут быть полезны. ## Распаковка Если у вас есть оболочка объекта и вы хотите получить базовое примитивное значение, вы можете использовать метод `valueOf()`: ```js var a = new String( "abc" ); var b = new Number( 42 ); var c = new Boolean( true ); a.valueOf(); // "abc" b.valueOf(); // 42 c.valueOf(); // true ``` Распаковка также может происходить неявно, при использовании значения оболочки объекта способом, требующим примитивного значения. Этот процесс (принуждение) будет рассмотрен более подробно в главе 4, но кратко: ```js var a = new String( "abc" ); var b = a + ""; // `b` имеет распакованное примитивное значение "abc" typeof a; // "object" typeof b; // "string" ``` ## Встроенные объекты как конструкторы Для `массива`, `объекта`, `функции` и значений регулярных выражений почти повсеместно предпочтительно использовать литеральную форму для создания значений, но литеральная форма создает тот же тип объекта, что и форма конструктора ( то есть нет неупакованного значения). Как мы видели выше с другими стандартными объектами, этих форм-конструкторов обычно следует избегать, если вы действительно не уверены, что они вам нужны, в основном потому, что они вводят исключения и ловушки, с которыми вы, вероятно, не *хотите* иметь дело. ### `Array(..)` ```js var a = new Array( 1, 2, 3 ); a; // [1, 2, 3] var b = [1, 2, 3]; b; // [1, 2, 3] ``` **Примечание:** Конструктор `Array(..)` не требует ключевого слова `new` перед ним. Если вы его опустите, он будет вести себя так, как будто вы его все равно использовали. Таким образом, «Массив (1,2,3)» — это тот же результат, что и «новый массив (1,2,3)». Конструктор `Array` имеет специальную форму, в которой, если передается только один аргумент `number`, вместо того, чтобы предоставлять это значение как *содержимое* массива, оно берется как длина для «предварительного размера массива» (ну, вроде как). Это ужасная идея. Во-первых, вы можете случайно споткнуться об эту форму, так как ее легко забыть. Но что еще более важно, нет такой вещи, как предварительное определение размера массива. Вместо этого вы создаете пустой массив, но устанавливаете для свойства `length` массива указанное числовое значение. Массив, который не имеет явных значений в своих слотах, но имеет свойство `length`, которое *подразумевает*, что слоты существуют, является странным экзотическим типом структуры данных в JS с очень странным и запутанным поведением. Возможность создания такого значения исходит исключительно из старых, устаревших, исторических функций («массивоподобные объекты», такие как объект «аргументы»). **Примечание.** Массив, в котором есть хотя бы один «пустой слот», часто называют «разреженным массивом». Дело не в том, что это еще один пример, когда консоли разработчиков браузера различаются в зависимости от того, как они представляют такой объект, что порождает еще большую путаницу. Например: ```js var a = new Array( 3 ); a.length; // 3 a; ``` Сериализация `a` в Chrome (на момент написания): `[ undefined x 3 ]`. **Это действительно прискорбно.** Это означает, что в слотах этого массива есть три `неопределенных` значения, хотя на самом деле слоты не существуют (так называемые "пустые слоты" - тоже плохое название!) Чтобы визуализировать разницу, попробуйте следующее: ```js var a = new Array( 3 ); var b = [ undefined, undefined, undefined ]; var c = []; c.length = 3; a; // [empty × 3] b; // [undefined, undefined, undefined] c; // [empty × 3] ``` **Примечание:** Как видно из `c` в этом примере, пустые слоты в массиве могут появиться после создания массива. Изменяя «длину» массива так, чтобы он превышал количество фактически определенных значений слотов, вы неявно вводите пустые слоты. На самом деле, вы могли бы даже вызвать `delete b[1]` в приведенном выше фрагменте, и это добавит пустой слот в середину `b`. Для `b` (в настоящее время в Chrome) вы найдете `[ undefined, undefined, undefined ]` в качестве сериализации, в отличие от `[ undefined x 3 ]` для `a` и `c`. Озадачены? Да, как и все остальные. Хуже того, на момент написания Firefox сообщает `[ , , , ]` для `a` и `c`. Вы поняли, почему это так запутанно? Посмотрите внимательно. Три запятые означают четыре слота, а не три слота, как мы ожидали. **Что!?** Firefox добавляет здесь дополнительный `,` в конце своей сериализации, потому что начиная с ES5 разрешены конечные запятые в списках (значения массива, списки свойств и т. д.) (и, таким образом, отбрасываются и игнорируются). Таким образом, если бы вы ввели значение `[ , , ]` в свою программу или консоль, вы фактически получили бы базовое значение, похожее на `[ , , ]` (то есть массив с тремя пустыми слотами). Этот выбор, хотя и сбивает с толку при чтении консоли разработчика, защищается тем, что вместо этого делает поведение копирования и вставки точным. Если вы сейчас качаете головой или закатываете глаза, вы не одиноки! Пожимает плечами. К сожалению, это становится хуже. `a` и `b` из приведенного выше фрагмента кода больше, чем просто сбивающий с толку вывод консоли, на самом деле ведут себя одинаково в некоторых случаях **но по-разному в других**: ```js a.join( "-" ); // "--" b.join( "-" ); // "--" a.map(function(v,i){ return i; }); // [ undefined x 3 ] b.map(function(v,i){ return i; }); // [ 0, 1, 2 ] ``` **Фу.** Вызов `a.map(..)` *терпит неудачу*, потому что слоты на самом деле не существуют, поэтому `map(..)` нечего перебирать. `join(..)` работает по-другому. По сути, мы можем думать о его реализации примерно так: ```js function fakeJoin(arr,connector) { var str = ""; for (var i = 0; i < arr.length; i++) { if (i > 0) { str += connector; } if (arr[i] !== undefined) { str += arr[i]; } } return str; } var a = new Array( 3 ); fakeJoin( a, "-" ); // "--" ``` Как видите, `join(..)` работает, просто *предполагая*, что слоты существуют, и зацикливается до значения `length`. Что бы ни делала `map(..)` внутри, она (очевидно) не делает такого предположения, поэтому результат от странного массива "пустых слотов" является неожиданным и, вероятно, приведет к сбою. Итак, если вы хотите *на самом деле* создать массив фактических «неопределенных» значений (а не просто «пустых слотов»), как вы можете это сделать (кроме как вручную)? ```js var a = Array.apply( null, { length: 3 } ); a; // [ undefined, undefined, undefined ] ``` Озадачены? Ага. Вот примерно как это работает. `apply(..)` - это утилита, доступная для всех функций, которая вызывает функцию, с которой она используется, но особым образом. Первый аргумент — это привязка объекта `this` (описанная в заголовке *this & Object Prototypes* этой серии), которая нам здесь не нужна, поэтому мы устанавливаем для нее значение `null`. Предполагается, что второй аргумент должен быть массивом (или чем-то *подобным* массиву, также известному как "array-like object"). Содержимое этого «массива» «распространяется» как аргументы рассматриваемой функции. Таким образом, `Array.apply(..)` вызывает функцию `Array(..)` и распределяет значения (значения объекта `{length: 3 }`) в качестве аргументов. Внутри `apply(..)` мы можем представить себе еще один цикл `for` (что-то вроде `join(..)` выше), который идет от `0` до `length` (`3` в нашем случае). Для каждого индекса он извлекает этот ключ из объекта. Таким образом, если бы параметр объекта массива был назван `arr` внутри функции `apply(..)`, доступ к свойству был бы: `arr[0]`, `arr[1]` и `arr[2]. ]`. Конечно, ни одно из этих свойств не существует в значении объекта `{ length: 3 }`, поэтому все три доступа к этим свойствам вернут значение `undefined`. Другими словами, это заканчивается вызовом `Array(..)` в основном следующим образом: `Array(undefined,undefined,undefined)`, так мы получаем массив, заполненный `undefined` значениями, а не этими (сумасшедшими) пустыми слотами. Хотя `Array.apply( null, {length: 3 } )` - это странный и многословный способ создания массива, заполненного `неопределенными` значениями, он **значительно** лучше и надежнее, чем то, что вы получаете с ножным ружьем `Array(3)` пустые слоты. Итог: **никогда, ни при каких обстоятельствах** вы не должны намеренно создавать и использовать эти экзотические массивы пустых слотов. Просто не делай этого. Они сумасшедшие. ### `Object(..)`, `Function(..)`, and `RegExp(..)` Конструкторы `Object(..)`/`Function(..)`/`RegExp(..)` также обычно необязательны (и поэтому их обычно следует избегать, если они специально не требуются): ```js var c = new Object(); c.foo = "bar"; c; // { foo: "bar" } var d = { foo: "bar" }; d; // { foo: "bar" } var e = new Function( "a", "return a * 2;" ); var f = function(a) { return a * 2; }; function g(a) { return a * 2; } var h = new RegExp( "^a*b+", "g" ); var i = /^a*b+/g; ``` Практически нет причин когда-либо использовать форму конструктора `new Object()`, тем более, что она вынуждает вас добавлять свойства одно за другим, а не многие сразу в литеральной форме объекта. Конструктор `Function` полезен только в самых редких случаях, когда вам нужно динамически определить параметры функции и/или ее тело функции. **Не рассматривайте `Function(..)` просто как альтернативную форму `eval(..)`.** Вам почти никогда не потребуется динамически определять функцию таким образом. Регулярные выражения, определенные в литеральной форме (`/^a*b+/g`), настоятельно предпочтительнее не только из-за простоты синтаксиса, но и из соображений производительности - механизм JS предварительно компилирует и кэширует их перед выполнением кода. В отличие от других форм конструктора, которые мы видели до сих пор, `RegExp(..)` имеет разумную полезность: для динамического определения шаблона для регулярного выражения. ```js var name = "Kyle"; var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" ); var matches = someText.match( namePattern ); ``` Такой сценарий время от времени возникает в программах JS, поэтому вам нужно использовать форму `new RegExp("pattern","flags")`. ### `Date(..)` and `Error(..)` Собственные конструкторы `Date(..)` и `Error(..)` намного полезнее, чем другие нативные конструкторы, потому что ни для одного из них нет литеральной формы. Чтобы создать значение объекта даты, вы должны использовать `new Date()`. Конструктор `Date(..)` принимает необязательные аргументы для указания используемой даты/времени, но если они опущены, предполагается текущая дата/время. На сегодняшний день наиболее распространенной причиной создания объекта даты является получение текущего значения метки времени (целое число со знаком в миллисекундах с 1 января 1970 года). Вы можете сделать это, вызвав `getTime()` для экземпляра объекта даты. Но еще более простой способ — просто вызвать статическую вспомогательную функцию, определенную в ES5: `Date.now()`. А полифилить это для pre-ES5 довольно просто: ```js if (!Date.now) { Date.now = function(){ return (new Date()).getTime(); }; } ``` **Примечание:** Если вы вызываете `Date()` без `new`, вы получите строковое представление даты/времени в этот момент. Точная форма этого представления не указана в спецификации языка, хотя браузеры, как правило, соглашаются на что-то близкое к: «Пт, 18 июля 2014 г., 00:31:02 GMT-0500 (CDT)». Конструктор `Error(..)` (очень похожий на `Array()` выше) ведет себя одинаково с ключевым словом `new`, присутствующим или опущенным. Основная причина, по которой вы хотите создать объект ошибки, заключается в том, что он захватывает текущий контекст стека выполнения в объект (в большинстве JS-движков после создания он раскрывается как свойство `.stack`, доступное только для чтения). Этот контекст стека включает в себя стек вызовов функций и номер строки, в которой был создан объект ошибки, что значительно упрощает отладку этой ошибки. Обычно вы используете такой объект ошибки с оператором `throw`: ```js function foo(x) { if (!x) { throw new Error( "x wasn't provided" ); } // .. } ``` Экземпляры объекта ошибки обычно имеют как минимум свойство `message`, а иногда и другие свойства (которые следует рассматривать как доступные только для чтения), например `type`. Однако, помимо проверки вышеупомянутого свойства `stack`, обычно лучше просто вызвать `toString()` для объекта ошибки (либо явно, либо неявно с помощью приведения – см. главу 4), чтобы получить сообщение об ошибке в понятном формате. сообщение. **Совет:** Технически, в дополнение к общему нативному коду `Error(..)`, есть несколько других нативных типов для конкретных ошибок: `EvalError(..)`, `RangeError(..)`, `ReferenceError(..)`, `SyntaxError(..)`, `TypeError(..)` и `URIError(..)`. Но очень редко можно вручную использовать эти конкретные ошибки. Они автоматически используются, если ваша программа действительно страдает от реального исключения (например, ссылка на необъявленную переменную и получение ошибки `ReferenceError`). ### `Symbol(..)` В ES6 добавлен дополнительный примитивный тип значения, который называется «Символ». Символы — это специальные «уникальные» (не строго гарантированные!) значения, которые можно использовать в качестве свойств объектов, практически не опасаясь каких-либо столкновений. В первую очередь они предназначены для специального встроенного поведения конструкций ES6, но вы также можете определить свои собственные символы. Символы можно использовать в качестве имен свойств, но вы не можете увидеть или получить доступ к фактическому значению символа из своей программы или из консоли разработчика. Если вы оцениваете символ в консоли разработчика, то, что показано, выглядит, например, как `Symbol(Symbol.create)`. В ES6 есть несколько предопределенных символов, доступ к которым осуществляется как к статическим свойствам функционального объекта `Symbol`, например, `Symbol.create`, `Symbol.iterator` и т.д. Чтобы использовать их, сделайте что-то вроде: ```js obj[Symbol.iterator] = function(){ /*..*/ }; ``` Чтобы определить свои собственные символы, используйте родной `Symbol(..)`. Собственный "конструктор" `Symbol(..)` уникален тем, что вам не разрешено использовать с ним `new`, так как это вызовет ошибку. ```js var mysym = Symbol( "my own symbol" ); mysym; // Symbol(my own symbol) mysym.toString(); // "Symbol(my own symbol)" typeof mysym; // "symbol" var a = { }; a[mysym] = "foobar"; Object.getOwnPropertySymbols( a ); // [ Symbol(my own symbol) ] ``` Хотя символы на самом деле не являются приватными (`Object.getOwnPropertySymbols(..)` отражает объект и раскрывает символы довольно публично), их использование для приватных или специальных свойств, вероятно, является их основным вариантом использования. Для большинства разработчиков они могут заменить имена свойств с префиксами подчеркивания `_`, которые почти всегда по соглашению означают: «Эй, это частное/специальное/внутреннее свойство, так что не трогайте его!» **Примечание:** "Символы" *не* "объекты", это простые скалярные примитивы. ### Прототипы встроенных объектов Каждый из встроенных нативных конструкторов имеет свой собственный объект `.prototype` -- `Array.prototype`, `String.prototype` и т.д. Эти объекты содержат поведение, уникальное для их конкретного подтипа объекта. Например, все строковые объекты, а также расширенные (через упаковку) примитивы `string`, имеют доступ к поведению по умолчанию как методы, определенные в объекте `String.prototype`. **Примечание:** По соглашению, принятому в документации, String.prototype.XYZ сокращается до String#XYZ, и аналогично для всех остальных `.prototype`. * `String#indexOf(..)`: найти позицию в строке другой подстроки * `String#charAt(..)`: получить доступ к символу в позиции в строке * `String#substr(..)`, `String#substring(..)`, and `String#slice(..)`: извлечь часть строки как новую строку * `String#toUpperCase()` and `String#toLowerCase()`: создать новую строку, которая преобразуется в верхний или нижний регистр * `String#trim()`: создать новую строку, лишенную всех конечных или начальных пробелов Ни один из методов не изменяет строку *на месте*. Модификации (такие как преобразование регистра или обрезка) создают новое значение из существующего значения. Благодаря делегированию прототипам (см. заголовок *this & Object Prototypes* в этой серии) любое строковое значение может получить доступ к этим методам: ```js var a = " abc "; a.indexOf( "c" ); // 3 a.toUpperCase(); // " ABC " a.trim(); // "abc" ``` Другие прототипы конструкторов содержат поведение, соответствующее их типам, например `Number#toFixed(..)` (строка числа с фиксированным количеством десятичных цифр) и `Array#concat(..)` (объединение массивов). Все функции имеют доступ к `apply(..)`, `call(..)` и `bind(..)`, поскольку они определены в `Function.prototype`. Но некоторые нативные прототипы не являются *просто* простыми объектами: ```js typeof Function.prototype; // "function" Function.prototype(); // Пустая функция! RegExp.prototype.toString(); // "/(?:)/" -- пустая регулярка "abc".match( RegExp.prototype ); // [""] ``` Особенно плохая идея, вы даже можете изменить эти собственные прототипы (а не просто добавить свойства, с которыми вы, вероятно, знакомы): ```js Array.isArray( Array.prototype ); // true Array.prototype.push( 1, 2, 3 ); // 3 Array.prototype; // [1,2,3] // не оставляйте так, иначе ждите странностей! // сбрасываем `Array.prototype` на пустой Array.prototype.length = 0; ``` Как видите, Function.prototype — это функция, RegExp.prototype — регулярное выражение, а Array.prototype — массив. Интересно и круто, да? #### Прототипы по умолчанию `Function.prototype` – пустая функция, `RegExp.prototype` – "пустое" (например, несоответствующее) регулярное выражение, а `Array.prototype` – пустой массив, сделайте для них все приятные значения "по умолчанию" для присвоения к переменным, если эти переменные еще не имели значения надлежащего типа. Например: ```js function isThisCool(vals,fn,rx) { vals = vals || Array.prototype; fn = fn || Function.prototype; rx = rx || RegExp.prototype; return rx.test( vals.map( fn ).join( "" ) ); } isThisCool(); // true isThisCool( ["a","b","c"], function(v){ return v.toUpperCase(); }, /D/ ); // false ``` **Примечание:** Начиная с ES6, нам не нужно использовать `vals = vals || ..` трюк с синтаксисом значения по умолчанию (см. главу 4) больше не используется, потому что значения по умолчанию могут быть установлены для параметров с помощью собственного синтаксиса в объявлении функции (см. главу 5). Одним из незначительных побочных преимуществ этого подхода является то, что `.prototype` уже созданы и встроены, поэтому создаются *только один раз*. Напротив, использование самих значений `[]`, `function(){}` и `/(?:)/` для этих значений по умолчанию (вероятно, в зависимости от реализации движка) будет воссоздавать эти значения (и, возможно, выполнять сборку мусора). их позже) для *каждого вызова* `isThisCool(..)`. Это может быть расточительно по памяти/ЦП. Кроме того, будьте очень осторожны, чтобы не использовать `Array.prototype` в качестве значения по умолчанию, **которое впоследствии будет изменено**. В этом примере vals используется только для чтения, но если бы вы вместо этого вносили изменения в vals на месте, вы фактически модифицировали бы сам Array.prototype, что привело бы к подводным камням, упомянутым ранее! **Примечание.** Хотя мы указываем на эти нативные прототипы и на некоторую их полезность, будьте осторожны, полагаясь на них, и еще более осторожны, не изменяя их каким-либо образом. См. Приложение A «Родные прототипы» для более подробного обсуждения. ## Обзор JavaScript предоставляет объектные оболочки вокруг примитивных значений, известных как стандартные объекты (String, Number, Boolean и т. д.). Эти оболочки объектов предоставляют значениям доступ к поведению, подходящему для каждого подтипа объекта (`String#trim()` и `Array#concat(..)`). Если у вас есть простое скалярное примитивное значение, такое как `"abc"`, и вы обращаетесь к его свойству `length` или какому-то методу `String.prototype`, JS автоматически "упаковывает" значение (оборачивает его в соответствующую оболочку объекта), чтобы доступы к свойствам/методам могли быть выполнены. ================================================ FILE: types & grammar/ch4.md ================================================ # Вы не знаете JS: Типы и грамматика # Глава 4: Приведение Теперь, когда мы гораздо более полно понимаем типы и значения Javascript, мы обратим наше внимание на очень спорную тему: приведение. Как мы упоминали в Главе 1, споры о том, является ли приведение полезной возможностью или недостатком языка (или чем-то посередине!) бушевали с первого дня. Если вы читали другие популярные книги по JS, вы знаете, что в подавляющем большинстве случаев распространено *мнение* о том, что приведение - это волшебство, зло, путаница и просто совершенно плохая идея. Следуя духу этой серии книг, вместо того, чтобы убегать от приведения значений, потому что так делают все остальные, или потому, что вас укусила какая-то муха, я думаю, вам следует заняться тем, чего вы не понимаете, и попытаться лучше *схватить* это. Наша цель - полностью изучить плюсы и минусы (да, *есть* плюсы!) приведения, чтобы вы могли принять обоснованное решение о его целесообразности в вашем коде. ## Конвертация значений Конвертация значения из одного типа в другой часто называется "преобразованием типа", когда делается явно, и "приведением", когда выполняется неявно (диктуется правилами использования значения). **Примечание:** Это может быть неочевидно, но результат приведения в JavaScript это всегда значение скалярного примитива (см. Главу 2), например, `string`, `number`, или `boolean`. Не существует приведения, которое бы приводило к созданию сложного значения, такого как `object` или `function`. Глава 3 посвящена "упаковке", которая помещает скалярные примитивные значения в их `object`-аналоги, но на самом деле это не приведение в строгом смысле слова. Другой распространенный способ различения этих терминов состоит в следующем: "преобразование типов" (или "конвертация типов") выполняется в статически типизированных языках во время компиляции, а "приведение типов" - это преобразование во время выполнения в динамически типизированных языках. Однако в JavaScript большинство людей называют все эти виды преобразований *приведением*, поэтому я предпочитаю обозначать их как "неявное приведение" и "явное приведение". Разница должна быть очевидной: "явное приведение" - это когда при взгляде на код ясно, что преобразование типа выполняется намеренно, тогда как "неявное приведение" - это когда преобразование - это менее очевидный побочный эффект какой-либо другой намеренной операции. Для примера рассмотрим эти два способа приведения: ```js var a = 42; var b = a + ""; // неявное приведение var c = String( a ); // явное приведение ``` В случае с `b` возникающее приведение происходит неявно, потому что оператор `+` в сочетании с одним из операндов, являющимся строковым значением (`""`), будет настаивать на том, что это операция конкатенации строк (сложения двух `string` вместе), что *в качестве (скрытого) побочного эффекта* приведет к тому, что значение `42` в `a` будет приведено к его строковому эквиваленту: `"42"`. Напротив, функция `String(..)` делает конвертацию очевидной, так как она явно берёт значение в `a` и приводит его к `string` представлению. Оба подхода приводят к одному и тому же результату: из `42` получается `"42"`. Но именно то, как это делается, лежит в основе жарких дебатов о приведении в JavaScript. **Примечание**: Технически, здесь, помимо стилистических различий, есть и нюансы в разном поведении. Мы рассмотрим их более подробно позже в разделе "Неявно: Строки <--> Числа". Термины "явный" и "неявный", или "очевидный" и "скрытый побочный эффект", *относительны*. Если вы точно знаете, что делает `a + ""`, и вы намеренно пишете это, чтобы привести к `string`, вы можете воспринимать эту операцию достаточно "явной". И наоборот, если вы никогда не видели функцию `String(..)`, используемую для приведения к `string`, ее поведение может показаться достаточно скрытым, чтобы стать для вас "неявным". Но мы ведем это обсуждение "явного" и "неявного" с точки зрения *среднего, достаточно информированного разработчика, но не эксперта или поборника JS спецификации*. В какой бы степени такое описание ни подходило вашему мироощущению, вам необходимо сделать поправку на наш угол обзора здесь. Просто помните: часто бывает так, что мы пишем наш код и являемся единственными, кто его читает. Даже если вы являетесь экспертом во всех тонкостях JS, подумайте, что почувствуют ваши менее опытные товарищи по команде, когда прочтут ваш код. Будет ли он "явным" или "неявным" для них так же, как и для вас? ## Абстрактные операции со значениями Прежде чем мы сможем приступить к *явному* и *неявному* приведению, нам нужно изучить базовые правила, которые определяют, как значения *становятся* `string`, `number` или `boolean`. Спецификация ES5 в разделе 9 определяет несколько "абстрактных операций" (затейливо обозначенные как "исключительно внутренние операции") с правилами конвертации значений. Мы специально обратим внимание на: `ToString`, `ToNumber`, `ToBoolean` и, в меньшей степени, на `ToPrimitive`. ### `ToString` Когда любое не строковое значение приводится в `string`-представление, преобразование выполняется абстрактной операцией `ToString` из раздела 9.8 спецификации. Встроенные примитивные значения имеют естественную строковое отображение: `null` становится `"null"`, `undefined` превращается в `"undefined"`, а `true` конвертируется в `"true"`. Числа (`number`) обычно представляются ожидаемым вами образом, но, как мы обсуждали в Главе 2, очень маленькие или очень большие числа представляются в экспоненциальной форме: ```js // умножим `1.07` на `1000` семь раз var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000; // семь раз по три цифры => 21 цифра a.toString(); // "1.07e21" ``` Для обычных объектов, если вы не укажете свой собственный, по умолчанию используется `toString()` (находится в `Object.prototype.toString()`), который вернет *внутренний `[[Class]]`* (см. Главу 3). Например, `"[object Object]"`. Но, как было показано ранее, если у объекта есть свой собственный метод `toString()`, и вы используете этот объект как `string`, автоматически будет вызван его `toString()`, и вместо объекта будет использован `string`-результат этого метода. **Примечание:** Технически путь приведения объекта к `string` пролегает через абстрактную операцию `ToPrimitive` (Спецификация ES5, раздел 9.1), но эти нюансы более подробно рассматриваются позже в разделе `ToNumber`, поэтому мы пропустим их здесь. У массивов базовый метод `toString()` переопределён и представляет собой сцепление всех его значений в виде строк через разделитель `","` (строковое представление каждого значения формируется индивидуально): ```js var a = [1,2,3]; a.toString(); // "1,2,3" ``` Опять же, `toString()` может быть либо вызван явно, либо он будет вызван автоматически, если не строковое значение используется в `string`-контексте. #### Строковое преобразование JSON Другая задача, которая, казалось бы, плохо связанна с `ToString`, - это когда вы используете утилиту `JSON.stringify(..)` для сериализации значения в строковый вид, совместимый с JSON. Важно отметить, что это строковое преобразование - не совсем то же самое, что приведение. Но поскольку оно связано с приведенными выше правилами `ToString`, мы сделаем небольшое отступление, чтобы рассмотреть логику строкового преобразования JSON. Обычно для большинства простых значений строковое преобразование JSON ведет себя так же, как `toString()`, за исключением того, что результатом сериализации *всегда является `string`*: ```js JSON.stringify( 42 ); // "42" JSON.stringify( "42" ); // ""42"" (строка с закавыченным строковым значением) JSON.stringify( null ); // "null" JSON.stringify( true ); // "true" ``` Любое *безопасное для JSON* значение может быть преобразовано в строку с помощью `JSON.stringify(..)`. Но что такое *безопасное для JSON*? Любое значение, которое может быть корректно отображено в представлении JSON. Вероятно, проще будет назвать значения, которые **не** являются безопасными для JSON. Некоторые примеры: `undefined`, `function`, `symbol` (ES6+) и `object` с циклическими ссылками (где ссылки на свойства объекта создают замкнутый круг, указывая друг на друга). Все это недопустимые значения для стандартной структуры JSON, главным образом потому, что они не переносимы на другие языки, которые используют JSON-данные. Утилита `JSON.stringify(..)` автоматически опустит значения `undefined`, `function` и `symbol`, когда столкнется с ними. Если такое значение окажется в `array`, то оно заменяется на `null` (чтобы сохранить информацию о местоположении элементов в массиве). Если любое из них хранится в свойстве `object`, это свойство будет просто исключено. Рассмотрим: ```js JSON.stringify( undefined ); // undefined JSON.stringify( function(){} ); // undefined JSON.stringify( [1,undefined,function(){},4] ); // "[1,null,null,4]" JSON.stringify( { a:2, b:function(){} } ); // "{"a":2}" ``` Но если вы попытаетесь передать в `JSON.stringify(..)` любой `object` с циклическими ссылками, то будет выдана ошибка. Строковое преобразование JSON ведет себя иначе, если в передаваемом ему `object` определен метод `toJSON()`. Этот метод будет вызван первым, чтобы получить сериализованное значение. Если вы собираетесь преобразовывать в JSON строку объект, который может содержать недопустимые JSON-значения, или если у вас в `object` есть значения, которые не подходят для сериализации, то вам следует определить для него метод `toJSON()`, который возвратит *безопасную для JSON* версию `object`. Например: ```js var o = { }; var a = { b: 42, c: o, d: function(){} }; // создадим циклическую ссылку в `a` o.e = a; // выдаст ошибку о циклической ссылке // JSON.stringify( a ); // определим пользовательскую сериализацию в JSON a.toJSON = function() { // при сериализации включать только свойство `b` return { b: this.b }; }; JSON.stringify( a ); // "{"b":42}" ``` Это очень распространенное заблуждение, что `toJSON()` должен возвращать строковое представление JSON. Вероятно, это неправильно до тех пор, пока вы не захотите сделать сериализацию самой `string` (обычно такого желания нет!). В действительности `toJSON()` должен возвращать обычное значение (любого подходящего типа), а `JSON.stringify(..)` сам преобразует его в строку. Другими словами, `toJSON()` следует интерпретировать как "к JSON-безопасному значению, подходящему для превращения в строку", а не "к строке JSON", как ошибочно предполагают многие разработчики. Рассмотрим: ```js var a = { val: [1,2,3], // вероятно, правильно! toJSON: function(){ return this.val.slice( 1 ); } }; var b = { val: [1,2,3], // вероятно, неправильно! toJSON: function(){ return "[" + this.val.slice( 1 ).join() + "]"; } }; JSON.stringify( a ); // "[2,3]" JSON.stringify( b ); // ""[2,3]"" ``` Во втором вызове мы сериализовали возвращаемый `string`, а не `array`, что, вероятно, было не тем, что мы хотели сделать. Пока мы говорим о `JSON.stringify(..)`, давайте обсудим некоторые менее известные возможности, которые тем не менее могут быть очень полезными. В `JSON.stringify(..)` может быть передан необязательный второй аргумент, который называется *replacer*. Он может быть либо `array`, либо `function`. Аргумент используется для настройки рекурсивной сериализации `object`, предоставляя механизм фильтрации, какие свойства должны включаться или исключаться, аналогично тому, как `toJSON()` может готовить значение для сериализации. Если *replacer* - `array`, то он должен быть массивом строк, каждая из которых будет содержать имя свойства, которое разрешено включать в сериализацию объекта. Если в `object` есть свойство, которого нет в этом списке, оно будет пропущено. Если *replacer* - `function`, то она будет вызвана один раз для самого `object`, а затем по разу для каждого свойства в `object`. Каждый раз передается два аргумента, *key* (ключ) и *value* (значение). Чтобы пропустить *key* при сериализации, верните значение `undefined`. В противном случае верните указанное *value*. ```js var a = { b: 42, c: "42", d: [1,2,3] }; JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}" JSON.stringify( a, function(k,v){ if (k !== "c") return v; } ); // "{"b":42,"d":[1,2,3]}" ``` **Примечание:** Когда *replacer* - это `function`, при первом вызове (где передается сам объект `a`) аргумент `k` равен `undefined`. Оператор `if` **отфильтровывает** свойство с именем `"c"`. Строковое преобразование рекурсивное, поэтому в массиве `[1,2,3]` каждое из его значений (`1`, `2`, `3`) передается как аргумент `v`, а индексы (`0`, `1`, `2`) - как аргумент `k`. В `JSON.stringify(..)` также может быть передан третий необязательный аргумент, называемый *space*, который используется в качестве отступа для более наглядного и читаемого вывода. *space* может быть положительным целым числом, говорящим, сколько пробелов следует использовать на каждом уровне отступа. *space* может быть и `string`. В этом случае для каждого уровня отступа будут использоваться первые десять символов его значения. ```js var a = { b: 42, c: "42", d: [1,2,3] }; JSON.stringify( a, null, 3 ); // "{ // "b": 42, // "c": "42", // "d": [ // 1, // 2, // 3 // ] // }" JSON.stringify( a, null, "-----" ); // "{ // -----"b": 42, // -----"c": "42", // -----"d": [ // ----------1, // ----------2, // ----------3 // -----] // }" ``` Помните, что `JSON.stringify(..)` напрямую не является формой приведения. Однако мы рассмотрели его здесь по двум причинам, которые связывают его действие с приведением `ToString`: 1. Значения `string`, `number`, `boolean` и `null` - все они в основном преобразуются JSON-строки так же, как приводятся в `string`-значения с помощью правил абстрактной операции `ToString`. 2. Если вы передаете `object`-значение в `JSON.stringify(..)`, и в этом `object` есть метод `toJSON()`, то `toJSON()` автоматически вызывается, чтобы (как бы) "привести" значение в *безопасное для JSON значение* перед началом преобразования в строку. ### `ToNumber` Если любое не `number` значение используется в контексте, требующем, чтобы оно было `number`, например, в математической операции, то спецификация ES5 определяет абстрактную операцию `ToNumber` в разделе 9.3. Например, `true` становится `1`, `false` - `0`, а `undefined` превращается в `NaN`, но (что любопытно) `null` преобразуется в `0`. `ToNumber` для `string` значения, по сути, работает по большей части так же, как правила/синтаксис для числовых литералов (см. Главу 3). В случае неудачи, результатом будет `NaN` (вместо синтаксической ошибки, как в случае с `number`-литералами). Один из примеров отличий - в этой операции восьмеричные числа с `0`-префиксом обрабатываются не как восьмеричные (с основанием 8) а, как обычные десятичные числа (с основанием 10), хотя такие восьмеричные являются корректным `number` литералом (см. Главу 2). **Примечание:** Различия между грамматикой `number`-литерала и `string`-значением в `ToNumber` тонкие и с многими нюансами, поэтому здесь они не будут рассматриваться. Для получения дополнительной информации обратитесь к разделу 9.3.1 спецификации ES5. Объекты (и массивы) сначала преобразуются в их эквивалент примитивного значения, а результирующее значение (если оно примитив, но еще не `number`) преобразуется в `number` в соответствии с выше описанными правилами `ToNumber`. Для преобразования в этот эквивалент примитивного значения, абстрактная операция `ToPrimitive` (спецификация ES5, раздел 9.1) использует внутреннюю операцию `DefaultValue` (спецификация ES5, раздел 8.12.8), чтобы определить, есть ли у значения метод `valueOf()`. Если `valueOf()` доступен, и он возвращает примитивное значение, то *это* значение используется для приведения. Если нет, но доступен `toString()`, он обеспечивает значение для приведения. Если ни одна из операций не может предоставить примитивное значение, выбрасывается ошибка `TypeError`. Начиная с ES5, вы можете создавать такой неприводимый объект - без `valueOf()` и `toString()` - если его скрытое свойство `[[Prototype]]` равно `null`. Обычно он создаётся с помощью `Object.create(null)`. Обратитесь к книге *This и Прототипы Объектов* из этой же серии для получения дополнительной информации о `[[Prototype]]`. **Примечание:** Позже в этой главе мы подробно рассмотрим, как приводить `number`, но для следующего фрагмента кода просто предположите, что это выполняет функция `Number(..)`. Рассмотрим: ```js var a = { valueOf: function(){ return "42"; } }; var b = { toString: function(){ return "42"; } }; var c = [4,2]; c.toString = function(){ return this.join( "" ); // "42" }; Number( a ); // 42 Number( b ); // 42 Number( c ); // 42 Number( "" ); // 0 Number( [] ); // 0 Number( [ "abc" ] ); // NaN ``` ### `ToBoolean` Далее, давайте немного поговорим о том, как ведут себя `boolean` значения в JS. Вокруг этой темы **много путаницы и неправильных представлений**, так что будьте бдительны! Прежде всего, в JS есть реальные ключевые слова `true` и `false`, и они ведут себя точно так, как вы ожидаете от `boolean` значений. Распространенное заблуждение, что значения `1` и `0` идентичны `true`/`false`. Хотя это может быть верно для других языков, в JS `number` - это `number`, а `boolean` - это `boolean`. Вы можете привести `1` в `true` (и наоборот) или `0` в `false` (и наоборот). Но это не одно и то же. #### Ложные значения Но это еще не конец истории. Нам нужно обсудить, как ведут себя значения, отличные от двух `boolean`, когда вы приводите их *к* `boolean` эквиваленту. В JavaScript все значения можно разделить на две категории: 1. значения, которые станут `false`, если их привести в `boolean` 2. все остальное (которые, очевидно, станет `true`) Я не шучу. Спецификация JS определяет конкретный, узкий список значений, которые становятся `false` при приведении в `boolean`. Как мы узнаем, что это за список значений? В спецификации ES5 раздел 9.2 определяет абстрактную операцию `ToBoolean`, которая точно определяет, что происходит для всех возможных значений, когда вы пытаетесь привести их "к булевому (логическому) значению". Из этой таблицы мы получаем список так называемых "ложных" значений: * `undefined` * `null` * `false` * `+0`, `-0` и `NaN` * `""` Это он. Если значение находится в этом списке, это "ложное" значение, и оно будет равно `false`, если вы приведёте его к `boolean`. Согласно логике, если значение *отсутствует* в этом списке, то оно должно быть в *другом списке*, который мы называем списком "правдивых" значений. Но JS спецификация на самом деле не определяет "правдивый" список как таковой. Она приводит отдельные примеры, как, например, явное указание, что все объекты - правдивые. Спецификация просто подразумевает: **все, что явно не указано в списке ложных, является правдивым**. #### Ложные объекты Погодите минутку, название этого раздела даже звучит противоречиво. Я буквально *только что сказал*, что спецификация называет все объекты правдивыми, верно? Такого понятия, как "ложный объект", не должно быть. Что бы это вообще могло значить? У вас может возникнуть соблазн подумать, что это объект-обертка (см. Главу 3) вокруг ложного значения (такого как `""`, `0` или `false`). Но не попадайтесь на эту *удочку*. **Примечание:** Это тонкая шутка с уточнением, которую некоторые из вас смогут понять. Рассмотрим: ```js var a = new Boolean( false ); var b = new Number( 0 ); var c = new String( "" ); ``` Мы знаем, что все три значения здесь являются объектами (см. Главу 3), обернутыми вокруг явно ложных значений. Но ведут ли себя эти объекты как `true` или как `false`? На это легко ответить: ```js var d = Boolean( a && b && c ); d; // true ``` Итак, все три ведут себя как `true`, ибо только так `d` может оказаться `true`. **Совет:** Обратите внимание на `Boolean( .. )`, обернутое вокруг выражения `a && b && c` - вы можете задаться вопросом, зачем оно там. Мы вернемся к этому позже, так что запомните это. Для краткости (с точки зрения мелочей) попробуйте узнать сами, каким будет `d`, если вы просто выполните `d = a && b && c` без вызова `Boolean( .. )`! Итак, если "ложные объекты" - это **не просто объекты, обернутые вокруг ложных значений**, то что же это такое, черт возьми? Сложность заключается в том, что они могут появляться в вашем JS коде, но на самом деле они не являются частью самого JavaScript. **Что!?** Есть конкретные ситуации, когда браузеры изобрели свой вид поведения *экзотических* значений, а именно идею "ложных объектов" поверх обычной семантики JS. "Ложный объект" - это значение, которое выглядит и действует как обычный объект (свойства и т.д.). Но, когда вы приводите его значение к `boolean`, оно превращается в `false`. **Почему!?** Самым известным примером является `document.all`: массивоподобный (объект), предоставляемый вашей JS программе *DOM*-ом (не самим движком JS), который отражает элементы страницы для вашей программы. Он *вёл* себя как обычный объект - действовал бы он правдиво. Но больше это не так. `document.all` само по себе никогда не было по-настоящему "стандартным" и уже давно признано устаревшим и стало неподдерживаемым. "Не могут ли они просто удалить это тогда?" Извини, хорошая попытка. Жаль, что они не могут. Существует слишком много старого JS кода, который использует его. Так, зачем заставлять его действовать ложно? Потому что приведение `document.all` к `boolean` (например, в операторе `if`) почти всегда использовалось как средство обнаружения старого, нестандартного IE. IE уже давно соответствует стандартам и во многих случаях продвигает веб вперед не меньше, а то и больше, чем любой другой браузер. Но весь этот старый код `if (document.all) { /* это IE */ }` все еще работает, и большая его часть, вероятно, никогда не уйдёт. Всё это наследие из старого кода по-прежнему предполагает, что он работает в IE десятилетней давности, что попросту приводит к некорректной работе страниц у пользователей IE. Итак, мы не можем удалить `document.all` полностью, но IE не хочет, чтобы код `if (document.all) { .. }` больше работал. Он хочет, чтобы пользователи современного IE получали новую, соответствующую стандартам логику кода. **"Что нам делать?"** "Я знаю! Давайте уничтожим систему типов JS и притворимся, что `document.all` - ложный!" Фу. Это отстой. Это сумасшедшая уловка, которую большинство разработчиков JS не понимают. Но альтернатива (ничего не делать с вышеупомянутыми проблемами) - *еще больший отстой*. Итак... вот что мы имеем: сумасшедшие, нестандартные "ложные объекты", добавленные браузерами в JavaScript. Ура! #### Правдивые значения Вернемся к правдивому списку. Какие именно значения правдивые? Помните: **значение правдивое, если его нет в списке ложных**. Рассмотрим: ```js var a = "false"; var b = "0"; var c = "''"; var d = Boolean( a && b && c ); d; ``` По вашему мнению, какое значение будет иметь `d` здесь? Это должно быть либо `true`, либо `false`. Это будет `true`. Почему? Потому что, хотя содержимое этих `string` выглядит как ложные значения, сами `string` значения являются истинными, потому что в списке ложных `string` есть единственное значение - это `""`. А эти? ```js var a = []; // пустой array -- правдивое или ложное? var b = {}; // пустой object -- правдивое или ложное? var c = function(){}; // пустая function -- правдивое или ложное? var d = Boolean( a && b && c ); d; ``` Да, вы уже догадались, `d` здесь по-прежнему `true`. Почему? Та же причина, что и раньше. Несмотря на то, чем это может казаться, `[]`, `{}` и `function(){}` *отсутствуют* в списке ложных значений и, следовательно, являются истинными значениями. Другими словами, правдивый список бесконечно длинен. Составить такой список невозможно. Вы можете только составить конечный список ложных значений и сверяться *с ним*. Потратьте пять минут, напишите ложный список на стикере для монитора или запомните его, если хотите. Тогда, вы легко сможете воспроизвести виртуальный правдивый список, когда это вам понадобится, просто спросив, есть ли он в ложном списке или нет. Важность правдивости и ложности заключается в понимании того, как будет вести себя значение, если вы его приведёте (явно или неявно) к `boolean` значению. Теперь, когда у вас есть эти два списка, мы можем углубиться в примеры приведения. ## Явное приведение *Явное* приведение относится к преобразованиям типов, которые являются очевидными и явными. Существует широкий спектр применений преобразования типов, которое соответствует категории *явного* приведения у большинства разработчиков. Цель заключается в том, чтобы выявить в нашем коде шаблоны - ясного и очевидного преобразования значения из одного типа в другой - так, чтобы не оставлять ям, в которые могут угодить будущие разработчики. Чем более демонстративны мы будем, тем больше вероятность, что кто-то после сможет прочитать и понять наш код без излишних усилий, какие у нас были намерения. Думается, не будет каких-либо существенных разногласий относительно *явного* приведения, поскольку оно наиболее точно соответствует общепринятой практике преобразования типов, сложившейся в языках со статической типизацией. Таким образом, мы примем как должное (на данный момент), что *явное* приведение едино в том, чтобы не быть злонамеренным или противоречивым. Однако мы вернемся к этому позже. ### Явно: Строки <--> Числа Мы начнем с самой простой и, возможно, наиболее распространенной операции приведения: преобразование значений между `string` и `number` представлениями. Для приведения между `string` и `number` мы используем встроенные функции `String(..)` и `Number(..)` (которые мы назвали "собственными конструкторами" в Главе 3), при этом **очень важно**, мы не используем ключевое слово `new` перед ними. Таким образом, мы не создаем объекты-обёртки. Вместо этого мы на самом деле выполняем *явное приведение* между двумя этими типами: ```js var a = 42; var b = String( a ); var c = "3.14"; var d = Number( c ); b; // "42" d; // 3.14 ``` `String(..)` преобразует любое значение в примитивное значение `string`, используя ранее рассмотренные правила операции `ToString`. `Number(..)` преобразует любое значение в примитивное значение `number`, используя ранее описанные правила операции `ToNumber`. Я называю это *явным* приведением, потому что в целом для большинства разработчиков довольно очевидно, что конечным результатом этих операций является приемлемое преобразование типа. Действительно, это очень похоже на то, как это работает в некоторых других статически типизированных языках. Например, в C/C++ вы можете написать либо `(int)x`, либо `int(x)`, и оба они преобразуют значение в `x` в целое число. Обе формы допустимы, но многие предпочитают последнюю, которая выглядит как вызов функции. В JavaScript, когда вы пишете `Number(x)`, это выглядит ужасно похоже. Имеет ли значение, что это *на самом деле* вызов функции в JS? Не совсем. Помимо `String(..)` и `Number(..)`, существуют другие способы "явного" приведения значений между `string` и `number`: ```js var a = 42; var b = a.toString(); var c = "3.14"; var d = +c; b; // "42" d; // 3.14 ``` Вызов `a.toString()` считается явным (довольно ясно, что "toString" означает "к строке"), но здесь есть определенная скрытая неявность. `toString()` не может быть вызван для *примитивного* значения, такого как `42`. Поэтому JS автоматически "упаковывает" (см. Главу 3) `42` в объект-обёртку, так что `toString()` может быть вызван. Другими словами, вы могли бы назвать это "явно неявным". `+c` здесь демонстрирует форму *унарного оператора* `+` (оператор только с одним операндом). Однако вместо выполнения математического сложения (или сцепления строк - см. ниже) унарный символ `+` явно приводит свой операнд (`c`) в значение `number`. Является ли `+c` *явным* приведением? Зависит от вашего опыта и точки зрения. Если вы знаете (а вы теперь знаете!), что унарный `+` явно предназначен для приведения к `number`, то это достаточно явно и очевидно. Однако, если вы никогда не видели этого раньше, это может показаться ужасно запутанным, неявным, со скрытыми побочными эффектами и т.д. **Примечание:** Общепринятая точка зрения JS-сообщества с открытым исходным кодом заключается в том, что унарный `+` является приемлемой формой *явного* приведения. Даже если вам действительно нравится форма `+c`, определенно есть места, где она может выглядеть ужасно запутанной. Рассмотрим пример: ```js var c = "3.14"; var d = 5+ +c; d; // 8.14 ``` Унарный оператор `-` также выполняет приведение, как и `+`, но он ещё меняет знак числа. При этом вы не можете поместить два оператора рядом с друг другом `--`, чтобы отменить смену знака, так как это интерпретируется как оператор уменьшения. Вместо этого вам нужно было бы сделать: `- -"3.14"` с пробелом между ними, тогда бы это вызвало приведение к `3.14`. Вероятно, вы можете придумать всевозможные отвратительные комбинации двоичных операторов (подобно `+` для сложения) рядом с унарной формой оператора. Вот еще один безумный пример: ```js 1 + - + + + - + 1; // 2 ``` Вам следует настойчиво избегать приведения с унарным `+` (или `-`), когда он примыкает к другим операторам. Хотя пример выше работает, такое почти везде считается плохой идеей. Даже `d = +c` (или `d = + c`, если уж на то пошло!) слишком легко спутать с `d + = c`, что совершенно другое! **Примечание:** Другим чрезвычайно запутанным случаем использования унарного `+` - это помещение его рядом с операторами инкремента `++` и декремента `--`. Например: `a +++b`, `a + ++b` и `a + + +b`. Больше подробностей о `++` смотрите в разделе "Побочные эффекты выражений" в Главе 5. Помните, мы пытаемся явно выразить свои намерения и **уменьшить** путаницу, а не усугублять ее! #### `Date` в `number` Другим распространенным случаем использования унарного оператора `+` является приведение объекта `Date` в `number`, так как в результате мы получаем метку времени unix (миллисекунды, прошедшие с 1 января 1970 года 00:00:00 UTC), представляющую значение даты/времени: ```js var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" ); +d; // 1408369986000 ``` Самым распространенным применением этой идиомы является фиксация времени *текущего времени* в виде метки времени, например, так: ```js var timestamp = +new Date(); ``` **Примечание:** Некоторые разработчики знают о специфическом синтаксическом "трюке" в JavaScript, который заключается в том, что `()` при вызове конструктора (функция, вызываемая с `new`), являются *необязательными*, если нет аргументов для передачи. Поэтому, вы можете встретить такой вариант: `var timestamp = +new Date;`. Однако не все разработчики согласны с тем, что отсутствие `()` улучшает читаемость, поскольку это необычное синтаксическое исключение применяется только к форме вызова `new fn()`, а не к обычной форме вызова `fn()`. Но приведение - это не единственный способ получить метку времени из объекта `Date`. Вариант без приведения, возможно, даже предпочтительнее, поскольку он более явный: ```js var timestamp = new Date().getTime(); // var timestamp = (new Date()).getTime(); // var timestamp = (new Date).getTime(); ``` Но *еще более* предпочтительным вариантом без приведения является использование появившейся в ES5 статической функции `Date.now()`: ```js var timestamp = Date.now(); ``` В старых браузерах вы можете использовать полифил `Date.now()`, он довольно простой: ```js if (!Date.now) { Date.now = function() { return +new Date(); }; } ``` Я бы рекомендовал избегать приведений, связанных с датами. Используйте `Date.now()` для метки времени *текущего момента* и `new Date( .. ).getTime()` для получения метки времени *не текущего момента*, который можно указать в виде даты/времени. #### Любопытный случай с `~` Одним из операторов приведения, который часто упускается из виду и обычно сбивает с толку, является оператор тильды `~` ("побитовое НЕ"). Многие из тех, кто даже понимает, что он делает, часто избегают его. Но, придерживаясь духа этой книги и серии, давайте углубимся в него, чтобы выяснить, может ли `~` дать нам что-нибудь полезное. В разделе "32-битные целые числа (со знаком)" Главы 2 мы писали, что в JS побитовые операторы работают только как 32-разрядные операторы, то есть они требуют от своих операндов иметь значение в 32-разрядном представлении. Правила того, как это происходит, управляется абстрактной операцией `ToInt32` (спецификация ES5, раздел 9.5). `ToInt32` сначала выполняет приведение `ToNumber`, что означает, что, если значение равно `"123"`, оно сначала станет `123`, прежде чем будут применены правила `ToInt32`. Хотя *технически* это не приведение (поскольку тип не меняется!), использование побитовых операторов (такого как `|` или `~`) с определенными специальными значениями `number` дает эффект приведения и другое значение `number`. Например, давайте сначала рассмотрим оператор `|` - "побитовое ИЛИ", используемый в идиоме `0 | x`, которая (как показано в Главе 2) по сути выполняет лишь преобразование `ToInt32`: ```js 0 | -0; // 0 0 | NaN; // 0 0 | Infinity; // 0 0 | -Infinity; // 0 ``` Эти специальные числа нельзя представить в 32-разрядном виде (поскольку они взяты из 64-разрядного стандарта IEEE 754 - см. Главу 2), поэтому `ToInt32` просто указывает `0` как результат этих значений. Спорно, является ли `0 | __` *явной* формой приведения операции `ToInt32` или она всё же *неявная*. С точки зрения спецификации, она безусловно *явная*, но если вы не понимаете побитовые операции, то она вероятно может показаться *неявной* и волшебной. Тем не менее, в соответствии с другими утверждениями в этой главе, мы будем называть приведение *явным*. Итак, давайте опять обратим наше внимание на `~`. Оператор `~` сначала "приводит" к 32-разрядному значению `number`, а затем выполняет побитовое отрицание (инвертируя каждый бит). **Примечание:** Это очень похоже на то, как `!` не только преобразует значение в `boolean`, но и инвертирует его значение (см. обсуждение "унарного `!`" ниже). Но... что?! Почему нас волнует, что биты инвертируются? Это довольно специфичная тема. JS-разработчикам довольно редко приходится задумываться об отдельных битах. Иной смысл сущности `~` даёт информатика старой школы / дискретная математика: здесь `~` - это операция дополнения двойки. Отлично, спасибо, это совершенно ясно! Давайте попробуем еще раз: `~x` примерно совпадает с `-(x+1)`. Это странно, но рассуждать об этом немного легче. Итак: ```js ~42; // -(42+1) ==> -43 ``` Вы, вероятно, все еще задаетесь вопросом, к чему, черт возьми, вся эта ерунда с `~`, или почему это действительно важно в обсуждении приведения? Давайте сразу перейдем к делу. Рассмотрим `-(x+1)`. Какое единственное значение, с которым вы можете выполнить эту операцию и получить результат `0` (или `-0` технически!)? `-1`. Другими словами, `~`, используемая с разными значениями `number`, даст ложное значение `0` (легко приводимое к `false`) для входного значения `-1`. В любом другом случае мы значение `number` будет правдивым. Какое отношение это имеет к делу? `-1` обычно зовётся "контрольным значением" - это значение, которому придается определенный семантический смысл в наборе данных того же типа (`number`). Язык C использует контрольные значения `-1` во многих функциях, которые возвращают значения `>= 0` в случае "успеха" и `-1` для "неудач". JavaScript заимствовал этот подход для `string` операции `indexOf(..)`, которая ищет подстроку и, если найдена, возвращает ее индекс-позицию или `-1`, если она не найдена. Довольно часто пытаются использовать `indexOf(..)` не только для поиска позиции, но и как `boolean` проверку наличия/отсутствия подстроки в другой `string`. Вот как обычно выполняют такие проверки: ```js var a = "Hello World"; if (a.indexOf( "lo" ) >= 0) { // true // нашёл! } if (a.indexOf( "lo" ) != -1) { // true // нашёл } if (a.indexOf( "ol" ) < 0) { // true // не нашёл! } if (a.indexOf( "ol" ) == -1) { // true // не нашёл! } ``` Я нахожу довольно неэстетичным видеть в коде `>= 0` или `== -1`. По сути, это "дырявая абстракция", поскольку она выставляет наружу, в мой код, свою внутреннюю реализацию - использование контрольного `-1` для "сбоя". Я бы предпочел, чтобы такие детали были скрыты. Наконец, мы видим, как `~` могла бы нам помочь! Использование `~` с `indexOf()` "приводит" (на самом деле просто преобразует) результат **к значению, которое можно привести к `boolean`**: ```js var a = "Hello World"; ~a.indexOf( "lo" ); // -4 <-- правдивое! if (~a.indexOf( "lo" )) { // true // нашёл! } ~a.indexOf( "ol" ); // 0 <-- ложное! !~a.indexOf( "ol" ); // true if (!~a.indexOf( "ol" )) { // true // не нашёл! } ``` `~` принимает возвращаемое значение `indexOf(..)` и преобразует его: для `-1` "сбоя" мы получаем ложный `0`, а все остальные значения являются правдивыми. **Примечание:** Псевдоалгоритм `-(x + 1)` для `~` подразумевает, что `~-1` равно `-0`, но на самом деле тильда выдает `0`, потому что операция на самом деле побитовая, а не математическая. Технически, `if (~a.indexOf(..))` использует *неявное* приведение результирующего значения `0` к `false` или ненулевого значения к `true`. Но в целом, мне кажется, `~` больше похожа на *явное* приведение, если вы знаете, цель этого оператора в этой идиоме. Я считаю, что это более ясный код, чем беспорядок с `>= 0` / `== -1`. ##### Отбрасывание битов Ещё одно место, где `~` может появиться в коде, с которым вы сталкиваетесь: некоторые разработчики используют двойную тильду `~~`, чтобы отбросить десятичную часть `number` (т.е. "привести" его к целому числу "integer"). Часто утверждается (хотя и ошибочно), что это тот же самое, что и вызов `Math.floor(..)`. Как работает `~~`: первая `~` выполняет "приведение" `ToInt32` и побитовую инверсию, затем вторая `~` выполняет еще одно побитовое переключение, возвращая все биты обратно в исходное состояние. Итог - просто "приведение" `ToInt32` (оно же усечение/отбрасывание). **Примечание:** Побитовая двойная инверсия `~~` очень похожа на действие двойного отрицания `!!`, которое рассматривается ниже в разделе "Явно: * --> Булево значение". Однако `~~` нуждается в некотором предостережении/разъяснении. Во-первых, он корректно работает только с 32-разрядными значениями. Но что еще более важно, с отрицательными числами она действует не так, как `Math.floor (..)`! ```js Math.floor( -49.6 ); // -50 ~~-49.6; // -49 ``` Оставляя в стороне отличие `Math.floor(..)`, `~~x` усекает до (32-разрядного) целого числа. Но то же самое делает и `x | 0`, и, по-видимому, с (немного) *меньшими усилиями*. Итак, почему же тогда вы могли бы выбрать `~~x` вместо `x | 0`? Приоритет операторов (см. Главу 5): ```js ~~1E20 / 10; // 166199296 1E20 | 0 / 10; // 1661992960 (1E20 | 0) / 10; // 166199296 ``` Как и во всех других советах здесь, используйте `~` и `~~` в качестве явного механизма "приведения" и преобразования значений только в том случае, если каждый, кто читает/пишет такой код, в курсе, как работают эти операторы! ### Явно: Парсинг числовых строк Аналогичный приведению `string` к `number` результат можно получить путем выявления чисел в символах строки. Однако есть явные различия между таким парсингом и преобразованием типов, рассмотренным ранее. Взгляните: ```js var a = "42"; var b = "42px"; Number( a ); // 42 parseInt( a ); // 42 Number( b ); // NaN parseInt( b ); // 42 ``` Парсинг числового значения из строки *терпим* к нечисловым символам - он просто прекращает синтаксический анализ слева направо при их обнаружении. Приведение же *не терпимо* и приводит к значению `NaN`. Парсинг не следует рассматривать как замену приведению. Эти две задачи, хотя и схожи, имеют разные цели. Разбирайте `string` как `number`, когда вы не знаете / неважно, появятся ли нечисловые символы с правой стороны. Приводите `string` (к `number`), когда допустимы лишь числовые значения, а что-то вроде `"42px"` должно быть отклонено. **Совет:** `parseInt(..)` имеет двойника, `parseFloat(..)`, который (как понятно) извлекает из строки число с плавающей запятой. Не забывайте, что `parseInt(..)` работает со значениями `string`. Нет абсолютно никакого смысла передавать значение `number` в `parseInt(..)`. Также не имеет смысла передавать значение любого другого типа, например, `true`, `function(){..}` или `[1,2,3]`. Если вы передаете значение, отличное от `string`, то оно сначала автоматически приводится в `string` (см. "`ToString`" выше), что очевидно своего рода скрытое *неявное* приведение. Это действительно плохая идея полагаться на такое поведение в вашей программе, поэтому никогда не используйте `parseInt(..)` со значением, отличным от `string`. До ES5 была другая ошибка с `parseInt(..)`, которая служила источником ошибок во многих программах на JS. Если вы не передавали второй аргумент (radix), чтобы обозначить, какую систему счисления использовать для интерпретации строки, `parseInt(..)` анализировала первые символы строки и делала предположения. Если первые два символа `"0x"` или `"0X"`, предполагалось (по соглашению), что вы хотели интерпретировать `string` как шестнадцатеричное `number` (основание - 16). Иначе, если первый символ `"0"`, предполагалось (опять же, по соглашению), что вы хотели интерпретировать `string` как восьмеричное `number` (основание - 8). Шестнадцатеричную строку (с начальным значением `0x` или `0X`) не так-то просто перепутать. Но угадывание восьмеричных чисел оказалось дьявольским явлением. Например: ```js var hour = parseInt( selectedHour.value ); var minute = parseInt( selectedMinute.value ); console.log( "The time you selected was: " + hour + ":" + minute); ``` Выглядит безобидным, правда? Попробуйте взять `08` для часа и `09` для минуты. Вы получите `0:0`. Почему? потому что ни `8`, ни `9` не являются допустимыми символами в восьмеричной системе. До ES5 заплатка была простой, но её так легко забыть: **всегда передавайте `10` в качестве второго аргумента**. Это было абсолютно безопасно: ```js var hour = parseInt( selectedHour.value, 10 ); var minute = parseInt( selectedMiniute.value, 10 ); ``` Начиная с ES5, `parseInt(..)` больше не угадывает восьмеричные числа. Если вы не скажете обратное, он предполагает базу - 10 (или базу - 16 для префиксов `"0x"`). Это гораздо приятнее. Просто будьте осторожны, если ваш код должен выполняться в окружении до ES5.В этом случае вам нужно передать `10` в radix (основание). #### Парсинг не строк Один печально известный пример поведения `parseInt(..)` был опубликован в саркастически-шутливом посте несколько лет назад: ```js parseInt( 1/0, 19 ); // 18 ``` Предположительное (но совершенно неверное) утверждение гласило: "Если я передам Infinity и разберу её в целое число, то я должен получить Infinity, а не 18". Вероятно, JS совсем без ума раз дает такой результат, верно? Хотя этот пример явно надуман и нереален, давайте на мгновение предадимся безумию и посмотрим, действительно ли JS настолько дурацкий. Во-первых, самый очевидный грех, совершенный здесь, заключается в передаче не-`string` в `parseInt(..)`. Этого не должно быть. Делая так, ты напрашиваешься на неприятности. Но даже совершая такое, JS вежливо приведет то, что вы передаете, в `string`, которую он затем может попытаться распарсить. Некоторые могут возразить, что это неоправданное поведение, и что `parseInt(..)` должен отказаться работать со значением, отличным от `string`. Возможно, это должно выдавать ошибку? Честно говоря, это было бы очень похоже на Java. Я содрогаюсь при мысли, что JS должен начать выдавать ошибки повсюду, так что `try..catch` требуется почти в каждой строке. Должен ли он возвращать `NaN`? Может быть. Но... как насчет: ```js parseInt( new String( "42") ); ``` Потерпит ли это тоже неудачу? Это значение, отличное от `string`. Если вы хотите, чтобы объект-обёртка `String` был распакован в `"42"`, то действительно ли так необычно, что `42` сначала становится `"42"`, чтобы потом превратиться в `42`? Я бы сказал, что это наполовину *явное* и наполовину *неявное* приведение, которое может иметь место, и может быть очень полезным. Например: ```js var a = { num: 21, toString: function() { return String( this.num * 2 ); } }; parseInt( a ); // 42 ``` Тот, что `parseInt(..)` принудительно приводит значение к `string` перед парсингом, вполне разумно. Если вы кладёте мусор и вынимаете его обратно, не вините мусорное ведро - оно просто добросовестно выполняло свою работу. Итак, если вы передаете значение типа `Infinity` (очевидный результат `1 / 0`), какое `string`-представление имеет наибольший смысл для его дальнейшего приведения? На ум приходят только два разумных варианта: `"Infinity"` и `"∞"`. JS выбрал `"Infinity"` и я рад, что он так поступил. Я думаю, это хорошо, что **все значения** в JS имеют какое-то `string` представление по умолчанию, поэтому они не таинственные черные ящики, которые мы не можем отлаживать и анализировать их. Хорошо, что насчет основания 19? Очевидно, совершенно фальшивая и надуманная. Никакие реальные JS-программы не используют основание 19. Это абсурд. Но опять же, давайте предадимся нелепице. В системе с основанием 19 допустимыми числовыми символами являются `0` - `9` и `a` - `i` (без учета регистра). Итак, вернемся к нашему примеру `parseInt( 1/0, 19 )`. По сути, это `parseInt( "Infinity", 19 )`. Как это анализируется? Первый символ - `"I"` - в глупой девятнадцатеричной системе имеет значение `18`. Второй символ `"n"` в допустимом наборе числовых символов отсутствует, и поэтому анализ просто вежливо останавливается, точно так же, как когда он наткнулся на `"p"` в `"42px"`. Результат? `18`. Получилось именно так, как и должно быть. Логика привела нас сюда, а не к ошибке или к `Infinity`, что **очень важно** для JS, и поэтому его не следует так легко отбрасывать. Приведём ещё примеры такого поведения `parseInt(..)`, которые могут показаться удивительными, но которые вполне разумные: ```js parseInt( 0.000008 ); // 0 ("0" из "0.000008") parseInt( 0.0000008 ); // 8 ("8" из "8e-7") parseInt( false, 16 ); // 250 ("fa" из "false") parseInt( parseInt, 16 ); // 15 ("f" из "function..") parseInt( "0x10" ); // 16 parseInt( "103", 2 ); // 2 ``` `parseInt(..)` на самом деле довольно предсказуем и последователен в своем поведении. Если вы будете использовать его правильно, вы получите разумные результаты. Если вы используете его неправильно, сумасшедшие результаты, которые вы получаете, не являются ошибкой JavaScript. ### Явно: * --> Булево значение Теперь давайте рассмотрим приведение любого значения, не являющегося `boolean`, к `boolean`. Точно так же, как со `String(..)` и `Number(..)` выше, `Boolean(..)` (без `new`, конечно!) - это явный способ приведения `ToBoolean`.: ```js var a = "0"; var b = []; var c = {}; var d = ""; var e = 0; var f = null; var g; Boolean( a ); // true Boolean( b ); // true Boolean( c ); // true Boolean( d ); // false Boolean( e ); // false Boolean( f ); // false Boolean( g ); // false ``` Хотя `Boolean (..)` явно выражает намерение, он мало распространен или не идиоматичен. Точно так же, как унарный оператор `+` преобразует значение в `number` (см. выше), унарный оператор отрицания `!` явно преобразует значение в `boolean`. *Проблема* заключается в том, что он также меняет значение с истинного на ложное или наоборот. Поэтому, самый популярный способ явного приведения к `boolean`, к которому прибегают JS разработчики, - это оператор двойного отрицания `!!`. В нём второй `!` вернет исходный `true`/`false`: ```js var a = "0"; var b = []; var c = {}; var d = ""; var e = 0; var f = null; var g; !!a; // true !!b; // true !!c; // true !!d; // false !!e; // false !!f; // false !!g; // false ``` Любое приведение `ToBoolean` выполняется *неявно* без `Boolean(..)` или `!!`, если используется в `boolean` контексте, таком как оператор `if (..) ..`. Но наша цель - это явное приведение значения к `boolean`, чтобы было понятно, что мы используем намеренное приведение `ToBoolean`. Другой пример использования явного приведения `ToBoolean` - это, когда вы хотите выполнить приведение к `true`/`false` при сериализации структуры данных в формате JSON: ```js var a = [ 1, function(){ /*..*/ }, 2, function(){ /*..*/ } ]; JSON.stringify( a ); // "[1,null,2,null]" JSON.stringify( a, function(key,val){ if (typeof val == "function") { // выполнить приведение `ToBoolean` для функции return !!val; } else { return val; } } ); // "[1,true,2,true]" ``` Если вы пришли в JavaScript из Java, вы можете узнать эту идиому: ```js var a = 42; var b = a ? true : false; ``` Тернарный оператор `? :` проверит `a` на правдивость и на основе этого присвоит `b` либо `true`, либо `false`. На первый взгляд эта идиома выглядит как форма *явного* приведения `ToBoolean`, поскольку, очевидно, операция выдает `true` или `false`. Однако присутствует скрытое *неявное* приведение, заключающееся в том, что выражение `a` сначала должно быть приведено в `boolean` для прохождения теста на правдивость. Я бы назвал эту идиому "явно неявной". Кроме того, я бы посоветовал вам **всегда избегайте эту идиому** в JavaScript. Это не дает никакой пользы, но, что куда хуже, выдает себя за то, чем оно не является. `Boolean(a)` и `!!a` намного лучше в качестве *явных* вариантов приведения. ## Неявное приведение *Неявное* приведение относится к скрытым преобразованиям типов с неочевидными побочными эффектами, которые неявно возникают в результате других действий. Другими словами, *неявные приведения* - это любые преобразования типов, которые не очевидны (для вас). Если цель *явного* приведения ясна (сделать код однозначным и более понятным), то также понятно, что *неявное* приведение преследует противоположную цель: усложнить понимание кода. Если принять всё за чистую монету, то, я полагаю, это источник большей части гнева по отношению к приведению. Большинство жалоб на "приведение JavaScript" на самом деле касаются (осознано или нет) *неявного* приведения. **Примечание:** Дуглас Крокфорд, автор книги *"JavaScript: сильные стороны"*, утверждал во многих выступлениях на конференциях и в статьях, что следует избегать приведения в JavaScript. Но что он, по-видимому, имеет в виду, так это то, что *неявное* приведение - это плохо (по его мнению). Однако, если вы прочтете его собственный код, вы найдете множество примеров приведения, как *неявного*, так и *явного*! По правде говоря, его беспокойство, похоже, в первую очередь направлено на операцию `==`, но, как вы увидите в этой главе, это только часть механизма приведения. Итак, **является ли скрытое приведение** злом? Это опасно? Является ли это недостатком в дизайне JavaScript? Должны ли мы избегать этого любой ценой? Бьюсь об заклад, большинство из вас, читатели, склонны с энтузиазмом воскликнуть: "Да!" **Не так быстро**. Выслушай меня. Давайте взглянем с другой точки зрения на то, что такое *неявное* приведение и каким оно может быть, а не просто заявить, что оно "противоположность хорошему явному виду приведения". Это слишком узко и упускает важный нюанс. Давайте определим цель *неявного* приведения как: уменьшить многословие, шаблонность и/или ненужные детали реализации, которые загрязняют наш код шумом, отвлекающим от более важной цели. ### Упрощение неявного Прежде чем мы перейдем к JavaScript, позвольте мне предложить что-нибудь псевдокодовое из какого-нибудь теоретического строго типизированного языка для иллюстрации: ```js SomeType x = SomeType( AnotherType( y ) ) ``` В этом примере у меня есть некоторый произвольный тип значения в `y`, который я хочу преобразовать в тип `SomeType`. Проблема в том, что этот язык не может перейти непосредственно от того, чем в данный момент является `y`, к `SomeType`. Ему нужен промежуточный шаг, на котором он сначала преобразуется в `AnotherType`, а затем из `AnotherType` в `SomeType`. Теперь, что, если бы этот язык просто позволил вам написать: ```js SomeType x = SomeType( y ) ``` Разве вы в целом не согласились бы с тем, что мы упростили преобразование типов, чтобы уменьшить ненужный "шум" промежуточного этапа преобразования? Я имею в виду, *действительно* ли так важно, прямо здесь, на этом этапе кода, увидеть и смириться с тем фактом, что `y` сначала переходит в `AnotherType`, а затем в `SomeType`? Кто-то мог бы возразить, что, по крайней мере, при некоторых обстоятельствах, да. Но я думаю, что во многих других обстоятельствах можно привести равный аргумент в пользу того, что здесь упрощение **на самом деле способствует удобочитаемости кода** путем абстрагирования или сокрытия таких деталей в самом ли языке, или в наших собственных абстракциях. Несомненно, где-то за кулисами все еще происходит промежуточный этап преобразования. Но если здесь эта деталь скрыта от глаз, мы можем просто рассуждать о том, что преобразуем `y` в `SomeType` в виде стандартной операции, скрывая ненужные детали. Хотя это и не идеальная аналогия, я собираюсь утверждать на протяжении всей остальной части этой главы, что *неявное* приведение JS можно рассматривать как оказание аналогичной помощи вашему коду. Но, **и это очень важно**, это не безграничное, абсолютное утверждение. Определенно, вокруг *неявного* приведения скрывается множество *пороков*, которые нанесут вашему коду гораздо больший вред, чем любые потенциальные улучшения читаемости. Очевидно, что мы должны научиться избегать таких конструкций, чтобы не отравлять наш код всевозможными ошибками. Многие разработчики считают, что если механизм может делать какую-то полезную вещь **A**, но также может быть использован не по назначению для совершения какой-то ужасной вещи **Z**, то мы должны полностью отказаться от него, просто на всякий случай. Мой совет вам таков: не соглашайтесь на это. Не "выплескивайте ребенка вместе с водой". Не думайте, что *неявное* приведение - это плохо, потому что все, что, по вашему мнению, вы когда-либо видели, - это его "плохие стороны". Я думаю, что здесь есть "сильные стороны", и я хочу помочь и вдохновить большинство из вас найти и принять их! ### Неявно: Строки <--> Числа Ранее в этой главе мы исследовали *явное* приведение между значениями `string` и `number`. Теперь давайте рассмотрим ту же задачу, но с использованием методов *неявного* приведения. Но прежде чем мы это сделаем, мы должны изучить некоторые нюансы операций, которые *неявно* приведут к приведению. Оператор `+` очень занятой. Он и складывает `number`, и сцепляет `string`. Итак, как JS догадывается, какой тип операции вы хотите использовать? Взгляните: ```js var a = "42"; var b = "0"; var c = 42; var d = 0; a + b; // "420" c + d; // 42 ``` Где та разница, которая формирует либо `"420"`, либо `42`? Распространенное заблуждение, что разница в том, является ли один или оба операнда `string`, поскольку это означает, что `+` предполагает сцепление `string`. Хотя это отчасти верно, но все гораздо сложнее. Рассмотрим: ```js var a = [1,2]; var b = [3,4]; a + b; // "1,23,4" ``` Ни один из этих операндов не является `string`, но очевидно, что они оба были приведены к `string`, а затем произошло сцепление `string`. Так что же происходит на самом деле? (**Предупреждение:** грядет очень подробный разбор спецификации, так что пропустите следующие два абзаца, если это вас пугает!) ----- Согласно разделу 11.6.1 спецификации ES5, алгоритм `+` (когда `object`-значение является операндом) будет сцеплять, если любой из операндов либо уже является `string`, либо если следующие шаги ведут к `string`. Итак, когда `+` получает `object` (включая `array`) для любого из операндов, он для значения сначала вызывает абстрактную операцию `ToPrimitive` (раздел 9.1), которая затем вызывает алгоритм `[[DefaultValue]]` (раздел 8.12.8) с контекстной подсказкой `number`. Если вы будете внимательны, то заметите, что эта операция теперь идентична тому, как абстрактная операция `ToNumber` обрабатывает `object` (см. раздел "`ToNumber`"" выше). Операция `valueOf()` над `array` не приведет к созданию простого примитива, поэтому затем он переходит к `toString()`. Так, два массива становятся `"1,2"` и `"3,4"` соответственно. Теперь `+` сцепляет две `string` так, как вы ожидаете: `"1,23,4"`. ----- Давайте оставим в стороне эти запутанные детали и вернемся к более раннему, упрощенному объяснению: если любой из операндов `+` является `string` (или становится таковым с помощью описанных выше шагов!), операция будет сцеплением `string`. В противном случае это всегда числовое сложение. **Примечание:** Часто цитируемый пример приведения - `[] + {}` против `{} + []`, которые приводят, соответственно, к `"[object Object]"` и `0`. Однако это еще не все, и мы рассмотрим эти детали в разделе "Блоки" в Главе 5. Что это значит для *неявного* приведения? Вы можете привести `number` к `string`, просто "добавив" `number` и `""` - пустую `string`: ```js var a = 42; var b = a + ""; b; // "42" ``` **Совет:** Числовое сложение с помощью оператора `+` является коммутативным, что означает, что `2 + 3` совпадает с `3 + 2`. Объединение строк с помощью `+`, очевидно, обычно не является коммутативным, **но** в конкретном случае `""` оно эффективно коммутативно, так как `a + ""` и `"" + a` приведут к одному и тому же результату. Чрезвычайно популярно / идиоматично (*неявно*) приводить `number` к `string` с помощью операции `+ ""`. Интересно, что даже некоторые из самых ярых критиков *неявного* приведения все еще используют этот подход в своем собственном коде вместо одной из его *явных* альтернатив. **Я думаю, что это отличный пример** полезной формы в *неявном* приведении, несмотря на то, как часто этот способ подвергается критике! Сравнивая *неявное* приведение `a + ""` с предыдущим примером *явного* приведения `String(a)`, необходимо учитывать еще одну особенность. Следуя логике работы абстрактной операции `ToPrimitive`, сначала `a + ""` вызывает `valueOf()` для значения `a`, затем возвращаемое значение преобразуется в `string` с помощью внутренней абстрактной операции `ToString`. В то же время `String(a)` просто вызывает `toString()` напрямую. Оба способа в конечном итоге приводят к `string`, но если вы используете `object` вместо обычного примитивного `number`, вы не обязательно можете получить *такое же* значение `string`! Рассмотрим: ```js var a = { valueOf: function() { return 42; }, toString: function() { return 4; } }; a + ""; // "42" String( a ); // "4" ``` Как правило, такого рода уловки вам не вредят, если вы только действительно не пытаетесь создавать запутанные структуры данных и методы, но вам следует быть осторожным, если вы пишете свои собственные методы `valueOf()` и `toString()` для какого-то `object`, поскольку то, как вы приводите значение, может повлиять на результат. А как насчет другого направления? Как мы можем *неявно привести* из `string` в `number`? ```js var a = "3.14"; var b = a - 0; b; // 3.14 ``` Оператор `-` определен только для числового вычитания, поэтому `a - 0` вызывает приведение значения `a` к `number`. Куда менее распространенные, `a * 1` и `a / 1` также приводят к тому же результату, ибо эти операторы предназначены только для числовых операций. Как насчет значений `object` с оператором `-`? Аналогичная история, как и для `+` выше: ```js var a = [3]; var b = [1]; a - b; // 2 ``` Оба значения `array` должны стать `number`, но сначала они преобразуются в `strings` (с использованием ожидаемой сериализации `toString()`), а затем преобразуются в `number` для выполнения вычитания `-`. Итак, является ли *неявное* приведение к `string` и `number` тем уродливым злом, о котором вы слышите в страшилках? Лично я так не думаю. Сравните `b = String(a)` (*явный*) с `b = a + ""` (*неявный*). Я думаю, что можно привести примеры того, что оба подхода полезны в вашем коде. Конечно, `b = a + ""` немного чаще встречается в JS программах, демонстрируя свою полезность, невзирая на *чувства* по поводу достоинств или опасностей *неявного* приведения в целом. ### Неявно: Булевы значения --> Числа По-моему, ситуация, когда *неявное* приведение может проявить свои лучшие стороны, - это случай упрощения определенных типов сложной `boolean` логики в виде простого числового сложения. Конечно, это не техника общего назначения, а конкретное решение для особых случаев. Рассмотрим: ```js function onlyOne(a,b,c) { return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c)); } var a = true; var b = false; onlyOne( a, b, b ); // true onlyOne( b, a, b ); // true onlyOne( a, b, a ); // false ``` Функция `onlyOne(..)` должна возвращать `true` только в том случае, если лишь один из её аргументов является `true` / правдивым. Она использует *неявное* приведение в проверках на правдивость и *явное* приведение в остальных случаях, включая возвращаемое итоговое значение. Но что, если нам нужно, чтобы эта утилита могла обрабатывать четыре, пять или двадцать флагов одним и тем же способом? Довольно сложно представить реализацию кода, который обрабатывал бы все эти перестановки сравнений. Вот где приведение `boolean` значений к `number` (очевидно, `0` или `1`) может сильно помочь: ```js function onlyOne() { var sum = 0; for (var i=0; i < arguments.length; i++) { // пропускаем ложные значения, // считая их 0, но избегайте NaN. if (arguments[i]) { sum += arguments[i]; } } return sum == 1; } var a = true; var b = false; onlyOne( b, a ); // true onlyOne( b, a, b, b, b ); // true onlyOne( b, b ); // false onlyOne( b, a, b, b, b, a ); // false ``` **Примечание:** Конечно, вместо цикла `for` в `onlyOne(..)` вы могли бы более лаконично использовать функцию ES5 `reduce(..)`, но я не хотел затуманивать концепцию. То, что мы здесь делаем, - это полагаемся на `1` для `true`/правдивых приведений и численно складываем их. `sum + = arguments[i]` использует *неявное* приведение, чтобы это произошло. Если одно и только одно значение в списке `arguments` равно `true`, то числовая сумма будет равна `1`, в противном случае сумма не будет равна `1` и, таким образом, желаемое условие не выполняется. Конечно, мы могли бы сделать тоже самое с помощью *явного* приведения: ```js function onlyOne() { var sum = 0; for (var i=0; i < arguments.length; i++) { sum += Number( !!arguments[i] ); } return sum === 1; } ``` Мы сначала используем `!!arguments[i]`, побуждая привести значения к `true` или `false`. Это для того, чтобы вы могли передавать значения, отличные от `boolean`, например, `onlyOne( "42", 0 )`, и утилита работала бы так, как ожидается (в противном случае вы получили бы сцепление `string`, и логика была бы неверной). После того, как мы чётко знаем, что это `boolean`, мы выполняем еще одно *явное* приведение с помощью `Number(..)`, чтобы получить `0` или `1`. Является ли *явная* форма приведения в этой утилите "лучше"? Она позволяет избежать ловушки `NaN`, как описано в комментариях к коду. Но, в конечном счете, это зависит от ваших потребностей. Лично я считаю, что первая версия, опирающаяся на *неявное* приведение, более элегантна (если вы не будете передавать `undefined` или `NaN`), а *явная* версия излишне многословна. Но, как и почти во всем, то, что мы здесь обсуждаем, это субъективные решения. **Примечание:** Независимо от подхода, *явного* или *неявного*, вы можете легко создать варианты `onlyTwo(..)` или `onlyFive(..)`, просто изменив итоговое сравнение с `1` на `2` или `5` соответственно. Это значительно проще, чем добавлять кучу выражений `&&` и `||`. Так что, в общем, приведение в данном случае очень полезно. ### Неявно: * --> Булево значение Теперь давайте обратим наше внимание на *неявное* приведение к `boolean` значениям, поскольку оно, безусловно, самое распространенное, а также всё ещё самое потенциально проблемное. Помните, *неявное* приведение - это то, что возникает, когда вы используете значение так, что приходится преобразовать значение. Для `number` и `string` операций довольно легко увидеть ситуации вынужденного приведения. Но какие операции требуют/заставляют (*неявно*) приводить к `boolean`? 1. Проверяемое выражение в операторе `if (..)`. 2. Проверяемое выражение (второе) в `for ( .. ; .. ; .. )`. 3. Проверяемое выражение в циклах `while (..)` и `do..while(..)`. 4. Проверяемое выражение (первое) в тернарном операторе `? :`. 5. Левый операнд (является проверяемым выражением - см. ниже!) в операторах `||` ("логическое или") и `&&` ("логическое и"). Любое значение, используемое в этих контекстах, которое еще не является `boolean`, будет *неявно* преобразовано в `boolean` с использованием правил абстрактной операции `ToBoolean`, рассмотренной выше в этой главе. Давайте взглянем на примеры: ```js var a = 42; var b = "abc"; var c; var d = null; if (a) { console.log( "угу" ); // угу } while (c) { console.log( "нет, никогда не выполнится" ); } c = d ? a : b; c; // "abc" if ((a && d) || c) { console.log( "угу" ); // угу } ``` Во всех этих контекстах значения, не являющиеся `boolean`, *неявно приводятся* к их `boolean` эквивалентам для выполнения проверки. ### Операторы `||` и `&&` Вероятно, вы видели операторы `||` ("логическое или") и `&&` ("логическое и") в большинстве или во всех других языках, которые вы использовали. Поэтому было бы естественно предположить, что в JavaScript они работают в основном так же, как и в других подобных языках. Есть очень малоизвестный, но очень важный нюанс. На самом деле, я бы сказал, что эти операторы даже не следует называть "логическими ___ операторами", поскольку такое название неполно описывает то, что они делают. Если бы я давал им более точное (хотя и более неуклюжее) название, я бы назвал их "операторами выбора" или, более полно, "операторами выбора операндов". Почему? Потому что в JavaScript они на самом деле не приводят к *логическому* значению (`boolean`), как это происходит в некоторых других языках. Так, к чему *они* приводят? Они приводят к значению одного (и только одного) из их двух операндов. Другими словами, **они выбирают одно из значений двух операндов**. Цитирую раздел 11.11 спецификации ES5: > Значение, создаваемое оператором && или ||, не обязательно имеет тип Boolean. Полученное значение всегда будет значением одного из выражений двух операндов. Давайте проиллюстрирую: ```js var a = 42; var b = "abc"; var c = null; a || b; // 42 a && b; // "abc" c || b; // "abc" c && b; // null ``` **Подождите, что!?** Подумайте об этом. В таких языках, как C и PHP, эти выражения приводят к `true` или `false`, но в JS (и Python, и Ruby, если уж на то пошло!) результат определяется самими значениями. Оба оператора `||` и `&&` выполняют `boolean` тест для **первого операнда** (`a` или `c`). Если операнд еще не является `boolean` (а здесь это не так), происходит обычное приведение `ToBoolean`, и проверка может быть выполнена. Для оператора `||`, если итог проверки `true`, выражение `||` возвращает значение *первого операнда* (`a` или `c`). Если итог проверки `false`, выражение `||` возвращает значение *второго операнда* (`b`). И наоборот для оператора `&&`. Если итог проверки `true`, выражение `&&` возвращает значение *второго операнда* (`b`). Если итог проверки `false`, выражение `&&` возвращает значение *первого операнда* (`a` или `c`). Результатом выражения `||` или `&&` всегда является значение одного из операндов, **а не** результат проверки (возможно, приведенный). В `c && b`, `c` равно `null` и, следовательно, является ложным. Но само выражение `&&` возвращает `null` (значение `c`), а не приведённый `false`, использованный при проверке. Теперь вы понимаете, что эти операторы действуют как "селекторы операндов"? Другой взгляд на эти операторы: ```js a || b; // примерно эквивалентно: a ? a : b; a && b; // примерно эквивалентно: a ? b : a; ``` **Примечание:** Я говорю `a || b` "примерно эквивалентно" `a ? a : b` потому что результат идентичен, но есть определённая разница. В `а ? a : b`, если бы `a` было более сложным выражением (например, которое могло бы иметь побочные эффекты, такие как вызов `function` и т.д.), то выражение `a`, возможно, было бы вычислено дважды (если первая оценка была правдивой). В `a || b`, напротив, выражение `a` вычисляется лишь один раз, а это значение используется и для проверки, и в качестве результата (если требуется). Тот же нюанс и в выражениях `a && b` и `a ? b : a`. Чрезвычайно распространенным и полезным применением данного поведения, которое вы, вероятно, уже использовали и не до конца поняли, следующее: ```js function foo(a,b) { a = a || "hello"; b = b || "world"; console.log( a + " " + b ); } foo(); // "hello world" foo( "yeah", "yeah!" ); // "yeah yeah!" ``` Идиома `a = a || "hello"` (иногда говорят, что это JavaScript-версия "оператора нулевого слияния" из C#) проверяет `a` и, если у переменной нет значения (или оно нежелательное ложное), то возвращает резервное значение по умолчанию (`"hello"`). Но **будь осторожен**! ```js foo( "That's it!", "" ); // "That's it! world" <-- Ой! ``` Видите проблему? `""` во втором аргументе - это ложное значение (см. `ToBoolean` ранее в этой главе). Поэтому проверка в `b = b || "world"` терпит неудачу и замещается значением по умолчанию `"world"`, хотя цель, вероятно, состояла в том, чтобы явно переданная пустая строка `""` была присвоена `b`. Эта идиома `||` чрезвычайно распространена и весьма полезна, но вы должны использовать ее только в тех случаях, когда *все ложные значения* должны быть проигнорированы. В противном случае вам следует быть более точным в своей проверке и, возможно, прибегнуть к тернарному оператору `? :`. Эта идиома *присвоения значения по умолчанию* настолько распространена (и полезна!), что даже те, кто публично и яростно осуждает приведение в JavaScript, часто используют ее в своем собственном коде! А как насчет `&&`? Есть еще одна идиома, которая несколько реже создается вручную, но которая часто используется JS минификаторами. Оператор `&&` "выбирает" второй операнд тогда и только тогда, когда первый операнд признаётся правдивым, и этот подход иногда называют "защитным оператором" (также см. "Короткое замыкание" в Главе 5) - проверка первого выражения "защищает" второе выражение: ```js function foo() { console.log( a ); } var a = 42; a && foo(); // 42 ``` `foo()` вызывается только потому, что `a` определяется, как правдивый. Если результат проверки ложный, то выполнение оператора в выражении `a && foo()` молча остановится - это называют "коротким замыканием" - и `foo()` никогда не будет вызван. Опять же, люди не так часто пишут подобные вещи. Обычно вместо этого они выбирают `if (a) { foo(); }`. Но минификаторы JS предпочитают `a && foo()`, потому что это намного короче. Итак, теперь, если вам когда-нибудь придется расшифровывать такой код, вы будете знать, что он делает и почему. Хорошо, итак, у `||` и `&&` есть несколько изящных трюков в рукаве, если вы готовы допустить *неявное* приведение в своей работе. **Примечание:** Обе идиомы `a = b || "something"` и `a && b()` опираются на короткое замыкание, которое мы более подробно рассмотрим в Главе 5. Тот факт, что эти операторы на самом деле не приводят к `true` и `false`, возможно, уже немного заморочил вам голову. И вы, вероятно, удивляетесь, как вообще работали все ваши операторы `if` и циклы `for`, если в них были сложносоставные логические выражения, такие как `a && (b || c)`. Не волнуйся! Мир не рушится. С вашим кодом (возможно) всё хорошо. Просто вы, наверное, никогда раньше не осознавали, что **после того, как** было вычислено составное выражение, происходило *неявное* приведение к `boolean`. Взгляните: ```js var a = 42; var b = null; var c = "foo"; if (a && (b || c)) { console.log( "yep" ); } ``` Этот код по-прежнему работает так, как вы всегда думали, за исключением одной незначительной дополнительной детали. Выражение `a && (b || c)` *на самом деле* возвращает `"foo"`, а не `true`. Итак, оператор `if` *затем* приводит значение `foo` к `boolean`, которое, конечно же, будет `true`. Видите? Нет причин для паники. Ваш код, вероятно, все еще в безопасности. Но теперь вы знаете больше о том, как он делает то, что делает. И теперь вы также понимаете, что такой код использует *неявное* приведение. Если вы все еще в лагере "избегайте (неявного) приведения", вам нужно будет вернуться к своему коду и сделать все эти проверки *явными*: ```js if (!!a && (!!b || !!c)) { console.log( "yep" ); } ``` Удачи вам в этом! ... Извините, просто подтруниваю. ### Приведение Symbol До сего момента не было почти никакой заметной разницы в результатах между *явным* и *неявным* приведением - на кону была только читаемость кода. Но Symbol из ES6 вносят подвох в систему приведения, который нам нужно кратко обсудить. По причинам, которые выходят далеко за рамки того, что мы обсуждаем в этой книге, допускается *явное* преобразование `symbol` в `string`, но *неявное* приведение того же самого запрещено и выкидывает ошибку. Рассмотрим: ```js var s1 = Symbol( "cool" ); String( s1 ); // "Symbol(cool)" var s2 = Symbol( "not cool" ); s2 + ""; // TypeError ``` Значения `symbol` вообще нельзя привести к `number` (всегда выдает ошибку), но, как ни странно, их можно как *явно*, так и *неявно* приводить к `boolean` (всегда `true`). Согласованной системе всегда легче научиться, а с исключениями всегда невесело иметь дело. Нам просто нужно быть осторожными с новыми `symbol` из ES6 и с тем, как их приводить. Хорошая новость: вероятно, вам крайне редко понадобится приводить значение `symbol`. То, как они обычно используются (см. Главу 3), не требует их приведения на постоянной основе. ## Обычное равенство vs. Строгое равенство Обычное равенство - это оператор `==`, а строгое равенство - это оператор `===`. Оба оператора используются для сравнения двух значений на "равенство". Это размежевание на "обычное" и "строгое" указывает на **очень важную** разницу в поведении между ними, в частности, в том, как они определяют "равенство". Очень распространенное заблуждение относительно этих двух операторов таково: "`==` проверяет значения на равенство, а `===` проверяет как значения, так и типы на равенство". Хотя это звучит красиво и разумно, но неточно. Бесчисленные уважаемые книги и блоги по JavaScript говорят именно об этом, но, к сожалению, все они *неправы*. Правильное описание таково: "`==` допускает приведение при сравнении на равенство, а `===` запрещает приведение". ### Быстродействие равенства Остановитесь и подумайте о разнице между первым (неточным) и этим вторым (точным) объяснением. В первом объяснении кажется очевидным, что `===` *выполняет больше работы*, чем `==`, потому что он должен *также* проверять тип. Во втором объяснении `==` - это то, что *выполняет больше работы*, потому что он должен выполнять приведение, если типы разные. Не попадайтесь в эту ловушку, думая как большинство, что это как-то связано с производительностью - будто бы `==` медленнее, чем `===`. Действительно это можно замерить и увидеть, что приведение действительно занимает *некоторое* время, но это всего лишь микросекунды (да, это миллионные доли секунды!). Если вы сравниваете два значения одного и того же типа, `==` и `===` используют идентичный алгоритм, и поэтому, за исключением незначительных различий в реализации движка, они делают одну и ту же работу. Если вы сравниваете два значения разных типов, производительность не является важным фактором. Что вы должны спросить себя: сравнивая эти две величины, нужно ли мне приведение или нет? Если вам нужно приведение, используйте `==` обычного равенства, но, если вы не хотите приведения, используйте `===` строгого равенства. **Примечание:** Здесь подразумевается, что и `==`, и `===` проверяют типы своих операндов. Разница заключается в том, как они реагируют, если типы не совпадают. ### Абстрактное равенство Поведение оператора `==` определено в разделе 11.9.3 "Абстрактный алгоритм сравнения равенства" спецификации ES5. То, что там перечислено, - это всеобъемлющий, но простой алгоритм, в котором явно указаны все возможные комбинации типов и то, как приведения (при необходимости) должны выполняться для каждой комбинации. **Предупреждение:** Когда (*неявное*) приведение поносят как слишком сложное и ущербное, чтобы быть "полезной сильной стороной", осуждаются именно эти правила "абстрактного равенства". Как правило, говорят, что они слишком сложны и неинтуитивны для практического изучения и использования разработчиками, и что они больше склонны вызывать ошибки в программах JS, чем обеспечивать большую читаемость кода. Я считаю, что это ошибочная предпосылка - что вы, читатели, являетесь компетентными разработчиками, которые пишут (читают и понимают!) алгоритмы (он же код) весь день напролет. Итак, далее следует простое изложение "абстрактного равенства" в простых терминах. Но я умоляю вас также прочитать раздел 11.9.3 спецификации ES5. Я думаю, вы будете удивлены тем, насколько это разумно. По сути, в первом пункте (11.9.3.1) говорится, что, если два сравниваемых значения имеют один и тот же тип, они просто и естественно сравниваются на Идентичность, как и следовало ожидать. Например, `42` равно только `42`, а `"abc"` равно только `"abc"`. Некоторые незначительные исключения из обычных ожиданий, о которых следует знать: * `NaN` никогда не равен самому себе (см. Главу 2) * `+0` и `-0` равны друг другу (см. Главу 2) Последнее положение пункта 11.9.3.1 описывает обычное равенство `==` для `object` значений (включая `function` и `array`). Два таких значения *равны* только в том случае, если они оба являются ссылками на *одно и то же значение*. Здесь не происходит никакого приведения. **Примечание:** Алгоритм сравнения для строгого равенства `===` определен идентично пункту 11.9.3.1, включая положение о двух значениях `object`. Это совсем малоизвестный факт, что **`==` и `===` ведут себя одинаково** в случае, когда сравниваются два `object`! Остальная часть алгоритма в 11.9.3 говорит, что, если вы используете обычное равенство `==` для сравнения двух значений разных типов, одно или оба значения должны быть *неявно* приведены. Это приведение выполняется таким образом, что оба значения в конечном итоге оказываются одного типа, и могут быть напрямую сопоставлены на Идентичность значений. **Примечание:** Операция `!=` обычного неравенства определяется точно так, как вы ожидаете, в том смысле, что это буквально выполнение операция `==`, а затем отрицание (инверсия) результата. То же самое относится и к строгой операции неравенства `!==`. #### Сравнение: `string` и `number` Чтобы проиллюстрировать приведение `==`, давайте сначала рассмотрим примеры `string` и `number`, приведенные ранее в этой главе: ```js var a = 42; var b = "42"; a === b; // false a == b; // true ``` Как и следовало ожидать, `a === b` терпит неудачу, потому что никакое приведение не допускается, и действительно, значения `42` и `"42"` различные. Однако второе сравнение `a == b` использует обычное равенство, что означает, что если типы окажутся разными, то алгоритм сравнения выполнит *неявное* приведение одного или обоих значений. Но какой вид приведения здесь выполняется? Приводится ли значение `a`, равное `42`, к `string`, или значение `b`, равное `"42"`, становится `number`? В спецификации ES5 в пунктах 11.9.3.4-5 говорится: > 4. Если Type(x) -- Number, а Type(y) -- String, > вернуть результат сравнения x == ToNumber(y). > 5. Если Type(x) -- String, а Type(y) -- Number, > вернуть результат сравнения ToNumber(x) == y. **Предупреждение:** Спецификация использует `Number` и `String` в качестве формальных имен для типов, а эта книга предпочитает `number` и `string` для обозначения примитивных типов. Не позволяйте заглавным буквам `Number` в спецификации ввести вас в заблуждение и принять это за функцию `Number()`. Для наших целей заглавные буквы в названии типа не имеют значения - в целом они имеют одно и то же значение. Очевидно, что спецификация говорит, что для сравнения значение `"42"` приведено к `number`. *Как* приведено - ранее было рассмотрено - с помощью абстрактной операции `ToNumber`. В этом случае совершенно очевидно, что результирующие два значения `42` равны. #### Сравнение: Любого с `boolean` Одна из самых больших ошибок с *неявным* приведением в обычном равенстве `==` возникает, когда вы пытаетесь напрямую сравнить значение с `true` или `false`. Рассмотрим: ```js var a = "42"; var b = true; a == b; // false ``` Подождите, что здесь произошло!? Мы знаем, что `"42"` - это истинное значение (см. выше в этой главе). Итак, как вышло что в `==` оно не равно `true`? Причина одновременно проста и обманчиво хитра. Это так легко неправильно понять, что многие разработчики JS никогда не уделяют достаточно пристального внимания, чтобы полностью понять это. Давайте еще раз процитируем спецификацию, пункты 11.9.3.6-7: > 6. Если Type(x) -- Boolean, > вернуть результат сравнения ToNumber(x) == y. > 7. Если Type(y) -- Boolean, > вернуть результат сравнения x == ToNumber(y). Давайте разберемся с этим. Во-первых: ```js var x = true; var y = "42"; x == y; // false ``` `Type(x)` действительно `Boolean`, поэтому выполняется `ToNumber(x)`, которое приводит `true` в `1`. Следом анализируется `1 == "42"`. Типы по-прежнему разные, поэтому (по сути, рекурсивно) мы прибегаем к алгоритму, который, как и выше, преобразует `"42"` в `42`, а `1 == 42` явно является `false`. Поменяем направление на противоположное, и мы все равно получим тот же результат: ```js var x = "42"; var y = false; x == y; // false ``` На этот раз `Type(y)` является `Boolean`, поэтому `ToNumber(y)` дает `0`, а `"42" == 0` рекурсивно становится `42 == 0`, что, конечно же, является `false`. Другими словами, **значение `"42"` не является ни `== true`, ни `== false`**. На первый взгляд это утверждение может показаться безумным. Как значение может быть ни правдивым, ни ложным? Но в этом-то и проблема! Вы задаете совершенно неправильный вопрос. На самом деле это не твоя вина. Твой мозг обманывает тебя. `"42"` действительно правдивое, но `"42" == true` **вообще не выполняет логический тест/приведение**, независимо от того, что говорит ваш мозг. `"42"` *не* приводится к `boolean` (`true`), а вместо этого `true` преобразуется в `1`, а затем `"42"` конвертируется в `42`. Нравится нам это или нет, `ToBoolean` здесь даже не задействован, поэтому правдивость или ложность `"42"` не имеет отношения к операции `==`! Единственно, что *важно*, - это понять, как алгоритм `==` сравнения ведет себя с разными комбинациями типов. Поскольку это относится и к `boolean` по обе стороны от `==`, запомните - `boolean` всегда *сначала* приводится к `number`. Если вам это кажется странным, вы не одиноки. Лично я рекомендовал бы ни при каких обстоятельствах не использовать `== true` или `== false`. Никогда. Но помните, я говорю только о `==`. `=== true` и `=== false` не делают приведения типа, поэтому они защищены от этого скрытого приведения `ToNumber`. Рассмотрим: ```js var a = "42"; // плохо (не пройдет проверку!): if (a == true) { // .. } // тоже плохо (не пройдет проверку!): if (a === true) { // .. } // приемлемо (неявное приведение): if (a) { // .. } // лучше (явное приведение): if (!!a) { // .. } // тоже хорошо (явное приведение): if (Boolean( a )) { // .. } ``` Если вы будете избегать в своем коде `== true` или `== false` (обычное равенство с `boolean`), вам никогда не придется беспокоиться об этой ментальной ловушки правдивости/ложности. #### Сравнение: `null` с `undefined` Другой пример *неявного* приведения можно наблюдать в случае обычного равенства `==` между значениями `null` и `undefined`. Еще раз цитирую спецификацию ES5, пункты 11.9.3.2-3: > 2. Если x равно `null`, а y -- `undefined`, вернуть значение `true`. > 3. Если x равно `undefined`, а y -- `null`, вернуть значение `true`. `null` и `undefined` при обычном равенстве `==` приравниваются (приводятся) друг к другу (а также к самим себе, очевидно) и к никаким другим значениям языка. Это означает, что `null` и `undefined` могут рассматриваться как неразличимые для целей сравнения, если вы используете оператор обычного равенства `==` для их взаимного *неявного* приведения. ```js var a = null; var b; a == b; // true a == null; // true b == null; // true a == false; // false b == false; // false a == ""; // false b == ""; // false a == 0; // false b == 0; // false ``` Приведение между `null` и `undefined` безопасно и предсказуемо, и никакие другие значения не могут давать ложных срабатываний при такой проверке. Я рекомендую использовать это приведение, чтобы `null` и `undefined` были неразличимы и, таким образом, рассматривались как одно и то же значение. Например: ```js var a = doSomething(); if (a == null) { // .. } ``` Проверка `a == null` пройдет только в том случае, если `doSomething()` возвращает либо `null`, либо `undefined`, и завершится ошибкой с любым другим значением, даже с другими ложными значениями, такими как `0`, `false` и `""`. *Явная* форма проверки, запрещающее любое такое приведение, (на мой взгляд) неоправданно сильно уродливее (и, возможно, чуть менее производительна!): ```js var a = doSomething(); if (a === undefined || a === null) { // .. } ``` По-моему, форма `a == null` - это еще один пример, когда *неявное* приведение улучшает читаемость кода, и делает это безопасным способом. #### Сравнение: `object` с не-`object` Пункты 11.9.3.8-9 спецификация ES5 гласят, что, если `object`/`function`/`array` сравнивается с простым скалярным примитивом (`string`, `number` или `boolean`), то: > 8. Если Type(x) является String или Number, а Type(y) - Object, > вернуть результат сравнения x == ToPrimitive(y). > 9. Если Type(x) - Object, а Type(y) - String или Number, > вернуть результат сравнения ToPrimitive(x) == y. **Примечание:** Вы можете заметить, что в этих положениях упоминаются только `String` и `Number`, но не `Boolean`. Это потому, что пункты 11.9.3.6-7 (см. выше) позаботятся о том, чтобы сначала привести `Boolean` операнд к `Number`. Рассмотрим: ```js var a = 42; var b = [ 42 ]; a == b; // true ``` Для значения `[ 42 ]` вызывается абстрактная операция `ToPrimitive` (см. выше раздел "Абстрактные операции со значениями"), результатом которой станет `"42"`. Теперь это просто `42 == "42"`, которое, как мы уже видели, становится `42 == 42`, так что `a` и `b` оказываются приведённо равными. **Совет:** Все особенности абстрактной операции `ToPrimitive`, которые мы обсуждали ранее в этой главе (`toString()`, `valueOf()`), применимы здесь, как и следовало ожидать. Это может быть весьма полезно, если у вас сложная структура данных, для которой вы хотите определить пользовательский метод `valueOf()`, чтобы предоставить простое значение для сравнения на равенство. В Главе 3 мы рассматривали "распаковку", когда `object`-обёртка вокруг примитивного значения (например, из `new String("abc")`) разворачивается, и возвращается базовое примитивное значение (`"abc"`). Такое поведение связано с `ToPrimitive`-приведением в алгоритме `==`: ```js var a = "abc"; var b = Object( a ); // same as `new String( a )` a === b; // false a == b; // true ``` `a == b` является `true`, потому что `b` принудительно (он же "unboxed", развернутый) через `ToPrimitive` к его простому скалярному примитивному значению `abc`, которое совпадает со значением в `a`. Однако есть некоторые значения, для которых это не так, из-за других переопределяющих правил в алгоритме `==`. Посмотрите: ```js var a = null; var b = Object( a ); // same as `Object()` a == b; // false var c = undefined; var d = Object( c ); // same as `Object()` c == d; // false var e = NaN; var f = Object( e ); // same as `new Number( e )` e == f; // false ``` Значения `null` и `undefined` не могут быть "упакованы" - у них нет эквивалента объектной обёртки - поэтому `Object(null)` аналогичен `Object()` в том смысле, что оба они просто создают обычный объект. `NaN` может быть упакован в эквивалент объектной оболочки `Number`, но когда `==` вызывает распаковку, сравнение `NaN == NaN` завершается неудачей, потому что `NaN` никогда не равен самому себе (см. Главу 2). ### Граничные случаи Теперь, когда мы тщательно изучили, как работает *неявное* приведение обычного равенства `==` (как логичным, так и неожиданным образом), давайте попробуем назвать худшие, самые безумные граничные случаи, чтобы мы могли понять, чего нам надо избегать, чтобы не быть нокаутированным ошибками приведения. Во-первых, давайте рассмотрим, как модификация встроенных прототипов может привести к сумасшедшим результатам: #### Number из любого другого значения будет... ```js Number.prototype.valueOf = function() { return 3; }; new Number( 2 ) == 3; // true ``` **Предупреждение:** `2 == 3` не попало бы в эту ловушку, потому что ни `2`, ни `3` не вызвали бы встроенный метод `Number.prototype.valueOf()`, потому что оба уже являются примитивными значениями `number` и могут быть сравнены напрямую. Однако `new Number(2)` должно пройти через приведение `ToPrimitive` и, таким образом, вызвать `valueOf()`. Зло, да? Конечно. Никто никогда не должен делать ничего подобного. Тот факт, что вы *можете* сделать это, иногда используется как критика приведения и `==`. Но это беспочвенное разочарование. JavaScript не *плох*, потому что вы можете делать такие вещи, разработчик *плох*, **если он делает такие вещи**. Не впадайте в заблуждение "мой язык программирования должен защищать меня от самого себя". Давайте рассмотрим другой хитрый пример, который выводит зло предыдущего примера на новый уровень: ```js if (a == 2 && a == 3) { // .. } ``` Вы могли подумать, что это невозможно, потому что `a` никогда не может быть равно как `2`, так и `3` *одновременно*. Но "в то же время" неточно, поскольку первое выражение `a == 2` происходит строго *перед* `a == 3`. Итак, что, если мы заставим `a.valueOf()` иметь побочные эффекты при каждом его вызове, так что при первом вызове он вернет `2`, а при втором - `3`? Это довольно просто: ```js var i = 2; Number.prototype.valueOf = function() { return i++; }; var a = new Number( 42 ); if (a == 2 && a == 3) { console.log( "Да, это случилось." ); } ``` Опять же, это злые уловки. Не делайте так. И не используйте их для жалоб на приведение. Потенциальные злоупотребления механизмом не являются достаточными доказательствами для осуждения механизма. Просто избегайте этих безумных трюков и придерживайтесь только корректного и надлежащего использования приведения. #### False-сравнения Наиболее частая жалоба на *неявное* приведение при обычном равенстве `==` исходит из того, что ложные значения ведут себя неожиданно, когда сравниваются друг с другом. Чтобы проиллюстрировать это, давайте посмотрим на список граничных случаев сравнения ложных значений, и увидим, какие из них обоснованы, а какие источник проблем: ```js "0" == null; // false "0" == undefined; // false "0" == false; // true -- О-О-О! "0" == NaN; // false "0" == 0; // true "0" == ""; // false false == null; // false false == undefined; // false false == NaN; // false false == 0; // true -- О-О-О! false == ""; // true -- О-О-О! false == []; // true -- О-О-О! false == {}; // false "" == null; // false "" == undefined; // false "" == NaN; // false "" == 0; // true -- О-О-О! "" == []; // true -- О-О-О! "" == {}; // false 0 == null; // false 0 == undefined; // false 0 == NaN; // false 0 == []; // true -- О-О-О! 0 == {}; // false ``` В этом списке 17 из 24 сравнений вполне разумны и предсказуемы. Например, мы знаем, что `""` и `NaN` вовсе не являются приравниваемыми значениями, и действительно, они не приводятся обычным равенством, тогда как `"0"` и `0` разумно приравниваемы и *действительно* приводятся обычным равенством. Однако семь сравнений отмечены знаком "О-О-О!", потому что, будучи ложнопозитивными, они скорее являются ошибками, которые могут сбить с толку. `""` и `0` совершенно разные значения, и редко, когда есть необходимость считать их равнозначными. Поэтому их взаимное приведение вызывает проблемы. Обратите внимание, что здесь нет никаких ложнонегативных случаев. #### Самые сумасшедшие из них Однако мы не должны останавливаться на достигнутом. Мы можем продолжать искать еще более неприятные способы приведения: ```js [] == ![]; // true ``` Оооо, это кажется более высоким уровнем сумасшествия, верно!? Ваш мозг, скорее всего, может обмануть вас, что вы сравниваете правдивое и ложное значения, поэтому результат `true` удивителен, поскольку мы *знаем*, что значение никогда не может быть правдивым и ложным одновременно! Но это не то, что происходит на самом деле. Давайте разберем это по полочкам. Что мы знаем об унарном операторе `!`? Он явно приводит к `boolean`, используя правила `ToBoolean` (он также инвертирует значение). Таким образом, еще до того, как `[] == ![]` будет обработано, оно фактически уже переведено в `[] == false`. Мы уже видели эту в нашем списке выше (`false == []`), поэтому неожиданный результат *не новый* для нас. Как насчет других граничных случаев? ```js 2 == [2]; // true "" == [null]; // true ``` Как мы говорили ранее в нашем обсуждении `ToNumber`, значения справа `[2]` и `[null]` подвергнутся приведению `ToPrimitive`, поэтому их будет легче сравнить с простыми примитивами (`2` и `""` соответственно) слева. Поскольку `valueOf()` для значений `array` просто возвращает сам `array`, приведение сводится к превращению `array` в строку. `[2]` станет `"2"`, которая затем приводится `ToNumber` к `2` справа в первом сравнении. `[null]` просто прямо становится `""`. Поэтому `2 == 2` и `"" == ""` вполне понятны. Если вам по-прежнему инстинктивно не нравятся эти результаты, то на самом деле ваше разочарование вызвано не приведением, как вы вероятно думаете. На самом деле это недовольство значением по умолчанию для `array`, когда приведение `ToPrimitive` возвращает значение `string`. Скорее всего, вы бы просто хотели, чтобы `[2].toString()` не возвращал `"2"`, и `[null].toString()` возвращал бы не `""`. Но что *должны* возвращать эти приведения к `string`? Я действительно не могу придумать ничего более подходящего для `[2]` приводимой к `string`, кроме `"2"`, за исключением, возможно, `"[2]"` - но это может оказаться очень странным в других контекстах! Вы могли бы справедливо утверждать, что поскольку `String(null)` становится `"null"`, то `String([null])` также должна стать `"null"`. Это разумное утверждение. Итак, вот кто настоящий виновник. *Неявное* приведение само по себе здесь не является злом. Даже *явное* приведение `[null]` к `string` приводит к `""`. Что вызывает разногласия, так это, разумно ли вообще `array` приводить к строковому эквиваленту его содержимого, и как именно это делать. Итак, направьте свое разочарование на правила для `String( [..] )`, потому что именно отсюда рождается сумасшествие. Возможно, вообще не должно быть приведения `array` к строке? Но это дало бы много других нехороших последствий в других частях языка. Еще одно часто цитируемое недоумение: ```js 0 == "\n"; // true ``` Как мы обсуждали выше пустые `""`, `"\n"` (`" "` или любая другая комбинация пробелов) приводится через `ToNumber`, и результат этого - `0`. К какому другому значению `number` вы хотели привести пробел? Вас беспокоит, что *явное* `Number(" ")` дает `0`? На самом деле единственное другое разумное значение `number`, к которому можно привести пустые строки или строки с пробелами, - это `NaN`. Но действительно ли это было бы лучше? Сравнение `" " == NaN`, конечно же, потерпело бы неудачу, но неясно, действительно ли бы мы *исправили* какие-либо из проблем. Вероятность того, что реальная JS-программа выйдет из строя из-за `0 == "\n"`, ужасно редка, и таких граничных случаев легко избежать. Преобразования типов **всегда** имеют граничные случаи на любом языке - ничего специфичного для приведения. Проблемы здесь заключаются в последующих сомнениях о конкретных граничных случаях (и, возможно, это правильно!?), но это не является весомым аргументом против общего механизма приведения. Итог: почти любое сумасшедшее приведение между *нормальными значениями*, с которым вы вероятно столкнетесь (кроме намеренных хаков `valueOf()` и `toString()` выше), будет сводиться к короткому списку из семи пунктов, которые мы определили выше. В противовес этим 24-м частым подозреваемым в странном приведении, рассмотрим другой список: ```js 42 == "43"; // false "foo" == 42; // false "true" == true; // false 42 == "42"; // true "foo" == [ "foo" ]; // true ``` В этих не ложных, не граничных случаях (а существует буквально бесконечное число сравнений, которые мы могли бы включить в этот список) результаты приведения абсолютно безопасны, разумны и объяснимы. #### Проверка на здравомыслие Ладно, мы действительно нашли какие-то сумасшедшие вещи, когда глубоко погрузились в *неявное* приведение. Неудивительно, что большинство разработчиков заявляют, что приведение - это зло, и его следует избегать, верно!? Но давайте сделаем шаг назад и проведем проверку на здравомыслие. Для оценки масштабов: у нас есть *список* из семи проблематичных приведений, и есть *другой список* (по крайней мере, 17, но на самом деле бесконечный) приведений, которые полностью вменяемы и объяснимы. Если вы ищете хрестоматийный пример "выплёскивания вместе с водой ребёнка", то вот он: отказ от любого приведения (бесконечно большой список безопасных и полезных средств) из-за буквально семи ошибок. Более разумной реакцией было бы спросить: "Как я могу использовать бесчисленные *сильные стороны* приведения, и избежать немногих *недостатков*?" Давайте еще раз посмотрим на список "недостатков": ```js "0" == false; // true -- О-О-О! false == 0; // true -- О-О-О! false == ""; // true -- О-О-О! false == []; // true -- О-О-О! "" == 0; // true -- О-О-О! "" == []; // true -- О-О-О! 0 == []; // true -- О-О-О! ``` Четыре из семи пунктов в этом списке включают сравнение `== false`, которого, как мы говорили ранее, вам следует **всегда, всегда** избегать. Это довольно простое правило для запоминания. Теперь список сократился до трех. ```js "" == 0; // true -- О-О-О! "" == []; // true -- О-О-О! 0 == []; // true -- О-О-О! ``` Будет ли применение таких приведений в обычной JS-программе разумным? В каких обстоятельствах они действительно могут случиться? Я не думаю, что высока вероятность того, что вы буквально будете использовать `== []` в вашей программе. По крайней мере, намеренно. Более вероятно, вы бы написали `== ""` или `== 0`, как тут: ```js function doSomething(a) { if (a == "") { // .. } } ``` У вас бы случился большой ОЙ, если бы вы случайно вызвали `doSomething(0)` или `do Something([])`. Другой сценарий: ```js function doSomething(a,b) { if (a == b) { // .. } } ``` Опять же, это может сломаться, если вы сделаете что-то вроде `doSomething("",0)` или `doSomething([],"")`. Таким образом, хотя *могут* существовать ситуации, когда эти приведения доставят вам беспокойство, и вы захотите быть осторожными с ними, они, вероятно, не очень распространены в вашей кодовой базе. #### Безопасное использование неявного приведения Самый важный совет, который я могу вам дать: изучите свою программу и подумайте о том, какие значения могут оказаться по обе стороны от сравнения `==`. Чтобы эффективно избежать проблем с такими сравнениями, вот несколько эвристических правил, которым надо следовать: 1. Если любая из сторон сравнения может иметь значения `true` или `false`, никогда, НИКОГДА не используйте `==`. 2. Если любая из сторон сравнения может иметь `[]`, `""`, или значения `0`, серьезно подумайте о том, чтобы не использовать `==`. В этих случаях почти наверняка лучше использовать `===` вместо `==`, чтобы избежать нежелательного приведения. Следуйте этим двум простым правилам, и вы избежите практически все ошибки приведения, которые могут навредить вам. **Для большей ясности - это избавит вас от множества головных проблем.** Вопрос `==` vs. `===` в действительности формулируется так: следует ли допускать приведение для сравнения или нет? Есть много случаев, когда такое приведение может быть полезным, позволяя вам более кратко выразить некоторую логику сравнения (например, с `null` и `undefined`). В совокупном порядке вещей существует относительно немного случаев, когда *неявное* приведение действительно опасно. Но в этих ситуациях, в целях безопасности, обязательно используйте `===`. **Совет:** Еще один случай, где приведение гарантированно *не* причинит вам вреда, - это оператор `typeof`. `typeof` всегда будет возвращать вам одну из семи строк (см. Главу 1), и ни одна из них не является пустой строкой `""`. Соответственно, нет ни одного случая, когда проверка типа какого-либо значения натолкнется на *неявное* приведение. `typeof x == "function"` на 100% так же безопасен и надежен, как `typeof x === "function"`. В спецификации буквально говорится, что алгоритм будет идентичен в этих случаях. Итак, не используйте `===` слепо везде просто потому, что так говорит ваша среда разработки, или (что хуже всего) потому, что в какой-то книге вам сказали **не думать об этом**. Вы сами отвечаете за качество своего кода. Является ли *неявное* приведение злом и опасным? В некоторых случаях - да, но в подавляющем большинстве - нет. Будьте ответственным и зрелым разработчиком. Изучите, как эффективно и безопасно применять мощь приведения (как *явного*, так и *неявного*). И научите окружающих делать то же самое. Вот удобная таблица, составленная Алексом Дори (@dorey на GitHub) для визуализации различных сравнений: Источник: https://github.com/dorey/JavaScript-Equality-Table ## Абстрактное сравнение отношений Хотя этой части *неявного* приведения часто уделяется гораздо меньше внимания, тем не менее важно подумать о том, что происходит со сравнениями `a < b` (аналогично тому, как мы только что подробно рассмотрели `a == b`). Алгоритм "Абстрактного сравнения отношений" в разделе 11.8.5 ES5 по существу делится на две части: что делать, если в сравнении оба значения `string` (вторая половина) или иное (первая половина). **Примечание:** Алгоритм определен только для `a < b`. Поэтому `a > b` обрабатывается как `b < a`. Сначала алгоритм инициирует приведение `ToPrimitive` для обоих значений, и если любой из возвращаемых результатов не `string`, то оба значения преобразуются в `number` с использованием правил операции `ToNumber` и затем сравниваются численно. Например: ```js var a = [ 42 ]; var b = [ "43" ]; a < b; // true b < a; // false ``` **Примечание:** Здесь действуют те же оговорки о `-0` и `NaN`, как и в рассмотренном ранее алгоритме `==`. Однако, если в сравнении `<` оба значения `string`, выполняется простое лексикографическое (в натуральном алфавите) сравнение символов: ```js var a = [ "42" ]; var b = [ "043" ]; a < b; // false ``` `a` и `b` *не* приводятся к `number`, потому что оба `array` после `ToPrimitive` приведения становятся `string`. Следовательно, `"42"` посимвольно сравнивается с `"043"`, начиная с первых символов `"4"` и `"0"` соответственно. Поскольку `"0"` лексикографически *меньше*, чем `"4"`, сравнение возвращает `false`. Точно такое же поведение и рассуждения наблюдаем здесь: ```js var a = [ 4, 2 ]; var b = [ 0, 4, 3 ]; a < b; // false ``` Здесь `a` становится `"4,2"`, а `b` становится `"0,4,3"`, и они лексикографически сравниваются идентично предыдущему коду. Как насчет этого: ```js var a = { b: 42 }; var b = { b: 43 }; a < b; // ?? ``` `a < b` также `false`, потому что `a` становится `[object Object]`, и `b` становится `[object Object]`. Поэтому очевидно, что `a` лексикографически не меньше `b`. Но вот странный случай: ```js var a = { b: 42 }; var b = { b: 43 }; a < b; // false a == b; // false a > b; // false a <= b; // true a >= b; // true ``` Почему `a == b` не `true`? Это же одно и то же `string` значение (`"[object Object]"`), поэтому кажется они должны быть равны, верно? Нет. Вспомним предыдущее обсуждение того, как `==` работает со ссылками на `object`. Но тогда как `a <= b` и `a >= b` становятся `true`, если **и** `a < b`, **и** `a == b`, **и** `a > b` - все равны `false`? Потому что в спецификации сказано, что для `a <= b`, на самом деле сначала вычисляется `b < a`, а затем её результат инвертируется. Так как `b < a` *тоже* `false`, то результат `a <= b` это `true`. Это, вероятно, ужасно противоречит тому, как до настоящего момента вы вероятно интерпретировали действие `<=` - скорее буквально: "меньше, чем *или* равно". JS интерпретирует `<=` как "не больше, чем" (`!(a > b)`, которое JS трактует как `!(b < a)`). Более того, `a >= b` также сначала интерпретируется как `b <= a`, а затем в ход идёт та же логика. К сожалению, не существует "строгого сравнения отношения", как для равенства. Другими словами, нет никакого способа предотвратить *неявное* приведение при сравнении отношений, таких как `a < b`, кроме как явно убедиться, что `a` и `b` имеют один и тот же тип, перед выполнением сравнения. Следуйте той же логике проверки на здравомыслие, что и в нашем предыдущем обсуждении `==` vs. `===`. Если приведение полезно и достаточно безопасно, как в сравнении `42 < "43"`, **используйте его**. С другой стороны, если вам нужна безопасность сравнении отношений, то сначала *явно приведите* значения, прежде чем использовать `<` (или его аналоги). ```js var a = [ 42 ]; var b = "043"; a < b; // false -- сравнение строк! Number( a ) < Number( b ); // true -- сравнение чисел! ``` ## Итоги В этой главе мы обратили наше внимание на то, как происходит преобразование типов в JavaScript, называемое **приведением**, которое можно описать либо *явным*, либо *неявным*. Приведение пользуется дурной славой, но в действительности во многих случаях оно весьма полезно. Важной задачей для ответственного разработчика JS становится изучение всех тонкостей приведения, чтобы решить, какие вещи помогут улучшить его код, а чего на самом деле следует избегать. *Явное* приведение - это код, из которого очевидно, что целью является преобразование значения из одного типа в другой. Его преимущество - улучшение читаемости и поддержки кода путем сокращения недоумений. *Неявное* приведение - это "скрытое" преобразование типа, являющееся побочным эффектом какой-либо другой операции, где не так очевидно, что произойдет конвертация типа. Хотя может показаться, что *неявное* приведение противопоставляется *явному* и, поэтому вредно (и действительно, многие так думают!), на самом деле *неявное* приведение также служит улучшению читаемости кода. Приведение должно использоваться ответственно и осознано, особенно это касается *неявного* приведения. Знайте, почему вы пишете тот код, который вы пишете, и как он работает. Стремитесь писать код, который другие также легко смогут изучить и понять. ================================================ FILE: types & grammar/ch5.md ================================================ # Вы не знаете JS: Типы и грамматика # Глава 5: Грамматика Последняя важная тема, которую мы хотим затронуть, - это то, как работает синтаксис языка JavaScript (он же его грамматика). Вы можете думать, что знаете, как писать JS, но в различных частях грамматики языка ужасно много нюансов, которые приводят к путанице и неправильному пониманию, поэтому мы хотим углубиться в эти части и прояснить некоторые вещи. **Примечание:** Термин "грамматика" может быть немного менее знаком читателям, чем термин "синтаксис". Во многих отношениях это схожие термины, описывающие *правила* того, как работает язык. У них есть небольшие различия, но они в основном не имеют значения для нашего здесь обсуждения. Грамматика для JavaScript - это структурированный способ описания того, как синтаксис (операторы, ключевые слова и т.д.) компонуется в правильно и корректно разработанные программы. Иными словами, обсуждение синтаксиса без грамматики упустило бы многие важные детали. Поэтому, в этой главе наше внимание акцентируется на том, что наиболее точно описывается как "грамматика", хотя разработчики непосредственно оперируют с синтаксисом языка. ## Инструкции и выражения Разработчики довольно часто предполагают, что термины "инструкция" и "выражение" примерно эквивалентны. Но здесь нам нужно разделить их, потому что в наших JS программах есть некоторые очень важные различия. Чтобы провести различие, давайте позаимствуем терминологию, с которой вы, возможно, более знакомы: английский язык. "Предложение" - это одно законченное высказывание из слов, выражающее мысль. Оно состоит из одной или нескольких "фраз", каждая из которых может быть связана знаками препинания или союзами ("и", "или" и т.д.). Сама фраза может состоять из более мелких фраз. Некоторые фразы являются неполными и сами по себе мало чего значат, в то время как другие фразы могут быть самостоятельными. Эти правила в совокупности называются *грамматикой* английского языка. Так же обстоит дело с грамматикой JavaScript. Инструкции - это предложения, выражения - это фразы, а операторы - это союзы/знаки препинания. Каждое выражение в JS может быть вычислено вплоть до одного конкретного значения. Например: ```js var a = 3 * 6; var b = a; b; ``` В этом фрагменте `3 * 6` является выражением (вычисляется до значения `18`). `a` во второй строке также является выражением, как и `b` в третьей строке. Оба выражения `a` и `b` вычисляются в соответствии со значениями, хранящимися в этих переменных в данный момент, которые равны `18`. Более того, каждая из трех строк представляет собой инструкцию, содержащую выражения. `var a = 3 * 6` и `var b = a` называются "операторами объявления", потому что каждый из них объявляет переменную (и необязательно присваивает ей значение). Присваивания `a = 3 * 6` и `b = a` (откидывая `var`) называются выражениями присваивания. Третья строка содержит только выражение `b`, и это также инструкция сама по себе (хотя и не очень интересная!). Обычно это называется "оператор выражения". ### Значение завершения инструкции Это весьма малоизвестный факт, что все инструкции имеют значения завершения (даже если это просто `undefined`). Как бы вы отнеслись к тому, чтобы взглянуть на значение завершения? Наиболее очевидный путь - ввести инструкцию в консоль разработчика вашего браузера, потому что, когда вы ее выполняете, консоль по умолчанию сообщает значение завершения самой последней инструкции, которую она выполнила. Давайте рассмотрим `var b = a`. Каково значение завершения этой инструкции? Выражение присваивания `b = a` возвращает значение, которое было присвоено (`18` см. выше), но сама инструкция `var` возвращает `undefined`. Почему? Потому что так инструкция `var` определена в спецификации. Если вы введете `var a = 42;` в свою консоль, вы увидите сообщение `undefined` вместо `42`. **Примечание:** Технически всё немного сложнее. В спецификации ES5, раздел 12.2 "Инструкция переменной", алгоритм `VariableDeclaration` действительно *возвращает* значение (`string`, содержащая имя объявленной переменной - странно, да!?), но это значение обычно поглощается (кроме цикла `for..in`) алгоритмом `VariableStatement`, который выдает пустое (известное как `undefined`) значение завершения. На самом деле, если вы много экспериментировали с кодом в своей консоли (или в JS среде REPL -- read /evaluate /print / loop), вы, вероятно, видели сообщение `undefined` после множества разных инструкций и, возможно, никогда не понимали, что бы это было. Проще говоря, консоль просто сообщает значение завершения инструкции. Но то, что консоль выводит как значение завершения, - это не то, что мы можем использовать внутри нашей программы. Итак, как мы можем зафиксировать значение завершения? Это гораздо более сложная задача. Прежде чем мы объясним *как*, давайте рассмотрим *почему* вам хотелось бы это сделать? Нам нужно рассмотреть другие типы значений завершения инструкции. Например, любой обыденный блок `{ .. }` имеет значение завершения, равное значению завершения его последней инструкции/выражения. Рассмотрим: ```js var b; if (true) { b = 4 + 38; } ``` Если бы вы ввели это в своей REPL-консоли, вы, вероятно, увидели бы сообщение `42`, поскольку `42` - это значение завершения блока `if`, который принял значение завершения своей последней инструкции выражения присваивания `b = 4 + 38`. Другими словами, значение завершения блока похоже на *неявный return* значения последней инструкции в блоке. **Примечание:** Это концептуально известно в таких языках, как CoffeeScript, которые имеют неявные `return` значения из `function`, которые совпадают со значением последней инструкции в функции. Но есть очевидная проблема. Такой код не работает: ```js var a, b; a = if (true) { b = 4 + 38; }; ``` Мы не можем зафиксировать значение завершения инструкции и присвоить его другой переменной каким-либо простым синтаксическим/грамматическим способом (по крайней мере, пока!). Итак, что мы можем сделать? **Предупреждение**: только для демонстрационных целей -- пожалуйста, не делайте нижеприведенное в вашем реальном коде! Мы можем использовать сильно оклеветанную функцию `eval(..)` (иногда произносится как "evil" - "злой"), чтобы зафиксировать это значение завершения. ```js var a, b; a = eval( "if (true) { b = 4 + 38; }" ); a; // 42 ``` Дааааааааа. Это ужасно некрасиво. Но это работает! И это иллюстрирует тот факт, что значения завершения инструкции - это реальная вещь, которая может быть зафиксирована не только в нашей консоли, но и в наших программах. Есть предложение для ES7 под названием "выражение исполнения" (do-выражение). Вот как это может сработать: ```js var a, b; a = do { if (true) { b = 4 + 38; } }; a; // 42 ``` Выражение `do { .. }` выполняет блок (с одним или несколькими операторами в нем), и значение завершения последней инструкции внутри блока становится значением завершения *`do`-выражения*, которое, как показано, затем может быть присвоено `a`. Общая идея состоит в том, чтобы иметь возможность обрабатывать инструкции как выражения -- они могут отображаться внутри других инструкций -- без необходимости оборачивать их в функциональные выражения и делать явный `return ..`. На данный момент значения завершения инструкций - это не больше, чем пустяки. Но они, возможно, будут приобретать все большее значение по мере развития JS, и, надеюсь, выражения `do { .. }` уменьшат соблазн использовать такие вещи, как `eval (..)`. **Предупреждение:** Повторяю мое предыдущее увещевание: избегайте `eval (..)`. Серьезно. За дополнительными деталями обращайтесь к книге этой же серии *Область Видимости и Замыкания*. ### Побочные эффекты выражений Большинство выражений не имеют побочных эффектов. Например: ```js var a = 2; var b = a + 3; ``` Выражение `a + 3` не имело *собственного* побочного эффекта, как, например, изменение `a`. У него был результат, равный `5`, и этот итог был присвоен `b` в инструкции `b = a + 3`. Наиболее распространенным примером выражения с (возможными) побочными эффектами является выражение вызова функции: ```js function foo() { a = a + 1; } var a = 1; foo(); // результат: `undefined`, сторонний эффект: изменённое `a` ``` Однако есть и другие побочные эффекты выражений. Например: ```js var a = 42; var b = a++; ``` Выражение `a++` выполняет два отдельных действия. *Сначала* оно возвращает текущее значение `a`, которое равно `42` (которое следом присваивается `b`). Но *затем* оно изменяет само значение `a`, увеличивая его на единицу. ```js var a = 42; var b = a++; a; // 43 b; // 42 ``` Многие разработчики ошибочно полагают, что `b` равно `43` так же, как и `a`. Неразбериха возникает из-за того, что не полностью учитывается, *когда* происходит побочный эффект оператора `++`. Операторы инкремента `++` и оператор декремента `--` являются унарными операторами (см. Главу 4), которые могут использоваться либо в постфиксной ("после") позиции, либо в префиксной ("до") позиции. ```js var a = 42; a++; // 42 a; // 43 ++a; // 44 a; // 44 ``` Когда `++` используется в префиксной позиции, как в `++a`, его побочный эффект (увеличение `a` на единицу) происходит *до* возврата значения из выражения, а не *после*, как в случае `a++`. **Примечание:** Считаете ли вы, что `++a++` - это корректный синтаксис? Если вы попробуете, то получите ошибку `ReferenceError`, но почему? Потому что операторы с побочным эффектом **требуют ссылки на переменную**, на которую нацелены их побочные эффекты. Для `++a++` сначала вычисляется часть `a++` (из-за приоритета оператора - см. ниже), которая возвращает значение `a` _до_ инкремента. Но затем он пытается вычислить `++42`, что (если вы попытаетесь это сделать) даст ту же ошибку `ReferenceError`, поскольку `++` не может иметь побочного эффекта непосредственно для значения `42`. Иногда ошибочно полагают, что вы можете инкапсулировать побочный эффект *после* `a++`, заключив его в пару скобок `( )`, например: ```js var a = 42; var b = (a++); a; // 43 b; // 42 ``` К сожалению, `( )` сами по себе не определяют новое обёрнутое выражение, которое вычисляется следом *за побочным эффектом* выражения `a++`, как мы могли бы надеяться. Фактически, даже если бы это произошло, `a++` сначала возвращает `42`, и, если у вас нет другого выражения, которое повторно вычисляет `a` после побочного эффекта `++`, вы не получите `43` из этого выражения, поэтому `b` не будет присвоен `43`. Однако есть вариант: `,` - инструкция серии операторов запятой. Этот оператор позволяет вам объединить несколько автономных выражений в одну инструкцию: ```js var a = 42, b; b = ( a++, a ); a; // 43 b; // 43 ``` **Примечание:** Здесь требуется `( .. )` вокруг `a++, a`. Причина в приоритете операторов, о котором мы поговорим позже в этой главе. Выражение `a++, a` означает, что второе выражение инструкции `a` вычисляется следом *за побочным эффектом* первого выражения инструкции `a++`, что означает, что оно возвращает значение `43` для выполнения присваивания `b`. Другим примером оператора с побочным действием, является `delete`. Как мы показали в Главе 2, `delete` используется для удаления свойства из `object` или ячейки из `array`. Но обычно он просто вызывается как отдельный оператор: ```js var obj = { a: 42 }; obj.a; // 42 delete obj.a; // true obj.a; // undefined ``` Результирующее значение оператора `delete` равно `true`, если запрошенная операция является допустимой, или `false` в противном случае. Но побочный эффект оператора заключается в том, что он удаляет свойство объекта (или ячейку массива). **Примечание:** Что мы подразумеваем под допустимым? Несуществующие свойства или свойства, которые существуют и конфигурируемы (см. Главу 3 из книги этой серии *This и Прототипы Объектов*), вернут `true` из оператора `delete`. В противном случае результат будет `false` или ошибка. Последним примером оператора с побочным эффектом, который может быть одновременно и очевидным, и неочевидным, является оператор присваивания `=`. Взгляните: ```js var a; a = 42; // 42 a; // 42 ``` Может показаться, что `=` в инструкции `a = 42` не является оператором с побочным действием. Но, если мы посмотрим на результирующее значение инструкции `a = 42`, то увидим - это только что присвоенное значение (`42`), поэтому присвоение в `a` значения по сути является побочным эффектом. **Совет:** Те же рассуждения о побочных эффектах применимы к составным операторам присваивания, таким как `+=`, `-=`, и т.д. Например, `a = b += 2` сначала обрабатывается как `b += 2` (то есть `b = b + 2`), а результат *этого* `=` затем присваивается `a`. Такое поведение, при котором выражение присваивания (или инструкция) завершается присвоенным значением, в первую очередь полезно для цепочек присвоений, таких как: ```js var a, b, c; a = b = c = 42; ``` Здесь `c = 42` вычисляется как `42` (с побочным эффектом присваивания `42` в `c`), затем `b = 42` вычисляется как `42` (с побочным эффектом присваивания `42` в `b`), и, наконец, вычисляется `a = 42` (с побочным эффектом присваивания `42` в `a`). **Предупреждение:** Распространенная ошибка, которую разработчики допускают при выполнении цепочки присваиваний выглядит так: `var a = b = 42`. Хотя это смотрится как одно и то же, но это не так. Если эта инструкция выполняется без наличия отдельной инструкции `var b` (где-то в области видимости), которая официально декларирует переменную `b`, то `var a = b = 42` не объявляет переменную `b` напрямую. В зависимости от `strict` режима это приведёт либо к ошибке, либо к случайной глобальной переменной (см. книгу этой серии *Область Видимости и Замыкания*). Другой пример для рассмотрения: ```js function vowels(str) { var matches; if (str) { // выбрать все гласные matches = str.match( /[aeiou]/g ); if (matches) { return matches; } } } vowels( "Hello World" ); // ["e","o","o"] ``` Это работает, и многие разработчики предпочитают именно такой вариант. Но мы можем упростить функцию, объединив два оператора `if` в один, если применим идиому, в которой побочный эффект присваивания будет работать на пользу: ```js function vowels(str) { var matches; // выбрать все гласные if (str && (matches = str.match( /[aeiou]/g ))) { return matches; } } vowels( "Hello World" ); // ["e","o","o"] ``` **Примечание:** `( .. )` вокруг `matches = str.match..` обязательны. Причина - приоритет операторов, который мы рассмотрим позже в разделе "Приоритет операторов" этой главы. Я предпочитаю этот более короткий стиль, так как он, по-моему, проясняет, что два условных выражения на самом деле связаны, а не разделены. Но, как и в случае с большинством стилистических вариантов в JS, это лишь мнение, какой вариант *лучше*. ### Контекстуальные правила В правилах грамматики JavaScript есть довольно много мест, где один и тот же синтаксис означает разные вещи в зависимости от того, где / как он используется. Такого рода вещи сами по себе могут вызвать немалую путаницу. Здесь мы не перечислим исчерпывающе все подобные случаи, а просто назовем несколько самых распространенных. #### `{ .. }` Фигурные скобки Есть два основных случая (их будет больше по мере развития JS!), когда в вашем коде появляется пара фигурных скобок `{ .. }`. Давайте взглянем на каждый из них. ##### Объектные литералы Во-первых, как литерал `object`: ```js // предположим, что определена функция `bar()` var a = { foo: bar() }; ``` Откуда мы знаем, что это литерал `object`? Потому что пара `{ .. }` - это значение, которое присваивается `a`. **Примечание:** Эта ссылка `a` называется "L-значение" (значение левой стороны), поскольку она является целью присваивания. Пара `{ .. }` является "R-значением" (значение правой стороны), поскольку она используется *просто* как значение (в данном случае как источник присваивания). ##### Метки Что произойдет, если мы удалим `var a =` из приведенного выше фрагмента? ```js // предположим, что определена функция `bar()` { foo: bar() } ``` Многие разработчики предполагают, что пара `{ .. }` - это просто отдельный литерал `object`, который ничему не присваивается. Но на самом деле все совсем по-другому. Здесь `{ .. }` - это просто обычный блок кода. В JavaScript не очень идиоматично (тем более в других языках!) иметь такой автономный блок `{ .. }`, но это вполне допустимая грамматика JS. Это может быть особенно полезно в сочетании с объявлениями переменных через `let` с областью видимости блока (см. книгу этой серии *Область Видимости и Замыкания*). Блок кода `{ .. }` здесь в значительной степени функционально идентичен блоку кода, сопутствующему некой инструкции, такой как циклы `for`/`while`, условного ветвления `if` и т.д. Но, если это обычный блок кода, тогда, что это за странный синтаксис `foo: bar()` и насколько это корректно? Этот обескураживающе малоизвестный инструмент в JavaScript называется "инструкцией с меткой". Здесь `foo` - это метка для инструкции `bar()` (у которой опущена завершающая `;` - см. далее в этой главе "Автоматические точки с запятой"). Но в чем смысл инструкции с меткой? Если бы в JavaScript был оператор `goto`, вы теоретически могли бы написать `goto foo` и заставить перейти сюда, продолжить выполнять код с этого места. Обычно `goto` считаются ужасными идиомами программирования, поскольку они значительно затрудняют понимание кода (он же "спагетти-код"), поэтому *очень хорошо*, что в JavaScript нет привычного `goto`. Однако JS *действительно* поддерживает ограниченную, специальную форму `goto`: переходы по метке. Обе инструкции `continue` и `break` могут опционально принимать указанную метку, и в этом случае поток программы "прыгает" подобно `goto`. Рассмотрим: ```js // `foo` labeled-loop foo: for (var i=0; i<4; i++) { for (var j=0; j<4; j++) { // всякий раз, когда встречается, продолжить внешний цикл if (j == i) { // перейти к следующей итерации // цикла, помеченного `foo` continue foo; } // пропускать нечетные числа if ((j * i) % 2 == 1) { // обычный (безметочный) `continue` внутреннего цикла continue; } console.log( i, j ); } } // 1 0 // 2 0 // 2 1 // 3 0 // 3 2 ``` **Примечание:** `continue foo` не означает "перейти к позиции с меткой 'foo', чтобы продолжить", а "продолжить цикл с меткой "foo" со следующей итерации". Итак, это *на самом деле* не произвольное `goto`. Как вы видите, мы пропустили итерацию с нечетным произведением `3 1`, а переход с меткой также пропустил итерации `1 1` и `2 2`. Возможно, немного более полезная форма перехода с меткой - это `break __` из внутреннего цикла, чтобы выбраться из внешнего цикла. Эту логику довольно громоздко реализовывать с помощью `break` без метки: ```js // `foo` labeled-loop foo: for (var i=0; i<4; i++) { for (var j=0; j<4; j++) { if ((i * j) >= 3) { console.log( "останавливаемся!", i, j ); // прервать цикл помеченный меткой `foo` break foo; } console.log( i, j ); } } // 0 0 // 0 1 // 0 2 // 0 3 // 1 0 // 1 1 // 1 2 // останавливаемся! 1 3 ``` **Примечание:** `break foo` не означает "перейти к позиции с меткой 'foo', чтобы продолжить", а "выйти из цикла/блока с надписью 'foo' и продолжить *после* него". Не совсем `goto` в традиционном понимании, да? Безметочная альтернатива `break` вышеописанному, вероятно, потребовала бы задействовать одну или несколько функций, доступ к переменной из общей области видимости и т.д. Скорее всего, это было бы более запутанным, чем `break` с меткой, поэтому использование `break` с меткой здесь, вероятно, является лучшим вариантом. Метка может применяться к блоку без цикла, но только `break` может сослаться на такую метку. Вы можете сделать `break ___` из любого блока с меткой, но вы не можете использовать `continue ___` или использовать `break` без метки из блока. ```js function foo() { // блок с меткой `bar` bar: { console.log( "Привет" ); break bar; console.log( "никогда не выводится" ); } console.log( "Мир" ); } foo(); // Привет // Мир ``` Циклы/блоки с метками крайне редки, и к ним часто относятся неодобрительно. Если это возможно, то лучше всего избегать их; например, используя вызовы функций вместо переходов из цикла. Но, возможно, есть редкие случаи, когда они могут быть полезны. Если вы собираетесь использовать переход по метке, обязательно задокументируйте то, что вы делаете, с подробными объяснениями! Очень распространено мнение, что JSON является корректным подмножеством JS, поэтому строка JSON (например, `{"a":42}` - обратите внимание на кавычки вокруг имени свойства, как того требует JSON!) считается допустимым кодом JavaScript. **Неправда!** Попробуйте ввести `{"a":42}` в вашу консоль JS, и вы получите сообщение об ошибке. Инструкция с меткой не может быть заключена в кавычки, поэтому `"a"` не является допустимой меткой, и, следовательно, `:` не может идти сразу после нее. Итак, JSON действительно является подмножеством синтаксиса JS, но JSON сам по себе не является допустимой грамматикой JS. Одно из чрезвычайно распространенных заблуждений из этой области заключается в том, что, если бы вы загрузили файл JS через тег `