11. Подмножества и расширения JavaScript

До сих пор в книге описывалась официальная версия языка JavaScript, соответствующая стандартам ECMAScript 3 и ECMAScript 5. В этой главе, напротив, будет идти речь о подмножествах и надмножествах языка JavaScript. Подмножества языка были определены, по большей части, для обеспечения более высокого уровня безопасности: сценарий, использующий только безопасное подмножество языка, может использоваться без опаски, даже если он был получен из непроверенного источника, такого как рекламный сервер. В разделе 11.1 описываются некоторые из этих подмножеств.

Стандарт ECMAScript 3 был опубликован в 1999 году, и прошло десять лет, прежде чем стандарт был обновлен до версии ECMAScript 5, вышедшей в 2009 году. Брендан Эйх (Brendan Eich), создатель JavaScript, продолжал развивать язык на протяжении всех этих десяти лет (спецификация ECMAScript явно разрешает расширение языка) и совместно с проектом Mozilla выпустил версии JavaScript 1.5, 1.6, 1.7, 1.8 и 1.8.1 в Firefox 1.0, 1.5, 2, 3 и 3.5. Некоторые из расширенных особенностей JavaScript вошли в стандарт ECMAScript 5, но многие остаются не- стандартизованными. Однако, как ожидается, некоторые из оставшихся нестандартных особенностей будут стандартизованы в будущем.

Эти расширения поддерживаются броузером Firefox, точнее, реализованным в нем интерпретатором Spidermonkey языка JavaScript. Созданный проектом Mozilla интерпретатор Rhino языка JavaScript, написанный на языке Java (раздел 12.1), также поддерживает большинство расширений. Однако, поскольку эти расширения языка не являются стандартными, они не особенно полезны для веб-разработчиков, которым требуется обеспечить совместимость своих веб-приложений со всеми броузерами. Тем не менее они описываются в этой главе, потому что они:

Вводный раздел был посвящен подмножествам языка, остальная же часть этой главы описывает расширения языка. Поскольку они нестандартизованы, они подаются в виде учебных материалов, с меньшим количеством строгих определений, чем особенности языка, описываемые в других главах книги.

содержание 11.1. Подмножества JavaScript содержание

Большая часть подмножеств языка была определена с целью обеспечения безопасности при выполнении программного кода, полученного из непроверенных источников. Однако существует одно интересное подмножество языка, которое было создано по другим причинам. Мы рассмотрим его в первую очередь, а затем перейдем к подмножествам языка, цель которых заключается в обеспечении безопасности.

содержание 11.1.1. Подмножество The Good Parts

Небольшая книга Дугласа Крокфорда (Douglas Crockford) «JavaScript: The Good Parts» (O'Reilly) описывает подмножество JavaScript, включающее части языка, которые, по мнению автора, достойны использования. Цель этого подмножества – упростить язык, скрыть его недостатки и в конечном счете сделать программирование проще, а программы – лучше. Крокфорд так объясняет свои устремления:

Большинство языков программирования имеют свои достоинства и недостатки. Я обнаружил, что могу писать более качественные программы, используя достоинства и избегая недостатков.

Подмножество Крокфорда не включает инструкции withwith и continue, а также функцию eval(). Оно позволяет определять функции только с помощью выражений определения и не содержит инструкции определения функций. Подмножество требует, чтобы тела циклов и условных инструкций заключались в фигурные скобки: оно не позволяет опускать скобки, даже если тело состоит из единственной инструкции. Оно требует, чтобы любая инструкция, не заканчивающаяся фигурной скобкой, завершалась точкой с запятой.

Подмножество не включает оператор точки, битовые операторы и операторы ++ и --. В нем также не допускается использовать операторы == и !=из-за выполняемых ими преобразований типов и требуется использовать вместо них операторы === и !==.

Поскольку в языке JavaScript отсутствует понятие области видимости блока, подмножество Крокфорда разрешает использовать инструкцию var только на верхнем уровне тела функции и требует, чтобы программист объявлял все локальные переменные функции с помощью единственной инструкции var- первой в теле функции. Подмножество не поощряет использование глобальных переменных, но это уже простое соглашение по программированию, а не фактическое ограничение языка.

Созданный Крокфордом веб-инструмент проверки качества программного кода (http://jslint.com) включает возможность проверки на соответствие требованиям подмножества Good Parts. Помимо проверок на отсутствие в программном коде недопустимых особенностей, инструмент JSLint также проверяет соблюдение общепринятых правил оформления, таких как корректное оформление отступов.

Крокфорд написал свою книгу до того, как в ECMAScript 5 был определен строгий режим, и теперь многие «недостатки» JavaScript, использование которых он осуждает в своей книге, могут быть запрещены за счет использования строгога режима. Теперь, с принятием стандарта ECMAScript 5, инструмент JSLint требует, чтобы программы включали директиву «use strict», если перед проверкой был включен параметр «The Good Parts».

содержание 11.1.2. Безопасные подмножества

Good Parts – это подмножество языка, созданное исходя из эстетических соображений и желания повысить производительность труда программиста. Существует также обширная категория подмножеств, созданных с целью повышения безопасности при выполнении программного кода JavaScript в безопасном окружении, или в «песочнице». Безопасные подмножества запрещают использование особенностей языка и библиотек, которые позволяют программному коду вырваться за пределы «песочницы» и влиять на глобальное окружение. Каждое подмножество снабжается статическим инструментом проверки, который анализирует программный код, чтобы убедиться, что он соответствует требованиям подмножества. Поскольку подмножества языка, которые могут пройти статическую проверку, обычно оказываются довольно ограниченными, некоторые системы организации безопасного окружения определяют большее, не так сильно ограничивающее, подмножество, добавляют этап трансформации программного кода, на котором выполняется проверка его соответствия более широкому подмножеству, и производят трансформацию программного кода для использования с более узким подмножеством языка. А кроме того, добавляют проверки времени выполнения в тех случаях, когда статический анализ программного кода не гарантирует полную безопасность.

Чтобы обеспечить приемлемый уровень безопасности при статической проверке, из языка JavaScript должны быть исключены следующие особенности:

Некоторые из этих ограничений, такие как запрет на использование функции eval() и инструкции with, не слишком обременительны для программистов, потому что эти особенности обычно не используются при программировании на языке JavaScript. Другие, такие как ограничение на использование квадратных скобок для доступа к свойствам, являются достаточно тяжелыми, и здесь на помощь приходит механизм трансляции программного кода. Транслятор может автоматически преобразовать использование квадратных скобок, например, в вызовы функций, выполняющие дополнительные проверки во время выполнения. С помощью аналогичных трансформаций можно обезопасить использование ключевого слова this. Правда, в этом случае за безопасность приходится заплатить уменьшением скорости выполнения программного кода в безопасном окружении.

В настоящее время существует несколько безопасных подмножеств. И хотя полное их описание их выходит за рамки этой книги, тем не менее мы коротко познакомимся с некоторыми из наиболее известных:

ADsafe

Подмножество ADsafe (http://adsafe.org) было одним из первых предложенных безопасных подмножеств. Это подмножество было создано Дугласом Крокфор- дом (Douglas Crockford) (который также определил подмножество The Good Parts). Подмножество ADsafe опирается только на статическую проверку, которая выполняется с помощью инструмента JSLint (http://jslint.org). Оно запрещает доступ к большинству глобальных переменных и определяет переменную ADSAFE, которая предоставляет доступ к безопасным функциям, включая, методы DOM специального назначения. Подмножество ADsafe не получило широкого распространения, но оно стало важным доказательством правильности самой концепции и послужило толчком к появлению других безопасных подмножеств.

dojox.secure

Подмножество dojox. secure (http://www.sitepen.com/blog/2008/08/01/secure-mashups-with-dojoxsecure/)  – это расширение для библиотеки Dojo (http://dojotoolkit.org), толчком к созданию которого послужило появление подмножества ADsafe. Подобно ADsafe, это подмножество основано на статической проверке использования ограниченного подмножества языка. В отличие от ADsafe, оно позволяет использовать стандартный DOM API. Кроме того, оно включает инструмент проверки, реализованный на языке JavaScript, благодаря чему имеется возможность динамической проверки программного кода перед его выполнением.

Caja

Подмножество Caja (https://code.google.com/archive/p/google-caja/) – это подмножество, распространяемое с открытыми исходными текстами, созданное компанией Google. Подмножество Caja (по-испански «коробка») определяет два подмножества языка. Подмножество Cajita («маленькая коробка») – сильно ограниченное подмножество, подобное тому, которое используется подмножествами ADsafe и dojox.secure. Valija («кейс» или «чемодан») – намного более широкое подмножество языка, близкое к подмножеству строгого режима ECMAScript 5 (с исключением eval()). Само название Caja – это имя компилятора, трансформирующего веб-содержимое (HTML, CSS и программный код JavaScript) в безопасные модули, которые можно включать в веб-страницы, не опасаясь, что они будут оказывать влияние на страницу в целом или на другие модули. Подмножество Caja – это часть OpenSocial API (http://code.google.com/apis/opensocial/); оно используется компанией Yahoo! на ее веб-сайтах. Например, содержимое, доступное на портале http://my.yahoo.com, организовано в модули Caja.

FBJS

Подмножество FBJS – это версия JavaScript, используемая на сайте Facebook (http://facebook.com) с целью дать пользователям возможность размещать непроверенное содержимое на страницах своих профилей. Подмножество FBJS обеспечивает безопасность за счет трансформации программного кода. Инструмент преобразования вставляет проверки, которые выполняются во время выполнения и предотвращают доступ к глобальному объекту с помощью ключевого слова this. Также он переименовывает все идентификаторы верхнего уровня, добавляя к ним префикс, определяемый модулем. Благодаря переименованию предотвращаются любые попытки изменить или прочитать глобальные переменные или переменные, принадлежащие другому модулю. Кроме того, благодаря добавлению префикса, все вызовы функции evalQ преобразуются в вызовы несуществующей функции. Подмножество FBJS реализует собственное безопасное подмножество DOM API.

Microsoft Web Sandbox

Подмножество Microsoft Web Sandbox (http://websandbox.livelabs.com/) определяет довольно широкое подмножество языка JavaScript (плюс HTML и CSS) и обеспечивает безопасность за счет радикальной переработки программного кода, фактически реализуя безопасную виртуальную машину JavaScript поверх небезопасной.

содержание 11.2. Константы и контекстные переменные содержание

Теперь оставим подмножества языка и перейдем к расширениям. В версии JavaScript 1.5 и выше появилась возможность использовать ключевое слово const для определения констант. Константы похожи на переменные, за исключением того, что попытки присваивания им значений игнорируются (они не вызывают ошибку), а попытка переопределить константу приводит к исключению:

Ключевое слово const действует подобно ключевому слову var: для него не существует области видимости блока, и объявления констант поднимаются в начало определения функции (раздел 3.10.1), содержащей их.

Отсутствие в языке JavaScript области видимости блока для переменных долгое время считали недостатком, поэтому в версии JavaScript 1.7 появилось ключевое слово let, решающее эту проблему. Ключевое слово const всегда было зарезервированным (но не используемым) словом в JavaScript, благодаря чему константы можно добавлять, не нарушая работоспособность существующего программного кода. Ключевое слово let не было зарезервировано, поэтому оно не распознается версиями интерпретаторов ниже 1.7.

Версии JavaScript

В этой главе при упоминании какой-то определенной версии JavaScript подразумевается версия языка, реализованная проектом Mozilla в интерпретаторах Spider monkey и Rhino и в веб-браузере Firefox.

Некоторые расширения языка, представленные здесь, определяют новые ключевые слова (такие как let), и, чтобы избежать нарушения работоспособности существующего программного кода, использующего эти ключевые слова, JavaScript требует явно указывать версию, чтобы иметь возможность использовать расширения. Если вы пользуетесь автономным интерпретатором Spidermonkey или Rhino, версию языка можно указать в виде параметра командной строки или вызовом встроенной функции version(). (Она ожидает получить номер версии, умноженный на сто. Чтобы получить возможность использовать ключевое слово let, нужно выбрать версию JavaScript 1.7, т.е. передать функции число 170.) В Firefox указать номер версии можно в теге script:

<script type="application/JavaScript; version=1.8">

Ключевое слово let имеет четыре формы использования:

В простейшем случае ключевое слово let используется как замена инструкции var. Переменные, объявленные с помощью инструкции var, определены в любой точке объемлющей функции. Переменные, объявленные с помощью ключевого слова let, определены только внутри ближайшего содержащего их блока (и, конечно же, внутри вложенных в него блоков). Если с помощью инструкции let объявить переменную, например, внутри тела цикла, она будет недоступна за пределами этого цикла:

Обратите внимание, что в этом примере инструкция let используется так же, как замена инструкции var в цикле for. Она создает переменную, которая будет доступна только в теле цикла и в выражениях проверки условия и увеличения цикла. Точно так же можно использовать инструкцию let в циклах for/infor each; раздел 11.4.1):

Существует одно интересное отличие между случаями, когда let используется как инструкция объявления и когда let используется как инструмент инициализации переменной цикла. При использовании let в качестве инструкции объявления значение выражения инициализации вычисляется в области видимости переменной. Но в цикле for выражение инициализации вычисляется за пределами области видимости новой переменной. Это отличие становится важным, только когда имя новой переменной совпадает с именем уже существующей переменной:

Переменные, объявленные с помощью инструкции var , определены в любой точке функции, где они объявлены, но они не инициализируются, пока инструкция var не будет выполнена фактически. То есть переменная существует (обращение к ней не вызывает исключение ReferenceError), но при попытке использовать переменную до инструкции var она будет иметь значение undefined. Переменные, объявляемые с помощью инструкции let, действуют аналогично: если попытаться использовать переменную до инструкции let (но внутри блока, где находится инструкция let), переменная будет доступна, но она будет иметь значение undefined.

Примечательно, что данная проблема отсутствует при использовании let для объявления переменной цикла – просто сам синтаксис не позволяет использовать переменную до ее инициализации. Существует еще один случай использования инструкции let, где отсутствует проблема использования переменной до ее инициализации. Блок, образуемый инструкцией let (в противоположность объявлению переменной с помощью инструкции let, показанному выше), объединяет блок программного кода с объявлением переменных для этого блока и их инициализацией. В этом случае переменные и выражения их инициализации заключаются в круглые скобки, за которыми следует блок инструкций в фигурных скобках:

Важно запомнить, что выражения инициализации переменных let-блока не являются частью этого блока и интерпретируются в другой области видимости. В приведенном примере создается новая переменная x, и ей присваивается значение, на единицу больше, чем значение существующей переменной x.

Последний случай использования ключевого слова let является разновидностью let-блока, в котором вслед за списком переменных и выражений инициализации в круглых скобках следует единственное выражение, а не блок инструкций. Такая конструкция называется let-выражением, а последний пример можно было переписать следующим образом:

Некоторые формы использования ключевых слов const и let (не обязательно все четыре, описанные здесь) в будущем наверняка будут включены в стандарт ECMAScript.

содержание 11.3. Присваивание с разложением содержание

В версии Spidermonkey 1.7 реализована разновидность составных инструкций присваивания, известная как присваивание с разложением. (Вы могли встречать присваивание с разложением в языках программирования Python или Ruby.) При присваивании с разложением значение справа от знака «равно» является массивом или объектом («составным» значением), а слева указывается одно или более имен переменных с применением синтаксиса, имитирующего литерал массива или объекта.

Инструкция присваивания с разложением извлекает (разлагает на составляющие) одно или более значений из значения справа и сохраняет в переменных, указанных слева. Кроме того, как и обычный оператор присваивания, присваивание с разложением может использоваться для инициализации вновь объявляемых переменных в инструкциях var и let.

Присваивание с разложением является простым и мощным инструментом при работе с массивами, и его особенно удобно использовать при работе с функциями, возвращающими массивы значений. Однако при использовании с объектами и вложенными объектами эта операция становится сложной и запутанной. Примеры, демонстрирующие простоту и сложность, приводятся ниже.

Следующий пример демонстрирует простоту присваивания с разложением при использовании с массивами значений:

Обратите внимание, как присваивание с разложением упрощает работу с функциями, возвращающими массивы значений:

При выполнении присваивания с разложением количество переменных слева не обязательно должно совпадать с количеством элементов массива справа. Лишние переменные слева получат значения undefined, а лишние значения справа будут просто игнорироваться. Список переменных слева может включать дополнительные запятые, чтобы пропустить определенные значения справа:

В JavaScript отсутствует синтаксическая конструкция, которая позволила бы присвоить переменной все оставшиеся или неиспользованные значения (как массив). Так, во второй строке последнего примера отсутствует возможность присвоить переменной y остаток массива [2,3].

Присваивание с разложением можно даже использовать для извлечения значений из вложенных массивов. В этом случае левая сторона инструкции присваивания должна выглядеть как литерал вложенного массива:

let [one, [twoA, twoB]] = [1, [2,2.5], 3]; // one=1, twoA=2, twoB=2.5

Присваивание с разложением можно также выполнять, когда справа находится объект. В этом случае конструкция слева должна выглядеть как литерал объекта: список пар имен свойств и имен переменных, разделенных запятыми, заключенный в фигурные скобки. Имя слева от каждого двоеточия – это имя свойства, а имя справа от каждого двоеточия – это имя переменной. Каждое свойство, указанное слева, будет отыскиваться в объекте справа от оператора присваивания, и его значение (или undefined) будет присвоено соответствующей переменной. Эта разновидность присваивания с разложением может сбивать с толку, особенно если в качестве имен свойств и переменных используются одни и те же идентификаторы. Необходимо понимать, что в приведенном ниже примере r, g, и b – это имена свойств, a red, green и blue – имена переменных:

Следующий пример копирует глобальные функции из объекта Math в переменные с целью упростить программирование большого количества тригонометрических операций:

// То же, что и let sin=Math.sin, cos=Math.cos, tan=Math.tan
let {sin:sin, cos:cos, tan:tan} = Math;

Подобно тому как присваивание с разложением может использоваться при работе с вложенными массивами, эта операция может использоваться при работе с вложенными объектами. В действительности, эти два синтаксиса можно комбинировать для описания произвольных структур данных. Например:

Имейте в виду, что подобные вложенные инструкции присваивания с разложением могут превратить программный код в трудночитаемые дебри, вместо того чтобы упростить его. Однако есть одна интересная закономерность, которая поможет вам разобраться в самых сложных случаях. Представьте сначала обычное присваивание (с единственным значением). После того как присваивание будет выполнено, переменную слева от оператора присваивания можно взять и использовать как выражение, которое будет возвращать присвоенное значение. Мы говорили, что в инструкции присваивания с разложением слева указывается синтаксическая конструкция, напоминающая литерал массива или объекта. Но обратите внимание, что после выполнения присваивания с разложением программный код слева, который выглядит как литерал массива или объекта, действительно будет интерпретироваться как обычный литерал массива или объекта: все необходимые переменные будут определены, и вы сможете копировать текст слева от знака «равно» и использовать его в своей программе как массив или объект.

содержание 11.4. Итерации содержание

Проектом Mozilla в расширение JavaScript были добавлены новые способы выполнения итераций, включая цикл for each, а также итераторы и генераторы в стиле языка Python. Они детально описываются ниже.

содержание 11.4.1. Цикл for/each

Цикл for/each – это новая инструкция цикла, определяемая стандартом Е4Х. Е4Х (ECMAScript for XML) – это расширение языка, позволяющее употреблять в программах на языке JavaScript теги языка XML и предоставляющее функции для работы с данными в формате XML. Стандарт Е4Х реализован далеко не во всех веб-браузерах, но он поддерживается реализацией JavaScript проекта Mozilla, начиная с версии 1.6 (в Firefox 1.5). В этом разделе мы рассмотрим только цикл for/each и особенности его использования с обычными объектами, не имеющими отношения к XML. Остальные подробности о Е4Х приводятся в разделе 11.7.

Цикл for/each напоминает цикл for/in. Однако вместо итераций по свойствам объекта он выполняет итерации по значениям свойств:

При использовании с массивами цикл for/each выполняет итерации по элементам (а не по индексам) массива. Обычно он перечисляет их в порядке следования числовых индексов, но в действительности такой порядок не определяется стандартом и не является обязательным:

Обратите внимание, что область применения цикла for/each не ограничивается элементами массива – он может перечислять значения перечислимых свойств объекта, включая перечислимые методы, унаследованные объектом. По этой причине обычно не рекомендуется использовать цикл for/each для работы с объектами. Это особенно верно для программ, которые должны выполняться под управлением версий интерпретаторов JavaScript до ECMAScript 5, в которых невозможно сделать пользовательские свойства и методы неперечислимыми. (Смотрите аналогичное обсуждение цикла for/in в разделе 7.6.)

содержание 11.4.2. Итераторы

В версии JavaScript 1.7 цикл for/in был дополнен более универсальными возможностями. Цикл for/in в JavaScript 1.7 стал больше похож на цикл for/in в языке Python, он позволяет выполнять итерации по любым итерируемым объектам. Прежде чем обсуждать новые возможности, необходимо дать некоторые определения.

Итератором называется объект, который позволяет выполнять итерации по некоторой коллекции значений и хранит информацию о текущей «позиции» в коллекции. Итератор должен иметь метод next() . Каждый вызов метода next() должен возвращать следующее значение из коллекции. Например, функция counter(), представленная ниже, возвращает итератор, который, в свою очередь, возвращает последовательность увеличивающихся целых чисел при каждом вызове метода next() . Обратите внимание, что здесь для хранения текущей информации используется область видимости функции, образующая замыкание:

При работе с конечными коллекциями метод next() итератора возбуждают исключение StopIteration, когда в коллекции не остается значений для выполнения очередной итерации. StopIteration – это свойство глобального объекта в JavaScript 1.7. Его значением является обычный объект (без собственных свойств), зарезервированный специально для нужд завершения итераций. Обратите внимание, что StopIteration не является функцией-конструктором, таким как TypeError() или RangeError(). Ниже приводится пример метода rangeIter(), возвращающего итератор, который выполняет итерации по целым числам в заданном диапазоне:

Обратите внимание, насколько неудобно использовать объект-итератор в цикле из-за необходимости явно обрабатывать исключение StopIteration . Из-за этого неудобства итераторы редко используются на практике непосредственно. Чаще используются итерируемые объекты. Итерируемый объект представляет коллекцию значений, по которым можно выполнять итерации. Итерируемый объект должен определять метод с именем __iterator__() (с двумя символами подчеркивания в начале и в конце), возвращающий объект-итератор для коллекции.

В JavaScript 1.7 в цикл for/in была добавлена возможность работы с итерируемыми объектами. Если значение справа от ключевого слова in является итерируемым объектом, то цикл for/in автоматически вызовет его метод __iterator__(), чтобы получить объект-итератор. Затем он будет вызывать метод next() итератора, присваивать возвращаемое им значение переменной цикла и выполнять тело цикла. Цикл for/in сам обрабатывает исключение StopIteration, и оно никогда не передается программному коду, выполняемому в цикле. Пример ниже определяет функцию range(), возвращающую итерируемый объект (а не итератор), который представляет диапазон целых чисел. Обратите внимание, насколько проще выглядит цикл for/in при использовании итерируемого объекта диапазона по сравнению с циклом while, в котором используется итератор диапазона.

Обратите внимание, что, несмотря на необходимость писать метод __iterator__() и возбуждать исключение StopIteration для создания итерируемых объектов и их итераторов, вам не придется (в обычной ситуации) вызывать метод __iterator__() и/или обрабатывать исключение StopIteration – все это сделает цикл for/in. Если по каким-то причинам потребуется явно получить объект-итератор итерируемого объекта, можно воспользоваться функцией Iterator(). (Iterator() – это глобальная функция, которая появилась в версии JavaScript 1.7.) Если передать этой функции итерируемый объект, она просто вернет результат вызова метода __iterator__(), что придаст дополнительную ясность программному коду. (Если передать функции Iterator() второй аргумент, она передаст его методу __iterator__().)

Однако функция Iterator() имеет еще одно важное назначение. Если ей передать объект (или массив), не имеющий метода __iterator__(), она вернет собственную реализацию итерируемого итератора для объекта. Каждый вызов метода next() этого итератора будет возвращать массив с двумя значениями. В первом элементе массива будет возвращаться имя свойства объекта, а во втором - значение этого свойства. Поскольку этот объект является итерируемым итератором, его можно использовать в цикле for/in вместо прямого вызова метода next(), а это означает, что функцию Iterator() можно использовать совместно с операцией присваивания с разложением при выполнении итераций по свойствам и значениям объекта или массива:

Итератор, возвращаемый функцией Iterator(), имеет еще две важные особенности. Во-первых, он игнорирует унаследованные свойства и выполняет итерации только по «собственным» свойствам, что чаще всего и требуется. Во-вторых, если передать функции Iterator() значение true во втором аргументе, возвращаемый итератор будет выполнять итерации только по именам свойств, без их значений. Обе эти особенности демонстрируются в следующем примере:

содержание 11.4.3. Генераторы

Генераторы – это особенность JavaScript 1.7 (заимствованная из языка Python), основанная на использовании нового ключевого слова yield. Программный код, использующий данную особенность, должен явно указать номер версии 1.7, как описывалось в разделе 11.2. Ключевое слово yield используется в функциях и действует аналогично инструкции return, возвращая значение из функции. Разница между yield и return состоит в том, что функция, возвращающая значение с помощью ключевого слова yield, сохраняет информацию о своем состоянии, благодаря чему ее выполнение может быть возобновлено. Такая способность к возобновлению выполнения делает yield замечательным инструментом для создания итераторов. Генераторы – очень мощная особенность языка, но понять принцип их действия совсем не просто. Для начала познакомимся с некоторыми определениями.

Любая функция, использующая ключевое слово yield (даже если инструкция yield никогда не будет выполняться), является функцией-генератором. Функции-генераторы возвращают значения с помощью yield. Они могут использовать инструкцию return без значения, чтобы завершиться до того, как будет достигнут конец тела функции, но они не могут использовать return со значением. За исключением использования ключевого слова yield и ограничений на использование инструкции return, функции-генераторы ничем не отличаются от обычных функций: они объявляются с помощью ключевого слова function, оператор typeof возвращает для них строку «function» и как обычные функции они наследуют свойства и методы от Function.prototype. Однако поведение функции-генератора совершенно отличается от поведения обычной функции: при вызове, вместо того чтобы выполнить свое тело, функция-генератор возвращает объект генератора.

Генератор – это объект, представляющий текущее состояние функции-генератора. Он определяет метод next(), вызов которого возобновляет выполнение функции-генератора и позволяет продолжить ее выполнение, пока не будет встречена следующая инструкция yield . Когда это происходит, значение в инструкции yield в функции-генераторе становится возвращаемым значением метода next() генератора. Если функция-генератор завершает свою работу вызовом инструкции return или в результате достижения конца своего тела, метод next() генератора возбуждает исключение StopIteration.

Тот факт, что генераторы имеют метод next(), который может возбуждать исключение StopIteration, явственно говорит о том, что они являются итераторами.

Генераторы иногда называют «итераторами-генераторами», чтобы отличить их от создающих их функций-генераторов. В этой главе вместо термина «итераторы-генераторы» мы будем использовать термин «генераторы». В других источниках вы можете встретить термин «генератор», обозначающий одновременно и функции-генераторы, и итераторы-генераторы.

В действительности они являются итерируемыми итераторами, т. е. они могут использоваться в циклах for/in. Следующий пример демонстрирует, насколько просто создавать функции-генераторы и выполнять итерации по значениям, которые они возвращают с помощью инструкции yield:

Функции-генераторы могут никогда не завершаться. Каноническим примером использования генераторов является воспроизведение последовательности чисел Фибоначчи:

Обратите внимание, что функция-генератор fibonacci() никогда не завершится. По этой причине создаваемый ею генератор никогда не возбудит исключение StopIteration. Поэтому вместо того чтобы использовать его как итерируемый объект в цикле for/in и попасть в бесконечный цикл, мы используем его как итератор и явно вызываем его метод next() десять раз. После того как фрагмент выше будет выполнен, генератор f по-прежнему будет хранить информацию о состоянии функции-генератора. Если в программе не требуется далее хранить эту информацию, ее можно освободить вызовом метода close() объекта f:

При вызове метода close() генератора производится завершение связанной с ним функции-генератора, как если бы она выполнила инструкцию return в той точке, где ее выполнение было приостановлено. Если это произошло в блоке try, автоматически будет выполнен блок finally перед тем, как close() вернет управление. Метод close() никогда не возвращает значение, но если блок finally возбудит исключение, оно продолжит свое распространение из вызова close() .

Генераторы часто бывает удобно использовать для последовательной обработки данных – элементов списка, строк текста, лексем в лексическом анализаторе и т. д. Генераторы можно объединять в цепочки подобно конвейеру команд в Unix. Самое интересное в этом подходе заключается в том, что он следует принципу отложенных вычислений: значения «извлекаются» из генератора (или из конвейера генераторов) по мере необходимости, а не все сразу. Эту особенность демонстрирует пример 11.1.

Пример 11.1. Конвейер

Обычно инициализация генераторов выполняется при их создании: аргументы, передаваемые функции-генератору, являются единственными значениями, которые принимают генераторы. Однако имеется возможность передать дополнительные данные уже работающему генератору. Каждый генератор имеет метод send(), который перезапускает генератор подобно методу next(). Разница лишь в том, что методу send() можно передать значение, которое станет значением, возвращаемым выражением yield в функции-генераторе. (В большинстве генераторов, которые не принимают дополнительных входных данных, ключевое слово yield выглядит как инструкция. Однако в действительности yield - это выражение, возвращающее значение.) Кроме методов next() и send() существует еще один способ перезапустить генератор – метод throw(). Если вызвать этот метод, выражение yield возбудит аргумент метода throw() как исключение, как показано в следующем примере:

содержание 11.4.4. Генераторы массивов

Еще одна особенность, заимствованная в JavaScript 1.7 из языка Python, – генераторы массивов. Это механизм инициализации элементов массива на основе элементов другого массива или итерируемого объекта. Синтаксис генераторов массивов основан на математической форме записи элементов множества, т. е. выражения и инструкции находятся совсем не там, где привыкли их видеть программисты на языке JavaScript. Тем не менее привыкание к необычному синтаксису происходит достаточно быстро, а мощь генераторов массивов просто неоценима.

Ниже приводится пример генератора массивов, использующего созданную выше функцию range() для инициализации массива, содержащего квадраты четных чисел, меньшие 100:

let evensquares = [x*x for (x in range(0,10)) if (x % 2 === 0)]

Эта строка примерно эквивалентна следующим пяти строкам:

let evensquares = [];
for(x in range(0,10)) {
  if (x % 2 === 0)
    evensquares.push(x*x);
}

В общем случае синтаксис генераторов массивов имеет следующий вид:

[ выражение for ( переменная in объект ) if ( условное выражение ) ]

Обратите внимание на три основные части в квадратных скобках:

Ниже приводятся несколько более конкретных примеров, которые помогут лучше понять синтаксис:

содержание 11.4.5. Выражения-генераторы

В JavaScript 1.8 (на момент написания этих строк выражения-генераторы не поддерживались в Rhino) можно заменить квадратные скобки в генераторах массивов круглыми скобками и получить выражения-генераторы. Выражение-генератор похоже на генератор массивов (синтаксис в круглых скобках в точности соответствует синтаксису в квадратных скобках), но его значением является объект генератора, а не массив. Преимущество выражений-генераторов перед генераторами массивов в том, что они используют прием отложенных вычислений – вычисления выполняются по мере необходимости, а не все сразу – и позволяют обрабатывать даже бесконечные последовательности. Недостаток генераторов состоит в том, что они обеспечивают только последовательный доступ к своим элементам. То есть, в отличие от массивов, генераторы не позволяют обращаться к элементам по индексам: чтобы получить n-е значение, придется выполнить n – 1 итераций.

Ранее в этой главе мы реализовали функцию map():

Выражения-генераторы позволяют избежать необходимости создавать или использовать такую функцию map(). Чтобы получить новый генератор h, возвращающий f(x) для каждого значения x, возвращаемого генератором g, достаточно использовать такой программный код:

let h = (f(x) for (x in g));

Используя генератор eachline() из примера 11.1, можно реализовать отсечение пробельных символов, а также фильтрацию комментариев и пустых строк, как показано ниже:

содержание 11.5. Краткая форма записи функций содержание

В JavaScript 1.8 (на момент написания этих строк выражения-генераторы не поддерживались в Rhino) появилась возможность краткой записи простых функций (называется «лексическим замыканием»). Если функция вычисляет единственное выражение и возвращает его значение, ключевое слово return и фигурные скобки, окружающие тело функции, можно опустить и просто поместить выражение сразу после списка аргументов. Например:

let succ = function(x) x+1, yes = function() true, no = function() false;

Это просто и удобно: функции, определяемые таким способом, ведут себя как обычные функции, в определении которых присутствуют фигурные скобки и ключевое слово слово return . Этот сокращенный синтаксис удобно использовать, в частности, при передаче функций другим функциям. Например:

содержание 11.6. Множественные блоки catch содержание

В JavaScript 1.5 инструкция try/catch была добавлена возможность использовать несколько блоков catch. Чтобы использовать эту возможность, необходимо, чтобы за именем параметра блока catch следовало ключевое слово if и условное выражение:

Когда возникает какое-либо исключение, по очереди проверяются все блоки catch. Исключение присваивается именованному параметру блока catch и вычисляется условное выражение. Если оно возвращает true, выполняется тело этого блока catch, а все остальные блоки catch пропускаются. Если блок catch не имеет условного выражения, он ведет себя как блок с условным выражением if true и выполняется всегда, если перед ним не был встречен блок catch, удовлетворяющий условию. Если условное выражение присутствует во всех блоках catch и ни в одном из них условное выражение не вернуло true, исключение продолжит распространение как необработанное. Обратите внимание, что, поскольку условное выражение уже находится в круглых скобках блока catch, его не требуется еще раз заключать в скобки, как в обычных инструкциях if.

содержание 11.7. Е4Х: ECMAScript for XML содержание

Расширение ECMAScript for XML, более известное как Е4Х, – это стандартное расширение JavaScript, определяющее ряд мощных особенностей для обработки XML-документов.

Расширение Е4Х определяется стандартом ЕСМА-357. Официальную спецификацию можно найти по адресу http://www.ecma-international.org/publications/standards/Ecma-357.htm.

Расширение Е4Х поддерживается интерпретаторами Spider- monkey 1.5 и Rhino 1.6. Из-за того что оно не получило широкой поддержки у производителей броузеров, расширение Е4Х, вероятно, лучше относить к серверным технологиям, основанным на интерпретаторах Spidermonkey или Rhino.

Расширение Е4Х представляет XML-документ (или элементы и атрибуты XML- документа) как объект XML, и представляет фрагменты XML (несколько элементов XML, не имеющих общего родителя) в виде родственного объекта XMLList. В этом разделе мы познакомимся с несколькими способами создания и обработки объектов XML. Объекты XML – это совершенно новый тип объектов, для работы с которыми в Е4Х предусмотрен (как мы увидим) специальный синтаксис. Как известно, для всех стандартных объектов JavaScript, не являющихся функциями, оператор typeof возвращает строку «object». Объекты XML, подобно функциям, отличаются от обычных объектов JavaScript, и для них оператор typeof возвращает строку «xmb. Важно понимать, что объекты XML никак не связаны с объектами DOM (Document Object Model – объектная модель документа), которые используются в клиентском JavaScript (глава 15). Стандарт Е4Х определяет дополнительные средства для преобразования XML-документов и элементов между представлениями E4X и DOM, но броузер Firefox не реализует их. Это еще одна причина, почему расширение Е4Х лучше относить к серверным технологиям.

Этот раздел представляет собой краткое учебное руководство по расширению Е4Х и не должен рассматриваться как полноценное его описание. В частности, объекты XML и XMLList имеют множество методов, которые вообще не будут упоминаться здесь. Их описание также отсутствует в справочном разделе. Тем из вас, у кого появится желание использовать расширение Е4Х, за более полной информацией необходимо обращаться к официальной спецификации.

Расширение Е4Х определяет совсем немного новых синтаксических конструкций. Самая заметная часть нового синтаксиса заключается в возможности использования разметки XML непосредственно в программном коде JavaScript и включения в него литералов XML, как показано ниже:

Синтаксис литералов XML в расширении E4X в качестве экранирующих символов использует угловые скобки, что позволяет помещать в разметку XML произвольные выражения на языке JavaScript. Ниже демонстрируется еще один способ создания точно такого же элемента XML, как в последнем примере:

В дополнение к синтаксису литералов можно также извлекать данные из строк с разметкой XML. Следующий пример добавляет в периодическую таблицу еще один элемент:

pt.element += new XML('<element id="5"><name>Boron<name></element>');

При работе с фрагментами XML вместо конструктора XML() используется конструктор XMLList():

pt.element += new XMLList('<element id="6"><name>Carbon</name></element>' +
                          '<element id="7"><name>Nitrogen</name></element>');

После создания XML-документа для доступа к его содержимому можно использовать интуитивно понятный синтаксис E4X:

Кроме того, расширение Е4Х добавляет новый синтаксис для работы с объектами XML. Оператор .. – это оператор доступа к вложенным элементам. Его можно использовать вместо привычного оператора . доступа к членам:

// Другой способ получить список всех тегов <name>
var names2 = pt..name;

Расширение Е4Х позволяет использовать даже оператор шаблона:

// Получить все вложенные теги <element>.
// Это еще один способ получить список всех тегов <name> .
var names3 = pt.element.*;

Расширение Е4Х отличает имена атрибутов от имен тегов с помощью символа @ (этот синтаксис заимствован из языка XPath). Например, значение атрибута можно запросить так :

// Получить атомное число гелия
var atomicNumber = pt.element[1].@id;

Оператор шаблона для имен атрибутов имеет вид @*:

// Список всех атрибутов всех тегов <element>
var atomicNums = pt.element.@*;

Расширение Е4Х включает даже мощные и удивительно выразительные синтаксические конструкции для фильтрации списков с помощью произвольных выражений-предикатов:

Цикл for/each, с которым мы познакомились выше в этой главе (раздел 11.4.1), в расширении Е4Х дополнен возможностью итераций по спискам тегов и атрибутов XML. Напомню, что цикл for/each похож на цикл for/in, за исключением того, что вместо итераций по свойствам объекта он выполняет итерации по значениям свойств:

В расширении Е4Х выражения могут присутствовать слева от оператора присваивания. Это позволяет изменять существующие и добавлять новые теги и атрибуты:

Так же легко можно удалять теги и атрибуты, используя стандартный оператор delete

Расширение Е4Х реализовано так, что позволяет выполнять большинство типичных операций с документами XML с помощью привычного синтаксиса языка. В Е4Х также имеются методы, которые можно вызывать относительно объектов XML. Например, метод insertChildBefore():

pt.insertChildBefore(pt.element[1],
                     element id="1"> <name>Deuterium</name></element>);

Расширение Е4Х полностью поддерживает пространства имен, а также включает синтаксические конструкции и функции для работы с пространствами имен XML: