Коварный баг длины в iOS 8 пагубно влияет на jQuery, итерацию объектов Underscore

Перевод поста iOS 8 insidious length bug breaks jQuery and Underscore object iteration, автор Guilherme Rodrigues

Спросите любого, и вам скажут, что я люблю Underscore. Я даже преподаю этому по мере возможности. Также вам скажут, что я не люблю айфоны...

Если у вас есть большое приложение на JavaScript , которое в значительной степени опирается на итерацию функций из JQuery (jQuery.each) или Underscore  (_.each,_.map и т.д.), у вас возникнет большая проблема с пользователями IOS. В частности, IOS 8.2 полностью ломает объект итерации на некоторых устройствах. Так что готовьтесь убить весь день на отладку iPhone. Если только кто-то не избавит вас от этой участи.

Неожиданное путешествие

Сегодня мы получили уведомление, что наш расчётный процесс в VTeX был неисправен на iPhone 6. Мы проверили Analytics и IOS 8 - все было в порядке. Странно...

Мы проверили несколько других устройств IOS и не смогли воспроизвести ошибку. Мы пробовали IOS-симулятор, запущенный под прошивкой 8.2: не повезло. В конце концов, мы получили на руки iPhone 6 под IOS 8.2, и мы могли наконец воссоздать. Так, как ... иногда.

К тому времени мы знали, что должны произвести отладку.

Безумие? Это undefined

Сама ошибка была довольно не информативной, как обычно, в JavaScript (Даже несмотря на то, что команда Chrome работает над улучшением!). Что-то должно был иметь lenght как undefined.


TypeError: Cannot read property 'length' of undefined

Мы в конечном счете разыскали код, который вызвал ошибку, что-то вроде этого:


// things = [{sellerId:'1', name: 'foo'}, {sellerId:'1', name: 'bar'}, {sellerId:'2', name: 'qux'}] 

var thingsGroupedById = _.groupBy(things, 'sellerId'); 
// { 1: [...], 2: [...] }

_.map(thingsGroupedById, function (things) {
  // things is undefined... only on iOS 8!
  for (var i = 0; i < things.length; i++) {...}
});

Если вы используете Underscore, вы будете знать, что отображение объекта совершенно нормально. Ожидаемый результат - получить value, key в функции. В этом случае, things должно быть массивом в ключах 1 и 2.

В настоящее время, мы становимся слегка обезумевшими.

Если это крякает, как утка

В JavaScript, массивы и объекты схожи, но это  не то же самое. Вы можете получить доступ к карте таким же образом, как вы можете получить доступ к массиву.


var a = [1,2,3]
a[0] // 1
var b = {0: 'foo'}
b[0] // 'foo'

Тем не менее, на этом сходство заканчивается. Массив имеет параметр lenght , в котором отсутствует объект (если специально не оговорено). Таким образом, многие библиотеки используют длину, чтобы определить, массив ли это.

Судя по всему, iOS 8.2 представила очень креативную ошибку. Это создает фантом свойства длины, доступный на объектах с ключами integer! Это инициирует ошибки в многих библиотеках, думая, что объект, который вы даете им - это массив. Серьезно, iOS?

(Если вам нужно подробное объяснение, посмотрите обсуждение Reddit по ссылке ниже)

Мы обнаружили, что ошибка была зарегистрирована в обсуждении на Stack Overflow. Кроме того, обсуждение вопроса уже идет в репозиториях JQuery и Underscore.

ОБНОВЛЕНИЕ: Реддитор BenjaminPoulain любезно подметил, что исправление этой ошибки доступно на WebKit. Благодарю!

Работа, работа, работа

Поскольку мы не могли заменить наш файл Underscore в данный момент, здесь сработает только творческий метод. Наш обход достаточно простой.


var thingsGroupedById = _.groupBy(things, function(t){ return 'seller' + t.sellerId });
// { seller1: [...], seller2: [...] }

Так как мы никогда не использовали ключ для чего-нибудь еще, мы просто добавили строку префикса, чтобы iOS была такой "умной". Мы, вероятно, увидим исправленные релизы этих библиотек достаточно скоро как pull-запросы уже предложены.