пятница, 11 мая 2007 г.

Итерирование по свойствам объекта в JavaScript.

Любой объект в JavaScript может быть расширен в рантайме. Более того, прототип (свойство prototype) любого объекта может быть также расширен, что влечет за собой изменение свойств всех объектов, унаследованных от измененного. Все добавленные в объект либо в прототип объекта-предка свойства и методы будут видны при использовании for (name in obj) {...}.

Крайний случай - это расширение встроенного объекта Object, от которого унаследовано практически все что есть в JavaScript (за некоторыми специфичными для разных браузеров исключениями). Например, библиотека json.js для работы с данными JSON от Дугласа Крокфорда расширяет Object методом toJSONString, который затем присутствует в любом объекте. Если данная библиотека загружена на странице, и у вас есть Firebug (если нету, то сейчас же идите по ссылке), зайдите в его консоль и напишите следующее:

var x = {};
for (i in x) {
console.log(i);
}


Вы увидите вывод "toJSONString" в консоли, хотя подразумевали, что объект пустой. Если вы добавите в объект данные, например x = {a:1, b:2}, то кроме свойств a и b вы также увидите метод toJSONString. Думаю, это не совсем то что вы хотели получить.

Что же делать, чтобы получить только то что было нужно, а именно только a и b? Нужно использовать метод объекта hasOwnProperty(propertyName). Этот метод возвращает true если свойство/метод присущи именно данному экземпляру, а не были получены им через цепочку прототипов.

var x = {a:1, b:2};
for (i in x) {
if (x.hasOwnProperty(i)) {
console.log(i);
}
}


Получаем только a и b.

Иллюстрацию того что может быть если этого не делать можно пока что посмотреть на странице моей предыдущей статьи. Там загружен виджет Tagometer от Del.icio.us, скрипт которого валится с ошибкой из-за того, что на странице также присутствует json.js. Скрипт тагометра использует объекты для хранения данных в свойствах, и не проверяет при итерировании, присвоено ли свойство объекту им самим, а не приехало из какого-либо другого скрипта через Object.prototype. В результате на одной из итераций получается метод toJSONString, а не объект, который ожидает код виджета.

Встроенный Object также вовсю расширяет такая известная и распространенная библиотека как Prototype (N.B. вскрытие показало, что он расширяет прототипы не Object, а других встроенных объектов, как String, Number, Array, Function, что более безопасно в данном контексте) и еще вагон и маленькая тележка. И, хотя в целом такая практика считается порочной, с этим следует считаться и писать свои скрипты так чтобы они могли обрабатывать такие ситуации и были совместимы с такими приемами. Окружение, где будет работать ваш скрипт, не всегда предсказуемо.

P.S.
Данная заметка была написана в процессе довольно продолжительной переписки со службой поддержки Del.icio.us по поводу вышеупомянутого бага. Убедить их в том что это баг, было непросто, даже ссылаясь на Дугласа Крокфорда, который, кроме всего прочего еще и один из основных архитекторов всего что касается JavaScript в Yahoo (чьим подразделением является Del.icio.us). Это и послужило поводом, т.к. я понял, что проблема недостаточно освещена. Сегодня я получил последний ответ. "I talked it over with our engineers and we decided to fix the issue. WE should have a fix in production later this week or early next." Затем увидел свежую статью Крокфорда про правильный способ итерирования по свойствам объекта в JavaScript в YUI Blog. Я категорически не утверждаю, что и эта статья проявилась в результате моей переписки :)

2 комментария:

  1. Максим, Вы допустили одну фактическую ошибку: prototype.js не изменяет Object.prototype. И на мой взгляд это правильно. А дописывать в каждый foreach лишний if(x.hasOwnProperty()) — неправильно. Надо писать библиотеки так, чтобы ими было удобно пользоваться, а не приходилось подстраиваться под них, как в случае json.js. С ним я просто столкнулся недавно по работе: когда нашел причину ошибки (тот самый toJSONString()), я не стал менять свой код, а переписал json.js.

    ОтветитьУдалить
  2. 2Victor
    Да, Вы правы, я погорячился. Prototype не расширяет Object.prototype, он расширяет другие встроенные объекты, такие как Number, String, Array, Function.
    Это уменьшает вероятность описанной ошибки, но все равно оставляет такую возможность.

    Но это не отменяет тот факт, что любой объект JavaScript в любой момент может быть расширен, и тут можно спорить до упора, но язык это позволяет, значит это случается. Если не делать такую проверку, то вероятность того что код сломается в непредсказуемом окружении, существует. И библиотеки надо писать с учетом этого факта.

    ОтветитьУдалить

Постоянные читатели