Начну с того, что метод 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» более подходящий. В скором времени внесу правки в статью. Еще раз спасибо за отклики. Для меня эта тема интересна, соответственно я тоже хочу разобраться, как лучше.