Начну с того, что метод jQuery.each я всегда использую только для работы с jQuery object. В остальном — циклы for / while или свой аналог «each». Вроде это и логично, но в документации говорится о том, что в��шеупомянутый метод можно использовать для любых коллекций.
Разрушим этот миф.
Давайте рассмотрим логику метода jQuery.each. На входе проверяется тип первого параметра, точнее, объект это или нет:
Получается, если у него нет свойства length, то это Object. А если length есть — то это Array.
Не очень универсальный подход. Давайте проверим.
Создадим объект «Obj» с несколькими свойствами, в число которых запишем и length.
А теперь отдадим его jQuery.each:
Результат выполнения:
Провал. jQuery определил его как массив, и использовал фиктивное свойство length для цикла for,
Но как же обойти эту проблему? У меня было несколько идей. Сейчас мы с вами постепенно по ним пройдемся, и дойдем до самой подходящей и универсальной реализации.
Этот способ нам не подходит, т.к. и typeof и объекта и массива — «object». Более того, если нам нужно различать еще и строки и числа, то мы снова получим «object» в случае их создания с помощью оператора new.
Вот тут я ошибался. Думал, дело в шляпе, подходящий способ найден.
Но избежать очередных подводных камней не удалось:
С объектом и массивом проблем нет. А вот с числами и строками instanceof корректно работает только при условии их создания через оператор new. Идем дальше.
Вот он, работающий способ. Сравнение конструкторов.
Для полной картины, таблица сравнения результатов:
Возможно, метод each должен выглядеть примерно так (кусок с args я не стал брать для демонстрации):
Всем спасибо за внимание.
upd.
Спасибо за полезные комментарии. Цель статьи, в первую очередь, показать способ дифференцирования типов данных в JavaScript, а не выявить баг в jQuery.
Соглашусь, что способ с использованием «toString» более подходящий. В скором времени внесу правки в статью. Еще раз спасибо за отклики. Для меня эта тема интересна, соответственно я тоже хочу разобраться, как лучше.
Разрушим этот миф.
Логика jQuery
Давайте рассмотрим логику метода jQuery.each. На входе проверяется тип первого параметра, точнее, объект это или нет:
length = obj.length, isObj = length === undefined || jQuery.isFunction( obj );
Получается, если у него нет свойства length, то это Object. А если length есть — то это Array.
Не очень универсальный подход. Давайте проверим.
Создадим объект «Obj» с несколькими свойствами, в число которых запишем и length.
var Obj = { name: "test", size: 2, length: 5 };
А теперь отдадим его jQuery.each:
$.each( Obj, function() { console.log( this, arguments ); } );
Результат выполнения:
Window [0, undefined] Window [1, undefined] Window [2, undefined] Window [3, undefined] Window [4, undefined]
Провал. jQuery определил его как массив, и использовал фиктивное свойство length для цикла for,
Но как же обойти эту проблему? У меня было несколько идей. Сейчас мы с вами постепенно по ним пройдемся, и дойдем до самой подходящей и универсальной реализации.
Способ первый: typeof
Этот способ нам не подходит, т.к. и typeof и объекта и массива — «object». Более того, если нам нужно различать еще и строки и числа, то мы снова получим «object» в случае их создания с помощью оператора new.
Способ второй: instanceof
Вот тут я ошибался. Думал, дело в шляпе, подходящий способ найден.
Но избежать очередных подводных камней не удалось:
var a = {}; a instanceof Object; => true var b = []; b instanceof Array; => true var c = 5; c instanceof Number; => false var d = ""; d instanceof String; => false
С объектом и массивом проблем нет. А вот с числами и строками instanceof корректно работает только при условии их создания через оператор new. Идем дальше.
Способ третий: constructor
Вот он, работающий способ. Сравнение конструкторов.
var a = {}; a.constructor === Object; => true var b = []; b.constructor === Array; => true var c = 5; c.constructor === Number; => true var d = ""; d.constructor === String; => true
Для полной картины, таблица сравнения результатов:
| Способ | typeof | instanceof | constructor |
|---|---|---|---|
| new Array() | «object» | true | true |
| [] | «object» | true | true |
| new Object() | «object» | true | true |
| {} | «object» | true | true |
| new String() | «object» | true | true |
| "" | «string» | false | true |
| new Number() | «object» | true | true |
| 5 | «number» | false | true |
Вывод:
Возможно, метод each должен выглядеть примерно так (кусок с args я не стал брать для демонстрации):
each: function( obj, callback, args ) { var name, i = 0, length = obj.length, isObj = obj.constructor === Object, isArray = obj.constructor === Array || obj.constructor === String; if ( isObj ) { for ( name in obj ) { if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) { break; } } } else if ( isArray ) { for ( ; i < length; ) { if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) { break; } } } }
Всем спасибо за внимание.
upd.
Спасибо за полезные комментарии. Цель статьи, в первую очередь, показать способ дифференцирования типов данных в JavaScript, а не выявить баг в jQuery.
Соглашусь, что способ с использованием «toString» более подходящий. В скором времени внесу правки в статью. Еще раз спасибо за отклики. Для меня эта тема интересна, соответственно я тоже хочу разобраться, как лучше.
