Книга утверждает, что «функции работают как обычные переменные — их можно скопировать в другую переменную или даже удалить». И приводится следующий пример:
>>> var sum = function(a, b) {return a + b;} >>> var add = sum; >>> delete sum true >>> typeof sum; "undefined"
Если не обращать внимания на пару отсутствующих точек с запятой, что еще не так с этим куском кода? Конечно, проблема в том, что удаление переменной sum не должно было пройти успешно; выражение с оператором delete не должно разрешаться в true и typeof sum не должно вернуть «undefined». Все потому, что в яваскрипте нельзя удалять переменные. По крайней мере, те, которые определены таким образом.
Так что же происходит в этом примере? Опечатка? Диверсия? Скорее всего нет. На самом деле весь этот отрывок кода скопирован из Файрбага, который Стоян, должно быть, использовал для тестирования по-быстрому. Почти похоже что у Файрбага какие-то свои правила по поводу удаления. Это Файрбаг сбил Стояна с толку! Так что же на самом деле здесь происходит?
Чтобы ответить на этот вопрос необходимо понимать как работает оператор delete в яваскрипте: что конкретно можно и нельзя удалять и почему. Сегодня я попытаюсь обяснить это по-подробнее. Мы изучим «странное» поведение Файрбага и убедимся что оно не такое уж и странное; мы покопаемся в том, что происходит за кулисами когда мы объявляем переменные и функции, назначаем свойства и удаляем их; мы посмотрим насколько браузеры соответствуют спецификации и познакомимся с некоторыми самыми общеизвестными багами; мы также поговорим о режиме строгого соответствия в 5-м издании ECMAScript, и о том, как он меняет поведение оператора delete.
Понятия яваскрипт и ECMAScript в тексте будут означать ECMAScript, если только мы не обсуждаем Мозилловскую реализацию JavaScript™.
Как ни странно, в Сети довольно мало описаний работы оператора delete. Статья на MDC хотя, возможно, и самая исчерпывающая, но, к сожалению, не включает несколько интересных деталей. Интересно, что одна из этих деталей является причиной странного поведения Файрбага. Статья же на MSDN практически бесполезна.
Теория
Так почему мы можем удалять свойства объектов:
var o = { x: 1 }; delete o.x; // true o.x; // undefined
но не можем удалять переменные, определенные таким образом:
var x = 1; delete x; // false x; // 1
или функции, объявленные вот так:
function x(){} delete x; // false typeof x; // "function"
Обратите внимание что delete возвращает false только когда свойство нельзя удалить.
Чтобы все это понять, сначала необходимо полностью осознать такие понятия как создание переменных и атрибуты свойств — те вещи, которые, к несчастью, редко объясняются в книгах про яваскрипт. Я пробегусь очень кратко по этим понятиям в несколько параграфов. Понять их совсем не сложно! Если вам не интересно, почему все работает так а не иначе, просто пропустите эту главу.
Тип кода
В ECMAScript 3 типа исполнаемого кода: глобальный, код функций и Eval-код. В некоторый степени их названия говорят сами за себя, но я коротко поясню:
Когда с кодом работают как с Программой, он выполняется в глобальной области видимости и считается глобальным кодом. В браузере, содержимое тега SCRIPT обычно парсится как Программа и таким образом выполняется как глобальный код.
Все что выполняется непосредственно внутри функций считается, очевидно, кодом функций. В браузерах содержимое атрибутов-событий (например, <p onclick="...">
) обычно парсится и выполняется как код функций.
И, наконец, текст, который передается встроенной функции Eval парсится как Eval-код. Вскоре мы увидим, чем этот тип отличается.
Контекст выполнения
Выполнение кода на ECMAScript всегда происходит в определенном контексте выполнения. Контекст выполнения — это, в некотором смысле, абстрактная сущность, которая помогает понимать, как работает область видимости и создание переменных. Для каждого из трех типов кода существует свой контекст выполнения. Когда выполняется функция, говорят, что управление переходит в контекст выполнения кода фукнции; когда выполняется глобальный код, управление переходит в контекст выполнения глобального кода и так далее.
Как можно заметить, контексты выполнения можно представить в виде стопки. Сначала может быть глобальный код со своим контекстом, этот код может запускать фукнции, с их контекстом, в свою очередь функция может вызвать другую функцию и так далее. Даже если функция рекурсивно вызывает сама себя, при каждом вызове происходит переход в новый контекст выполнения.
Объект активации / Объект переменных
Каждый контекст выполнения имеет связанный с ним так называемый объект переменных. Также как и контекст выполнения, объект переменных — это абстрактная сущность, механизм для описания процесса создания переменных. А теперь, интересное: переменные, и функции, определенные в исходном коде на самом деле добавляются как свойства к этому самому объекту.
Когда управление переходит контекст выполнения глобального кода, в качестве объекта переменных используется глобальный объект. Именно поэтому переменные и функции объявленные глобально становятся свойствами глобального объекта:
/* "this" ссылается на глобальный объект в глобальной области видимости */ var GLOBAL_OBJECT = this; var foo = 1; GLOBAL_OBJECT.foo; // 1 foo === GLOBAL_OBJECT.foo; // true function bar(){} typeof GLOBAL_OBJECT.bar; // "function" GLOBAL_OBJECT.bar === bar; // true
OK, глобальные переменные становятся свойствами глобального объекта, но что происходит с локальными переменными, которые мы определяем в коде функций? На самом деле все происходит примерно также: они становятся свойствами объекта переменных. Единственное отличие в том, что внутри кода функции объект переменных не глобальный объект, а так называемый объект активации. Объект активации создается каждый раз, когда происходит переход в контекст выполнения функции.
Кроме переменных и функций свойствами объекта активации становятся и и переданные в функцию аргументы. Они получают имена указанные для них при определении функции — так называемые формальные параметры. Аргументы не указанные в формальных параметрах (и указанные тоже) становятся свойствами специального объекта arguments, который становится в свою очередь свойством объекта активации. Обратите внимание, что объект активации – это внутренний механизм, к нему нет доступа из кода программы.
(function(foo){ var bar = 2; function baz(){} /* Образно говоря, специальный объект "arguments" становится свойством объекта активации родительской фукнции: ACTIVATION_OBJECT.arguments; // Объект arguments ...так же как и аргумент "foo": ACTIVATION_OBJECT.foo; // 1 ...как и переменная "bar": ACTIVATION_OBJECT.bar; // 2 ...как и функция, определенная локально: typeof ACTIVATION_OBJECT.baz; // "function" */ })(1);
И, наконец, переменные, объявленные внтури кода, пропущенного через Eval становятся свойствами объекта переменных того контекста, откуда Eval был вызван. Eval-код попросту использует объект переменных того контекста, в котором он был вызван:
var GLOBAL_OBJECT = this; /* "foo" становится свойстовм объекта переменных вызывающего контекста, в данном случае — глобального объекта */ eval('var foo = 1;'); GLOBAL_OBJECT.foo; // 1 (function(){ /* `bar` становится свойстовм объекта переменных вызывающего контекста, в данном случае — объект активации родительской функции */ eval('var bar = 1;'); /* Образно говоря, ACTIVATION_OBJECT.bar; // 1 */ })();
Атрибуты свойств
Мы почти закончили. Теперь, когда понятно, что происходит с переменными (они становятся свойствами), осталось понять что такое атрибуты свойств. Каждое свойство может иметь или не иметь несколько атрибутов из следующего списка: ReadOnly (только для чтения), DontEnum (не участвует в итерациях each), DontDelete (неудалимое) and Internal (внутреннее). Можно представить их как флаги — атрибут либо есть у свойства либо его нет. В рамках сегодняшнего обсуждения нас интересует только свойство DontDelete.
Когда объявленные переменные и функции становятся свойствами объекта переменных, либо объекта активации (для кода функций) либо глобального объекта (для глобального кода), эти свойства создаются с аттрибутом DontDelete. Однако все явные или неявные назначения свойств создают свойства без атрибута DontDelete. Именно поэтому мы можем удалять одни свойства и не можем — другие:
var GLOBAL_OBJECT = this; /* "foo" является свойством глобального объекта. Она создается с помошью объявления переменной и следовательно имеет атрибут DontDelete. Поэтому ее нельзя удалить. */ var foo = 1; delete foo; // false typeof foo; // "number" /* "bar" является свойством глобального объекта. Она создана с помошью объявления функции и следовательно имеет атрибут DontDelete. И поэтому ее тоже нельзя удалить. */ function bar(){} delete bar; // false typeof bar; // "function" /* "baz" также является свойством глобального объекта. Однако она создана через назначение свойства и поэтому не имеет атрибута DontDelete. Именно поэтому ее можно удалить. */ GLOBAL_OBJECT.baz = 'blah'; delete GLOBAL_OBJECT.baz; // true typeof GLOBAL_OBJECT.baz; // "undefined"
Встроенные свойства и DontDelete
Итак, речь об особом атрибуте свойства, который запрещает удаление этого свойства. Обратите внимание что некоторые свойства встроенных объектов имеют атрибут DontDelete и поэтому их нельзя удалить. Особая переменная arguments (она же — свойство объекта активации) имеет атрибут DontDelete. У каждой функции свойство length также имеет этот атрибут:
(function(){ /* "arguments" нельзя удалить из-за DontDelete */ delete arguments; // false typeof arguments; // "object" /* свойство фукнции "length" тоже нельзя удалить */ function f(){} delete f.length; // false typeof f.length; // "number" })();
Свойства являющиеся аругментами фукнции тоже создаются с атрибутом DontDelete, и поэтому их тоже нельзя удалить:
(function(foo, bar){ delete foo; // false foo; // 1 delete bar; // false bar; // 'blah' })(1, 'blah');
Необъявленные присвоения
Как вы, наверное, помните необъявленные присвоения создают свойства Глобального объекта. Кроме случаев, когда одноименное свойство нашлось где-то в цепочки областей видимости раньше Глобального объекта. И теперь, когда мы знаем разницу между назначением свойства и объявлением переменной — последнее устанавливает DontDelete, а первое — нет, должно быть понятно почему необъявленные присвоеня создают удаляемые свойства:
var GLOBAL_OBJECT = this; /* создает свойство глобального объекта с помощью объявления переменной; свойство имеет атрибут DontDelete */ var foo = 1; /* создает свойство глобального объекта с помошью необъявленного присвоения; свойство не имеет атрибута DontDelete */ bar = 2; delete foo; // false typeof foo; // "number" delete bar; // true typeof bar; // "undefined"
Обратите внимание, что атрибуты задается только в момент создания свойства (например, ни один из атрибутов не задан). Последующие присвоеня не меняют атрибутов существующих свойств. Это важно понимать.
/* "foo" создана как свойство с атрибутом DontDelete */ function foo(){} /* Последующие присвоения не изменяют атрибутов. DontDelete на месте! */ foo = 1; delete foo; // false typeof foo; // "number" /* Но назначение свойства, которое еще не существует создает свойство без атрибутов (и, соотвественно, без DontDelete) */ this.bar = 1; delete bar; // true typeof bar; // "undefined"
Путаница с Файрбагом
Так что же происходи в Файрбаге? Почему, переменные созданные в консоли можно удалить, в противовес тому, что мы только что узнали? Ну, как было сказано ранее, у Eval-кода свой подход к объявлению переменных. Переменные, объявленные в Eval-коде на самом деле создаются как свойства без DontDelete:
eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined"
и точно также в коде функции:
(function(){ eval('var foo = 1;'); foo; // 1 delete foo; // true typeof foo; // "undefined" })();
В этом чуть необычного поведения Файрбага. Похоже, весь текст в консоли парсится и выполняется через Eval, а не как глобальный или код функций. Очевидно, что любые объявленные переменные становятся свойствами без атрибута DontDelete, и поэтому их можно легко удалить. Имейте в виду различия между обычным глобальным кодом и консолью Файрбага.
Удаление переменных с помощью Eval
Есть очень интересное поведение функции eval, которое вкупе с одним из аспектов ECMAScript технически может позволить удалять неудаляемые свойства. Дело в том, что объявления функций могут перезаписывать переменные с таким же именем в том же контексте выполнения:
function x(){ } var x; typeof x; // "function"
Обратите внимание, как объявление функции перезаписывает одноименную переменную (или, другими словами, то же свойство объекта переменных). Это происходит потому что объявления функций выполняются после объявления переменных и могут их перезаписывать. Причем объявления функций не только перезаписывают старые значения свойств, но и заменяют их атрибуты. Если мы объявим функцию через Eval, то эта фукнция должна также заменить атрибуты старого свойства своими. А так как переменные определенные с помощью eval создают свойства без атрибута DontDelete, определение этой новой фукнции должно удалить существующий атрибут DontDelete у нужной переменной, делая ту удаляемой (и она, конечно, заменит значение свойства на ссылку на вновь созданную функцию).
var x = 1; /* Нельзя удалить, у "x" есть DontDelete */ delete x; // false typeof x; // "number" eval('function x(){}'); /* свойство "x" теперь является ссылкой на фукнцию и не должно иметь атрибута DontDelete */ typeof x; // "function" delete x; // должно быть "true" typeof x; // должно быть "undefined"
К несчастью этот трюк не работает ни в одной из реализаций, которые я пробовал. Возможно, я что-то упустил, либо такое поведение слишком неясно и авторы реализаци не считают нужным обращать на него внимание.
Совместимость браузеров
Полезно знать как это работает в теории, но практика важнее. Следуют ли браузеры стандартам в отношении создания и удаления переменных и свойств? В основном, да.
Я написал небольшой набор тестов, чтобы проверить соответствие работы оператора delete в глобальном коде, коде фукнций и Eval-коде. Тесты проверяют и значение, которое возвращает оператор delete, и то, удаляются ли (или не удалаются) свойства так, как должны или нет. Значение, которое возвращает delete не так важно как результат. Не очень важно, вернул ли delete true вместо false, но очень важно чтобы свойства с атрибутом DontDelete не удалялись и наборот.
Больщинство современных браузеров вполнее соответствуют стандартам. Если не считать тонкостей с eval, о которых я написал чуть выше, следующие браузеры полностью проходят все тесты: Opera 7.54+, Firefox 1.0+, Safari 3.1.2+, Chrome 4+.
У Safari 2.x и 3.0.4 есть проблемы с удалением аргументов фукнций; похоже, эти свойства создаются без атрибута DontDelete и их возможно удалить. У Safari 2.x больше проблем с удалением значения без переменной (например, delete 1) вызывает ошибку; объявления функций создают удаляемые свойства (но, не объявления переменных, как ни странно); объявленые через eval переменные становятся неудаляемыми (но к фукнциям это не относится).
Так же как и Safari, Konqueror (3.5, но не 4.3) вызывает ошибку при удалении значения без переменной (например, delete 1) и ошибочно делает аргументы функции удаляемыми.
Gecko DontDelete bug
Браузеры, основанные на Gecko 1.8.x: Firefox 2.x, Camino 1.x, Seamonkey 1.x, и так далее показывают интересный баг, когда явное назначение свойства может удалить у него атрибут DontDelete даже если свойство задано через объявление переменной или фукнции:
function foo(){} delete foo; // false (как положено) typeof foo; // "function" (как положено) /* теперь назначим свойство явно */ this.foo = 1; // ошибочно очищает атрибут DontDelete delete foo; // true typeof foo; // "undefined" /* обратите внимание, что такого не происходит когда присвоение происходит неявно */ function bar(){} bar = 1; delete bar; // false typeof bar; // "number" (несмотря на то, что присвоение затерло изначальное значение)
Удивительно, но Internet Explorer 5.5 – 8 проходит все тесты кроме удаления значения без ссылки (например, delete 1), так же как и в старом Safari он вызывает ошибку. Но на самом деле в IE есть несколько более серъезных багов, которые сразу незаметны. Это баги работы с глобальным объектом.
Баги IE
Целая глава про баги Internet Explorer? Как неожиданно!
В IE (по крайней мере, в 6-8), следующее выражение вызывает ошибку когда выполняется в глобальном коде:
this.x = 1; delete x; // TypeError: Объект не поддерживает это свойство или метод
и это тоже вызывает ошибку, но другую, просто чтобы было интереснее:
var x = 1; delete this.x; // TypeError: Не могу удалить 'this.x'
Такое впечатление, что в IE объявление переменных в глобальном коде не создает свойств глобального объекта. Создание свойства через назначение (this.x = 1) и последующее удаление с помощью delete x вызывает ошибку. Создание переменной с помощью объявления переменной (var x = 1) и ее последующее удаление вызывает другую ошибку.
Но и это еще не все. Создание переменной явным назначением на самом деле всегда вызывает ошибку при попытке удаления. Причем не только вызывается ошибка, но и похоже, у созданного свойства есть атрибут DontDelete, которого, конечно, быть не должно:
this.x = 1; delete this.x; // TypeError: Объект не поддерживает это действие typeof x; // "number" (все еще существует, не удалилась, а должна была!) delete x; // TypeError: Объект не поддерживает это свойство typeof x; // "number" (опять не удалилась)
Более того, в IE необъявленные назначения (те, которые должны создавать свойство глобального объекта) создают-таки в IE удаляемые свойства, а не так как что можно было подумать:
x = 1; delete x; // true typeof x; // "undefined"
Но если попробовать удалить это свойство ссылаясь на него через this в Глобальном коде (delete this.x), выскочит знакомая ошибка:
x = 1; delete this.x; // TypeError: Не могу удалить "this.x"
Если копнуть глубже, выяснится, что delete this.x выполненное в глобальном коде вообще не работает. Когда свойство создано через явное назначение (this.x = 1), delete вызывает ошибку; когда свойство создано через необъявленное назначение (x = 1) или через объявление (var x = 1), delete вызывает другую ошибку.
С другой стороны, delete x, вызывает ошибку только когда свойство создано через явное назначение — this.x = 1. Если свойство создано через объявление переменной (var x = 1), удаления не происходит никогда и оператор delete верно возвращает false. Если свойство создано через неявное наначение (x = 1), удаление работает как ожидается.
Я долго над этим думал в сентябре, и Гаретт Смит (Garrett Smith) подсказал, что в IE «Глобальный объект переменных является объектом JScript, а глобальный объект реализован браузером.» Garrett привел в качестве ссылки статью в блоге Эрика Липперта (Eric Lippert).
В некотором смысле, мы можем подтвердить эту теорию выполнив несколько тестов. Обратите внимание, что this и window вроде как ссылаются на один и тот же объект (если верить оператору ===), но объект переменных (на котором объявляется функция) отличается от того, на что ссылается this.
/* В Глобальном коде */ function getBase(){ return this; } getBase() === this.getBase(); // false this.getBase() === this.getBase(); // true window.getBase() === this.getBase(); // true window.getBase() === getBase(); // false
Заблуждения
Нельзя переоценить ценность знания того, как и почему работают вещи. В сети я встречал несколько заблуждений связанных с непониманием того, как работает оператор delete. Например, есть очень популярный ответ на Stackoverflow (с на удивление высоким рейтингом), уверенно заявляющий, что «delete не должен работать ни для чего кроме свойств объектов”. Но если понимать основы работы оператора delete становится понятно что этот ответ несколько неточен. Оператору delete все равно переменную он видит или свойство (фактически, для него все является ссылкой (reference)) и его заботит только наличие или отсутствие атрибута DontDelete (ну и существование самого свойства).
Также интересно наблюдать, как одни заблуждения порождают новые, когда в том самом обсуждении кто-то предложил удалять переменные (что не сработает, если только переменная не определена через eval), и другой участник его неверно поправляет, что возможно удалять переменные в глобальном коде и нельзя внутри функций.
Аккуратнее с объяснением работы яваскрипта в сети и в идеале всегда разберитесь как и почему оно работат так, а не иначе изначально 😉
delete и объекты браузера
Алгоритм работы delete описан примерно так:
- Если операнд не ссылка, вернуть true
- Если у объекта нет собственного свойства с таким именем, вернуть true (как мы помним, объект может быть объектом активации или глобальным объектом)
- Если свойство есть, но имеет атрибут DontDelete, вернуть false
- Иначе, удалить свойство и вернуть true
- Однако, поведение оператора delete в отношении объектов браузера может быть довольно непредсказуемым. И в этом нет ничего плохого: объектам браузера позволено спецификацией реализовывать любое поведение для таких операций, как чтение (внутренний метод [[Get]]), запись (внутренний оператор [[Put]]) или удаление (внутренний метод [[Delete]]) и некоторых других. Возможность реализовывать свое поведение для [[Delete]] делает объекты браузеров такими хаотичными.
Мы уже видели странности IE, когда удаление некоторых объектов, которые, похоже, реализованы как объекты браузера, вызывает ошибки. Некоторые версии Firefox вызывают ошибки когда мы пытаемся удалить window.location. Когда дело доходит до объектов браузера доверять значению, которое возвращает delete тоже нельзя. Взгляните, что происходит в Файрфоксе:
/* "alert" является прямым свойством объекта "window" (если верить методу "hasOwnProperty") */ window.hasOwnProperty('alert'); // true delete window.alert; // true typeof window.alert; // "function"
Удаление window.alert возвращает true несмотря на то, что такого происходить не должно. Свойство разрешается в ссылку (и поэтому первое условие алгоритма не должно сработать). Это прямое свойство объекта window (и второе условие не должно работать). Единственное оставшееся условие когда delete может вернуть true это на самом деле удалить свойство. Однако это свойство не удаляется.
Мораль — не доверяйте объектам браузера.
Режим строгого соответствия ES5
Что нового добавляет режим строгого соответствия спецификации в пятом издании ECMAScript? Добавлено несколько ограничений. Если выражения в операторе delete является прямой ссылкой на переменную, аргумент функции или идентификатор функции то возникнет ошибка SyntaxError. Кроме того, если свойство имеет внутреннее свойство [[Configurable]] == false, возникнет ошибка TypeError:
(function(foo){ "use strict"; // включает строгий режим внутри этой фукнции var bar; function baz(){} delete foo; // SyntaxError (удаляем аргумент) delete bar; // SyntaxError (удаляем переменную) delete baz; // SyntaxError (удаляем переменную созданную объявлением фукнции) /* "length" для эксземпляров функци имеет { [[Configurable]] : false } */ delete (function(){}).length; // TypeError })();
Удаление необъявленных переменных (другими словами, тех, которые не разрешаются в ссылку) также вызывает ошибку SyntaxError:
"use strict"; delete i_dont_exist; // SyntaxError
В каком-то смысле это похоже на то как работает неявное присвоение в строгом режиме (с той разницей, что возвращается ReferenceError вместо SyntaxError):
"use strict"; i_dont_exist = 1; // ReferenceError
Как вы понимаете, все эти ограничений имеют смысл, учитывая, сколько непонимания вызывает удаление переменных, функций и аргументов. Вместо того чтобы тихо игнорировать удаление строгий режим принимает более жесткие и понятные меры.
Вывод
Статься получилась и так довольно объемной чтобы обсуждать такие вещи как удаление элементов массива с помощью delete b и какие сложности это с собой несет. Всегда можно обратиться к статье на MDC за объяснением или почитать спецификации чтобы поэкспериментировать самому.
Вот, вкратце, как работает удаление в яваскрипте:
- Переменные и функции являются либо объекта активации либо глобального объекта.
- Свойства имеют атрибуты, один из которых, DontDelete, отвечает за возможность удаления свойства.
- Переменные и функции в глобальном и коде функций всегда создают свойства с DontDelete.
- Аргументы функций также являтся свойствами объекта активации и создаются с атрибутом DontDelete.
- Переменные и функции в Eval-коде всегда создают свойства без DontDelete.
- Новые свойства всегда создаются без атрибутов и, соответственно, без DontDelete.
- Объекты браузера могут реагировать на удаление как им вздумается.
Если вам хочется поглубже узнать про обсуждаемое явление обратитесь к 3-му изданию спецификации ECMA-262.
Надеюсь, вам понравился этот обзор и вы узнали что-то новое.
ссылка на оригинал статьи http://habrahabr.ru/post/155849/
Добавить комментарий