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:
obj.length
to the server for logging prior to calling each()
(confirming that length
was undefined
)delete obj.length
prior to calling each()
(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.
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();
This isn't an answer but rather an analysis of what's going on under the covers after much testing. I hope that, after reading this, someone on either safari mobile side or the JavaScript VM on iOS side can take a look and correct the issue.
We can confirm that the _.each() function is treating js objects {} as arrays [] because the safari browser returns the 'length' property of an object as an integer. BUT ONLY IN CERTAIN CASES.
If we use an object map where the keys are integers:
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!!!
Inspecting with the debugger on mobile Safari browser we clearly see that obj.length is "undefined". However stepping to next line:
var length = obj.length;
the length variable is clearly being assigned the value 26 which is an integer. The integer part is important because the bug in underscore.js occurs at these two lines in the underscore.js code:
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)
However if we were to change the object in question just slightly and add a key-value pair where the key is a string (and is the last item in object) such as:
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'
More interestingly, if we change the object again and a key-value pair such as:
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!
It appears that the bug (wherever it is happening: Safari/JavaScript VM) looks at the key of last item in the object and if it is an integer adds one (+1) to it and reports that as a length of the object...even though obj.hasOwnProperty('length') comes back as false.
This occurs on:
This does not occur on:
Because we can't really prove it with a jsFiddle we did the next best thing and got a screen capture of it stepping through the debugger. We posted the video on youTube and it can be seen at this location:
https://www.youtube.com/watch?v=IR3ZzSK0zKU&feature=youtu.be
As stated above this is just an analysis of the problem in more detail. We are hoping someone with more understanding under the hood can comment on the situation.
One simple solution is to NOT USE _.each function (or the jQuery equivalent). We can confirm that using angular.js forEach function remedies this issue. However, we use underscore.js pretty extensively and _.each is used nearly everywhere we iterate through arrays/collections.
Update
This was confirmed as a bug and there is now a fix for this bug on WebKit as of 2015-03-27:
fix: http://trac.webkit.org/changeset/182058
original bug report: https://bugs.webkit.org/show_bug.cgi?id=142792
For anyone looking at this and using jQuery, just a heads' up that this has now been fixed in versions 2.1.4 and 1.11.3, which specifically only contain a hot-fix to the above issue:
http://blog.jquery.com/2015/04/28/jquery-1-11-3-and-2-1-4-released-ios-fail-safe-edition/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With