Подтвердить что ты не робот

Таинственный провал 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();
4b9b3361

Ответ 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

Ответ 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'
}