javascript - Error misterioso de jQuery.each() y Underscore.each() en iOS
underscore.js mobile-safari (5)
Un breve resumen para cualquiera que llegue aquí desde Google: hay un error en iOS8 (solo en dispositivos de 64 bits) que hace que aparezca intermitentemente una propiedad fantasma de "longitud" en objetos que solo tienen propiedades numéricas. Esto hace que funciones como $ .each () y _.each () intenten incorrectamente iterar su objeto como una matriz.
He presentado un informe de problemas (realmente una solicitud de solución) con jQuery ( https://github.com/jquery/jquery/issues/2145 ), y hay un problema similar en el rastreador de guiones bajos ( https://github.com/jashkenas/underscore/issues/2081 ).
Actualización: Este es un error confirmado de webkit. Se realizó una revisión el 2015-03-27, pero no hay ninguna indicación sobre qué versión de iOS tendrá la corrección. Consulte https://bugs.webkit.org/show_bug.cgi?id=142792 . Actualmente se sabe que iOS 8.0 - 8.3 están afectados.
Actualización 2: se puede encontrar una solución alternativa para el error de iOS en jQuery 2.1.4+ y 1.11.3+, así como Underscore 1.8.3+. Si está utilizando cualquiera de estas versiones, entonces la biblioteca se comportará correctamente. Sin embargo, aún depende de usted asegurarse de que su propio código no se vea afectado.
Esta pregunta también se puede llamar: "¿Cómo puede un objeto sin longitud tener una longitud?"
Tengo un tipo de problema de zona crepuscular con Safari móvil (visto tanto en iPhones como en iPads con iOS 8). Mi código tiene muchos fallos intermitentes al usar la implementación "cada" de jQuery ( $.each()
) y de _.each()
).
Después de una investigación, descubrí que en todos los casos de falla, each
función trataba a mi objeto como una matriz. Luego intentaría iterarlo como una matriz ( obj[0]
, obj[1]
, etc.) y fallaría.
Tanto jQuery como Underscore usan la propiedad length
para determinar si un argumento es un objeto o una colección de tipo array / array. Por ejemplo, Underscore usa esta prueba:
if (length === +length) { ... this is an array
Mis objetos no tenían un parámetro de longitud, pero estaban activando las instrucciones if
anteriores. Doble validación de que no había length
por:
- Enviar el valor de
obj.length
al servidor para registrar antes de llamar aeach()
(confirmando que lalength
was undefined
) - Al llamar a
delete obj.length
antes de llamar aeach()
(esto no cambió nada).
Finalmente, he podido capturar este comportamiento en el depurador con un iPhone conectado a Safari en una Mac.
La siguiente imagen muestra que $ .isArrayLike piensa que la length
es 7.
Sin embargo, una traza de la consola muestra que la length
undefined
está undefined
, como se esperaba:
En este punto, creo que esto es un error en iOS Safari, especialmente porque es intermitente. Me encantaría saber de otras personas que han visto este problema y quizás hayan encontrado una manera de contrarrestarlo.
Actualizar
Me pidieron que creara un violín de esto, pero desafortunadamente no puedo. Parece que hay un problema de tiempo (que incluso puede diferir entre dispositivos) y no puedo reproducirlo en un violín. Este es el conjunto mínimo de código con el que pude reprochar el problema y requiere un archivo .js externo. Con este código pasa el 100% del tiempo en mi iPhone 6 ejecutando 8.1.2. Si cambio algo (p. Ej., Hacer el JS en línea, eliminar cualquier código JS no relacionado, etc.), el problema desaparece.
Aquí está el código:
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();
El problema es el código de usuario, no el kit web iOS8.
var obj = {
23: ''a'',
24: ''b'',
25: ''c''
}
Las claves de mapa anteriores son números enteros, cuando se deben usar cadenas. No es un mapa válido según el estándar de javascript, por lo que el comportamiento no está definido, y Apple está eligiendo interpretar `obj ''como una matriz de 26 elementos (índices de 0 a 25 inclusive).
Utilice las teclas de cadena y el problema de longitud desaparecerá:
var obj = {
''23'': ''a'',
''24'': ''b'',
''25'': ''c''
}
Esto no es una respuesta, sino más bien un análisis de lo que sucede debajo de las coberturas después de muchas pruebas. Espero que, después de leer esto, alguien en el lado móvil de safari o en la máquina virtual de JavaScript en el lado de iOS pueda echar un vistazo y corregir el problema.
Podemos confirmar que la función _.each () trata a los objetos js {} como matrices [] porque el navegador safari devuelve la propiedad ''length'' de un objeto como un entero. PERO SOLO EN CIERTOS CASOS .
Si usamos un mapa de objetos donde las claves son enteros:
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!!!
Al inspeccionar con el depurador en el navegador Safari de Safari, vemos claramente que obj.length está "indefinido". Sin embargo, paso a la siguiente línea:
var length = obj.length;
A la variable de longitud se le asigna claramente el valor 26, que es un número entero. La parte entera es importante porque el error en underscore.js ocurre en estas dos líneas en el código de 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)
Sin embargo, si tuviéramos que cambiar el objeto en cuestión solo un poco y agregar un par clave-valor donde la clave sea una cadena (y sea el último elemento en el objeto) como:
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''
Más interesante, si cambiamos el objeto de nuevo y un par clave-valor como:
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!
Parece que el error (donde sea que esté sucediendo: Safari / JavaScript VM) mira la clave del último elemento en el objeto y si es un número entero le agrega uno (+1) e informa que es una longitud del objeto. ... aunque obj.hasOwnProperty (''length'') vuelve como falso.
Esto ocurre en:
- Algunos iPads (pero NO TODOS los que tenemos) con iOS versión 8.1.1, 8.1.2, 8.1.3
- Los iPads en los que ocurre, ocurren constantemente ... cada vez
- solo el navegador Safari en iOS
Esto no ocurre en:
- cualquier iPhone que probamos con iOS 8.1.3 (usando tanto Safari como Chrome)
- cualquier iPad con iOS 7.xx (utilizando tanto Safari como Chrome)
- navegador chrome en iOS
- cualquier js fiddles que intentamos crear utilizando los iPads mencionados anteriormente que crearon constantemente el error
Debido a que realmente no podemos demostrarlo con un jsFiddle, hicimos la siguiente mejor cosa y obtuvimos una captura de pantalla de él pasando por el depurador. Publicamos el video en youTube y se puede ver en esta ubicación:
https://www.youtube.com/watch?v=IR3ZzSK0zKU&feature=youtu.be
Como se indicó anteriormente, esto es solo un análisis del problema en más detalle. Esperamos que alguien con más comprensión bajo el capó pueda comentar sobre la situación.
Una solución simple es NO UTILIZAR la función _.each (o el equivalente de jQuery). Podemos confirmar que el uso de la función angular.js forEach soluciona este problema. Sin embargo, utilizamos underscore.js de forma bastante extensa y _.each se usa en casi todos los lugares en que iteramos a través de matrices / colecciones.
Actualizar
Esto se confirmó como un error y ahora hay una solución para este error en WebKit a partir del 2015-03-27:
solución: http://trac.webkit.org/changeset/182058
informe de error original: https://bugs.webkit.org/show_bug.cgi?id=142792
He trabajado un tiempo con algo que podría ser similar o el mismo problema. La compañía para la que trabajo tiene un juego que usa KineticJS 4.3.2. Kinetic hace un uso intensivo de Arrays al agrupar objetos gráficos en un lienzo html5. El problema que ocurrió fue que a veces faltaba el empuje en la matriz o que faltaban las propiedades del objeto que estaba almacenado en la matriz. El problema ocurrió en Safari y cuando se ejecuta desde la pantalla de inicio en iOS8. Por alguna razón, el problema no se produjo cuando se ejecuta en Chrome en iOS. El problema tampoco se produjo con un depurador conectado. Después de muchas pruebas y búsquedas en la red, creemos que se trata de un error de optimización JIT en webkit en iOS. Un colega encontró los siguientes enlaces.
TypeError: se intentó asignar a una propiedad de solo lectura. en la aplicación Angularjs en iOS8 Safari
https://github.com/angular/angular.js/issues/9128
http://tracker.zkoss.org/browse/ZK-2487
Actualmente hemos realizado una solución al eliminar la notación de puntos al acceder a la matriz que no funcionó (.children fue reemplazado por ["children"]). No he podido crear un jsFiddle.
La actualización a la última versión de lodash (3.10.0) solucionó este problema para mí.
Tenga en cuenta que hay un cambio importante en esta versión de _.first
con _.first
vs _.take
.
Para cualquiera que no esté familiarizado con lodash, es una bifurcación de subrayado que hoy en día es (imo) una mejor solución.
Realmente, muchas gracias a @OzSolomon por explicar, describir y resolver este problema.
Si pudiera dar una recompensa a una pregunta, la habría hecho.
Para cualquiera que vea esto y use jQuery, solo un aviso de que ahora se ha corregido en las versiones 2.1.4 y 1.11.3, que específicamente solo contienen una solución al problema anterior:
http://blog.jquery.com/2015/04/28/jquery-1-11-3-and-2-1-4-released-ios-fail-safe-edition/