A brief summary for anyone landing here from Google: There is a bug in iOS8 (on 64-bit devices only) that intermittently causes a phantom "length" property to appear on objects that only have numeric properties. This causes functions such as $.each() and _.each() to incorrectly try to iterate your object as an array.
I have filed an issue report (really a workaround request) with jQuery (https://github.com/jquery/jquery/issues/2145), and there is a similar issue on the Underscore tracker (https://github.com/jashkenas/underscore/issues/2081).
Update: This is a confirmed webkit bug. A fix was comitted on 2015-03-27, but there is no indication as to which version of iOS will have the fix. See https://bugs.webkit.org/show_bug.cgi?id=142792. Currently iOS 8.0 - 8.3 are known to be affected.
Update 2: A workaround for the iOS bug can be found in jQuery 2.1.4+ and 1.11.3+ as well as Underscore 1.8.3+. If you're using any of these versions, then the library itself will behave properly. However, it's still up to you to ensure that your own code isn't affected.
This question can also be called: "How can an object without a length have a length?"
I'm having a twilight zone kind of issue with mobile Safari (seen on both iPhones and iPads running iOS 8). My code has a lot of intermittent failures using the "each" implementation of both jQuery ($.each()
) and Underscore (_.each()
).
After some investigation, I discovered that in all cases of failure, the each
function was treating my object as an array. It would then try to iterate it like an array (obj[0]
, obj[1]
, etc.) and would fail.
Both jQuery and Underscore use the length
property to determine if an argument is an object or an array/array-like collection. For example, Underscore uses this test:
if (length === +length) { ... this is an array
My objects had no length parameter, yet they were triggering the above if
statements. I double validated that there was no length
by:
- Sending the value of
obj.length
to the server for logging prior to callingeach()
(confirming thatlength
was undefined
) - Calling
delete obj.length
prior to callingeach()
(this didn't change anything.)
I have finally been able to capture this behavior in the debugger with an iPhone attached to Safari on a Mac.
The following picture shows that $.isArrayLike thinks that length
is 7.
However, a console trace shows that length
is undefined
, as expected:
At this point I believe this is a bug in iOS Safari, especially since it's intermittent. I'd love to hear from others who's seen this problem and perhaps found a way to counter it.
Update
I was asked to create a fiddle of this, but unfortunately I can't. There seems to be a timing issue (which may even differ between devices) and I can't reproduce it in a fiddle. This is the minimum set of code I was able to repro the problem with, and it requires an external .js file. With this code happens 100% of the time on my iPhone 6 running 8.1.2. If I change anything (e.g. making the JS inline, removing any of the unrelated JS code, etc), the problem goes away.
Here is the code:
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();