Таинственный провал jQuery.each() и Underscore.each() на iOS
Краткий обзор для всех, кто приземляется здесь от Google: в iOS8 есть ошибка (только на 64-разрядных устройствах), которая периодически вызывает свойство phantom "length", которое появляется на объектах, которые имеют только числовые свойства. Это вызывает такие функции, как $.each() и _.each(), чтобы попытаться выполнить итерацию вашего объекта в виде массива.
Я опубликовал отчет об ошибке (действительно, обходной запрос) с jQuery (https://github.com/jquery/jquery/issues/2145), и есть аналогичная проблема на Подчеркивание трекера (https://github.com/jashkenas/underscore/issues/2081).
Обновление: Это подтвержденная ошибка веб-кита. Исправление было выполнено в 2015-03-27, но нет никаких указаний относительно того, какая версия iOS будет иметь исправление. См. https://bugs.webkit.org/show_bug.cgi?id=142792. В настоящее время известно, что iOS 8.0 - 8.3 затронуты.
Обновление 2: Обходной путь для ошибки iOS можно найти в jQuery 2.1.4+ и 1.11.3+, а также в Underscore 1.8.3+. Если вы используете какую-либо из этих версий, то сама библиотека будет вести себя правильно. Однако вам все равно, чтобы ваш собственный код не был затронут.
Этот вопрос также можно вызвать: "Как объект без длины имеет длину?"
У меня проблема с сумеречной зоной с мобильным Safari (видно на обоих iPhone и iPad, работающих под управлением iOS 8). В моем коде много прерывистых сбоев, использующих "каждую" реализацию как jQuery ($.each()
), так и Underscore (_.each()
).
После некоторого расследования я обнаружил, что во всех случаях сбоя функция each
обрабатывала мой объект как массив. Затем он попытался бы повторить его как массив (obj[0]
, obj[1]
и т.д.) И потерпит неудачу.
Оба jQuery и Underscore используют свойство length
, чтобы определить, является ли аргумент объектом или массивом или массивом. Например, Underscore использует этот тест:
if (length === +length) { ... this is an array
У моих объектов не было параметра длины, но они вызывали вышеуказанные выражения if
. Я дважды подтвердил, что не было length
:
- Отправка значения
obj.length
на сервер для ведения журнала до вызова each()
(подтверждение того, что length
was undefined
)
- Вызов
delete obj.length
до вызова each()
(это ничего не изменило.)
Наконец-то я смог зафиксировать это поведение в отладчике с iPhone, прикрепленным к Safari на Mac.
На следующем рисунке показано, что $.isArrayLike считает, что length
равно 7.
![Debugger stopped in $.isArrayLike]()
Однако консольная трасса показывает, что length
- undefined
, как и ожидалось:
![Console trace]()
В этот момент я считаю, что это ошибка в Safari iOS, тем более, что она прерывистая. Мне бы хотелось услышать от других, кто видел эту проблему, и, возможно, нашел способ противостоять ей.
Update
Меня попросили создать скрипку, но, к сожалению, я не могу. Кажется, существует проблема синхронизации (которая может даже отличаться между устройствами), и я не могу воспроизвести ее в скрипке. Это минимальный набор кода, с которым я смог воспроизвести проблему, и требует внешнего .js файла. С этим кодом происходит 100% времени на моем iPhone 6, работающем под управлением 8.1.2. Если я что-то изменил (например, сделав JS inline, удалив любой из несвязанных JS-кодов и т.д.), Проблема исчезнет.
Вот код:
index.html
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="script.js"></script>
</head>
<body>
Should say 3:
<div id="res"></div>
<script>
function trigger_failure() {
var obj = { 1: '1', 2: '2', 3: '3' };
print_last(obj);
}
$(window).load(trigger_failure);
</script>
</body>
</html>
script.js
function init_menu()
{
var elemMenu = $('#menu');
elemMenu
.on('mouseenter', function() {})
.on('mouseleave', function() {});
elemMenu.find('.menu-btn').on('touchstart', function(ev) {});
$(document).on('touchstart', function(ev) { });
return;
}
function main_init()
{
$(document).ready(function() {
init_menu();
});
}
function print_last(obj)
{
var a = $($.parseHTML('<div></div>'));
var b = $($.parseHTML('<div></div>'));
b.append($.parseHTML('foo'));
$.each(obj, function(key, btnText) {
document.getElementById('res').innerHTML = ("adding " + btnText);
});
}
main_init();
Ответы
Ответ 1
Это не ответ, а скорее анализ того, что происходит под обложками после долгих испытаний. Надеюсь, что после прочтения этого, кто-то на стороне сафари или на стороне JavaScript на стороне iOS может взглянуть и исправить проблему.
Мы можем подтвердить, что функция _.each() обрабатывает js objects {} как массивы [], потому что браузер Safari возвращает свойство length для объекта как целого. НО ТОЛЬКО В НЕКОТОРЫХ СЛУЧАЕ.
Если мы используем карту объекта, где ключи являются целыми числами:
var obj = {
23:'some value',
24:'some value',
25:'some value'
}
obj.hasOwnProperty('length'); //...this comes out as 'false'
var length = obj.length; //...this returns 26!!!
Проверяя с помощью отладчика в браузере браузера Safari, мы ясно видим, что obj.length является "undefined". Однако переход к следующей строке:
var length = obj.length;
переменной длины явно присваивается значение 26, которое является целым числом. Целочисленная часть важна, потому что ошибка в underscore.js происходит в этих двух строках в коде underscore.js:
var i, length = obj.length;
if (length === +length) { //... it treats an object as an array because
//... it has assigned the obj (which has no own
//... 'length' property) an actual length (integer)
Однако, если бы мы немного изменили рассматриваемый объект и добавили пару ключ-значение, где ключ является строкой (и является последним элементом в объекте), например:
var obj = {
23:'some value',
24:'some value',
25:'some value',
'foo':'bar'
}
obj.hasOwnProperty('length'); //...this comes out as 'false'
var length = obj.length; //...this now returns 'undefined'
Более интересно, если мы снова изменим объект и пару ключ-значение, например:
var obj = {
23:'some value',
24:'some value',
25:'some value',
75:'bar'
}
obj.hasOwnProperty('length'); //...this comes out as 'false'
var length = obj.length; //...this now returns 76!
Похоже, что ошибка (где бы она ни происходила: Safari/JavaScript VM) просматривала ключ последнего элемента в объекте и, если это целое число, добавляет к нему (+1) и сообщает, что в качестве длины объекта... хотя obj.hasOwnProperty('length') возвращается как false.
Это происходит при:
- некоторые iPads (но НЕ ВСЕ, что у нас есть) с iOS версии 8.1.1, 8.1.2, 8.1.3
- iPads, с которым он происходит, это происходит последовательно... каждый раз
- только браузер Safari на iOS
Это не происходит:
- любые iPhone, которые мы пытались использовать в iOS 8.1.3 (с использованием как Safari, так и Chrome)
- любые iPads с iOS 7.x.x(с использованием как Safari, так и Chrome)
- браузер Chrome на iOS
- любые js-скрипты, которые мы пытались создать, используя вышеупомянутые iPads, которые последовательно создавали ошибку.
Поскольку мы не можем действительно доказать это с помощью jsFiddle, мы сделали следующее самое лучшее и получили экранный захват, который прошел через отладчик. Мы разместили видео на youTube, и его можно увидеть в этом месте:
https://www.youtube.com/watch?v=IR3ZzSK0zKU&feature=youtu.be
Как указано выше, это просто анализ проблемы более подробно. Мы надеемся, что кто-то с большим пониманием под капотом может прокомментировать ситуацию.
Одним простым решением является функция НЕ ИСПОЛЬЗОВАТЬ _.each(или эквивалент jQuery). Мы можем подтвердить, что использование функции angular.js forEach устраняет эту проблему. Тем не менее, мы используем underscore.js довольно широко и _.each используется почти везде, где мы перебираем массивы/коллекции.
Обновление
Это было подтверждено как ошибка, и теперь есть исправление для этой ошибки в WebKit по состоянию на 2015-03-27:
fix: http://trac.webkit.org/changeset/182058
исходный отчет об ошибке: https://bugs.webkit.org/show_bug.cgi?id=142792
Ответ 2
Для тех, кто смотрит на это и использует jQuery, только голова, которая теперь исправлена в версиях 2.1.4 и 1.11.3, которые специально содержат только горячие исправления для выше вопроса:
http://blog.jquery.com/2015/04/28/jquery-1-11-3-and-2-1-4-released-ios-fail-safe-edition/
Ответ 3
Обновление до последней версии lodash (3.10.0) исправило эту проблему для меня.
Обратите внимание, что в этой версии lodash есть разрывное изменение с _.first
vs _.take
.
Для тех, кто не знаком с lodash - это вилка подчеркивания, которая в наши дни (imo) является лучшим решением.
На самом деле большое спасибо @OzSolomon за объяснение, описание и выяснение этой проблемы.
Если бы я смог дать щедрость на вопрос, я бы это сделал.
Ответ 4
Я работал некоторое время с чем-то, что могло бы быть аналогичным или той же проблемой. В компании, в которой я работаю, есть игра, в которой используется KineticJS 4.3.2. Kinetic интенсивно использует массивы при группировке графических объектов на холсте html5.
Возникшие проблемы заключались в том, что в массиве иногда отсутствовал push или что свойства объекта, хранящегося в массиве, отсутствовали. Проблема возникла в Safari и при запуске с рабочего стола в iOS8. По какой-то причине проблема не возникла при запуске в Chrome на iOS. Проблема также не возникала при подключении отладчика.
После большого тестирования и поиска в сети мы считаем, что это ошибка оптимизации JIT в webkit на iOS. Коллега нашел следующие ссылки.
TypeError: Попытка назначить свойство readonly. в приложении Angularjs на Safari iOS8
https://github.com/angular/angular.js/issues/9128
http://tracker.zkoss.org/browse/ZK-2487
В настоящее время мы сделали обходной путь, удалив dot-notation при доступе к массиву, который не работал (.children был заменен на [ "children" ]).
Я не смог создать jsFiddle.
Ответ 5
Проблема заключается в пользовательском коде, а не в iOS8 webkit.
var obj = {
23: 'a',
24: 'b',
25: 'c'
}
Ключи карты выше - целые числа, когда нужно использовать строки. Это не допустимая карта по стандарту javascript, поэтому поведение undefined, и Apple предпочитает интерпретировать "obj" как массив из 26 элементов (индексы от 0 до 25 включительно).
Используйте строковые ключи, и проблема длины будет отсутствовать:
var obj = {
'23': 'a',
'24': 'b',
'25': 'c'
}