iOS 上 jQuery.each() 和 Underscore.each() 的神秘失败

Posted

技术标签:

【中文标题】iOS 上 jQuery.each() 和 Underscore.each() 的神秘失败【英文标题】:Mysterious failure of jQuery.each() and Underscore.each() on iOS 【发布时间】:2015-01-26 17:54:41 【问题描述】:

简要总结给从 Google 登陆这里的任何人:ios8 中存在一个错误(仅限 64 位设备),它会间歇性地导致幻像“长度”属性出现在只有数值属性。这会导致 $.each() 和 _.each() 等函数错误地尝试将对象迭代为数组。

我已经向 jQuery (https://github.com/jquery/jquery/issues/2145) 提交了问题报告(实际上是一个解决方法请求),并且在 Underscore 跟踪器 (https://github.com/jashkenas/underscore/issues/2081) 上也存在类似问题。

更新:这是一个已确认的 webkit 错误。 2015 年 3 月 27 日提交了一个修复程序,但没有迹象表明哪个版本的 iOS 将有修复程序。见https://bugs.webkit.org/show_bug.cgi?id=142792。目前已知 iOS 8.0 - 8.3 会受到影响。

更新 2: 可以在 jQuery 2.1.4+ 和 1.11.3+ 以及 Underscore 1.8.3+ 中找到 iOS 错误的解决方法。如果您使用这些版本中的任何一个,则库本身将正常运行。但是,您仍需确保自己的代码不受影响。

这个问题也可以叫做:“没有长度的对象怎么会有长度?”

我在使用移动 Safari 时遇到了一些问题(在运行 iOS 8 的 iPhone 和 iPad 上都可以看到)。我的代码在使用 jQuery ($.each()) 和 Underscore (_.each()) 的“每个”实现时出现了很多间歇性故障。

经过一番调查,我发现在所有失败的情况下,each 函数都将我的对象视为一个数组。然后它会尝试像数组一样迭代它(obj[0]obj[1] 等)并且会失败。

jQuery 和 Underscore 都使用 length 属性来确定参数是对象还是类数组/数组的集合。例如,Underscore 使用这个测试:

if (length === +length)  ... this is an array

我的对象没有长度参数,但它们触发了上述if 语句。我再次确认没有length by:

    在调用each()之前将obj.length的值发送到服务器进行日志记录(确认lengthwas undefined) 在调用 each() 之前先调用 delete obj.length(这并没有改变任何东西。)

我终于能够在调试器中捕获此行为,并将 iPhone 连接到 Mac 上的 Safari。

下图显示$.isArrayLike认为length是7。

但是,控制台跟踪显示 lengthundefined,正如预期的那样:

在这一点上,我认为这是 iOS Safari 中的一个错误,尤其是因为它是间歇性的。我很想听听其他看到此问题并可能找到解决方法的人。

更新

我被要求创建一个小提琴,但不幸的是我做不到。似乎存在时间问题(设备之间甚至可能不同),我无法在小提琴中重现它。这是我能够重现问题的最少代码集,它需要一个外部 .js 文件。这段代码在我运行 8.1.2 的 iPhone 6 上 100% 发生。如果我更改任何内容(例如,使 JS 内联、删除任何不相关的 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: 'Just me', 2: '2', 3: '3', 4: '4', 5: '5' , 6: 'More than 5' 并作为参数传递给另一个名为 $.each() 的函数。 您能在fiddle 中重现问题吗? @dystroy 我看看能不能搞定它。 【参考方案1】:

这不是一个答案,而是在经过多次测试后对幕后发生的事情的分析。我希望在阅读完这篇文章后,无论是 Safari 移动端还是 iOS 端 javascript VM 的人都可以看看并纠正这个问题。

我们可以确认 _.each() 函数将 js 对象 视为数组 [],因为 safari 浏览器将对象的“长度”属性作为整数返回。 但仅在某些情况下

如果我们使用键是整数的对象映射:

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 是“未定义的”。但是进入下一行:

var length = obj.length;

长度变量显然被赋值为 26,它是一个整数。整数部分很重要,因为 underscore.js 的 bug 出现在 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)会查看对象中最后一项的 key,如果它是整数,则将其加一 (+1)并报告为对象的长度...即使 obj.hasOwnProperty('length') 返回为 false。

这发生在:

iOS 版本 8.1.1、8.1.2、8.1.3 的一些 iPad(但不是我们拥有的所有 iPad) 它确实发生在 iPad 上,它始终如一地发生......每次 仅限 iOS 上的 Safari 浏览器

这不会发生在:

我们尝试使用 iOS 8.1.3 的任何 iPhone(同时使用 Safari 和 Chrome) 任何装有 iOS 7.x.x 的 iPad(同时使用 Safari 和 Chrome) iOS 上的 chrome 浏览器 我们尝试使用上述 iPad 创建的任何 js fiddles 始终会产生错误

因为我们无法用 jsFiddle 真正证明这一点,所以我们做了次好的事情,并通过调试器获取了它的屏幕截图。我们在 youtube 上发布了视频,可以在这个位置看到:

https://www.youtube.com/watch?v=IR3ZzSK0zKU&feature=youtu.be

如上所述,这只是对问题的更详细分析。我们希望有更深入了解的人可以对这种情况发表评论。

一个简单的解决方案是不使用 _.each 函数(或等效的jQuery)。我们可以确认使用 angular.js forEach 函数可以解决这个问题。但是,我们非常广泛地使用 underscore.js,并且 _.each 几乎在我们遍历数组/集合的所有地方都使用。

更新

这已被确认为一个错误,并且在 2015 年 3 月 27 日的 WebKit 上已对此错误进行了修复:

修复: 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_.take 之间存在重大变化。

对于不熟悉 lodash 的任何人 - 它是下划线的一个分支,现在 (imo) 是一个更好的解决方案。

非常感谢@OzSolomon 对这个问题的解释、描述和解决。

如果我能悬赏某个问题,我会做的。

【讨论】:

【参考方案4】:

我已经处理了一段时间可能类似或相同的问题。我工作的公司有一款使用 KineticJS 4.3.2 的游戏。在 html5 画布上对图形对象进行分组时,Kinetic 会大量使用数组。 发生的问题是有时数组上缺少推送,或者存储在数组中的对象上的属性丢失。该问题发生在 Safari 中以及在 iOS8 中从主屏幕运行时。由于某种原因,在 iOS 上的 Chrome 中运行时没有出现此问题。连接调试器时也没有出现该问题。 在网上大量测试和搜索后,我们认为这是 iOS 上 webkit 的 JIT 优化错误。一位同事找到了以下链接。

TypeError: Attempted to assign to readonly property. in Angularjs application on iOS8 Safari

https://github.com/angular/angular.js/issues/9128

http://tracker.zkoss.org/browse/ZK-2487

目前,我们通过在访问不起作用的数组时删除点符号来解决问题(.children 已替换为 [ "children" ])。 我无法创建 jsFiddle。

【讨论】:

【参考方案5】:

问题在于用户代码,而不是 iOS8 webkit。

var obj = 
  23: 'a',
  24: 'b',
  25: 'c'

当应该使用字符串时,上面的映射键是整数。它不是 javascript 标准的有效映射,因此行为未定义,Apple 选择将“obj”解释为 26 个元素(索引 0 到 25 包括在内)的数组。

使用字符串键,长度​​问题就会消失:

var obj = 
  '23': 'a',
  '24': 'b',
  '25': 'c'

【讨论】:

这不是真的。整数键是完全有效的(尽管 IIRC js 引擎应该将它们转换为字符串(或至少假装)。虽然你的修复可以从我读过的内容中工作。这种行为只是一个错误。 指向 ECMAscript 规范中的部分。您描述的是常见做法,而不是标准。 这就是我如何解释对象文字的语法规范(注意属性名下的数字文字)``` ObjectLiteral : PropertyNameAndValueList PropertyNameAndValueList , PropertyNameAndValueList : PropertyAssignment PropertyNameAndValueList , PropertyAssignment PropertyAssignment : PropertyName : AssignmentExpression PropertyName : IdentifierName StringLiteral NumericLiteral ``` ecma-international.org/ecma-262/5.1/#sec-11.1.5

以上是关于iOS 上 jQuery.each() 和 Underscore.each() 的神秘失败的主要内容,如果未能解决你的问题,请参考以下文章

jquery $().each和$.each()

Jquery之each函数详解

jQuery .each 函数

jquery each 遍历

jquery each 和 map 区别

Jquery each&forEach