В этом посте любитель javascript, вооружившись бензопилой, безжалостно распиливает одну из лучших, на его взгляд, универсальных библиотек.
Препарируем вот этот исходник с точки зрения ученика 5-го «Б» Васи Пупкина, прочитавшего книжку по основам javascript, и страстно желающего получше запутаться в полученных знаниях.
Начальная разделка
- создание модуля или 'универсальная заготовка'
код
(function() { // сохраняет глобальный объект (window или exports) для дальнейшей работы var root = this; // сохраняет предыдущий underscore, если тот уже был подключен раньше. var previousUnderscore = root._; // крепит черточку к глобальному объекту if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else { root._ = _; } // поставит в глобал предыдущую черточку и вернет текущую _.noConflict = function() { root._ = previousUnderscore; return this; }; // просто, чтобы было меньше глюков при работе с модулем if (typeof define === 'function' && define.amd) { define('underscore', [], function() { return _; }); } }.call(this)); // передает внутрь модуля глобальный объект
- начальный _ (нечто, чем можно будет пользоваться так _.функция(что-то) или так _(что-то).функция() )
код
// то что будет использоваться как черточка - объект или как черточка - создатель объекта с кучей функций. var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; };
- создание коротких ссылок
код
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // достаем функции из прототипов стандартных конструкторов var push = ArrayProto.push, slice = ArrayProto.slice, concat = ArrayProto.concat, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // достаем функции из самих объектов var nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind;
- добавка версии
код
_.VERSION = '1.7.0';
- создание функции, которая используется на каждом элементе, внутри других функций, которые перебирают элементы
код
// при необходимости обертывает функцию для использования на конкретном контексте, // то есть this внутри func будет ссылкой на context var createCallback = function(func, context, argCount) { if (context === void 0) return func; // нет контекста - не надо обертывать switch (argCount == null ? 3 : argCount) { // передано ли колличество аргументов для func case 1: return function(value) { // если один аргумент, то создадим новую ф-цию, которая вызовет func на конкретном контексте return func.call(context, value); // и с этим аргументом }; case 2: return function(value, other) { return func.call(context, value, other); }; case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } return function() { return func.apply(context, arguments); // создание самой медленной функции, которая вызовет func на нужном контексте, }; // c любым колличеством аргументов, которые хранятся в массивоподобном объекте arguments }; // создает функцию, которая _.iteratee = function(value, context, argCount) { if (value == null) return _.identity; // просто возвращает переданное значение if (_.isFunction(value)) return createCallback(value, context, argCount); if (_.isObject(value)) return _.matches(value); // проверяет наличие конкретного набора свойств return _.property(value); // проверяет наличие одного названного свойства };
- навешивание полезных функций на черточку, чтобы можно было вызывать вот так _.функция(объект на обработку, еще аргументы)
коддля коллекций
_.each = _.forEach = function(obj, iteratee, context) { }; _.map = _.collect = function(obj, iteratee, context) { }; _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { }; _.reduceRight = _.foldr = function(obj, iteratee, memo, context) { }; _.find = _.detect = function(obj, predicate, context) { }; _.filter = _.select = function(obj, predicate, context) { }; _.reject = function(obj, predicate, context) { }; _.every = _.all = function(obj, predicate, context) { }; _.some = _.any = function(obj, predicate, context) { }; _.contains = _.include = function(obj, target) { }; _.invoke = function(obj, method) { }; _.pluck = function(obj, key) { }; _.where = function(obj, attrs) { }; _.findWhere = function(obj, attrs) { }; _.max = function(obj, iteratee, context) { }; _.min = function(obj, iteratee, context) { }; _.shuffle = function(obj) { }; _.sample = function(obj, n, guard) { }; _.sortBy = function(obj, iteratee, context) { }; // дальше похожие функции, которые делаются за счет добавки переданного уникального функционала к тому, который общий для всех _.groupBy = group(function(result, value, key) { }); _.indexBy = group(function(result, value, key) { }); _.countBy = group(function(result, value, key) { }); _.sortedIndex = function(array, obj, iteratee, context) { }; _.toArray = function(obj) { }; _.size = function(obj) { }; _.partition = function(obj, predicate, context) { };
для массивов// типа коллекций, у которых, вместо названий свойств, номера _.first = _.head = _.take = function(array, n, guard) { }; _.initial = function(array, n, guard) { }; _.last = function(array, n, guard) { }; _.rest = _.tail = _.drop = function(array, n, guard) { }; _.compact = function(array) { }; _.flatten = function(array, shallow) { }; _.without = function(array) { }; _.uniq = _.unique = function(array, isSorted, iteratee, context) { }; _.union = function() { }; _.intersection = function(array) { }; _.difference = function(array) { }; _.zip = function(array) { }; _.object = function(list, values) { }; _.indexOf = function(array, item, isSorted) { }; _.lastIndexOf = function(array, item, from) { }; _.range = function(start, stop, step) { };
для функций// функции для обработки других функций _.bind = function(func, context) { }; _.partial = function(func) { }; _.bindAll = function(obj) { }; _.memoize = function(func, hasher) { }; _.delay = function(func, wait) { }; _.defer = function(func) { }; _.throttle = function(func, wait, options) { }; _.debounce = function(func, wait, immediate) { }; _.wrap = function(func, wrapper) { }; _.negate = function(predicate) { }; _.compose = function() { }; _.after = function(times, func) { }; _.before = function(times, func) { }; _.once = _.partial(_.before, 2); // получается из before, у которого первым аргументом всегда будет 2
для объектов_.keys = function(obj) { }; _.values = function(obj) { }; _.pairs = function(obj) { }; _.invert = function(obj) {}; _.functions = _.methods = function(obj) {}; _.extend = function(obj) { }; _.pick = function(obj, iteratee, context) { }; _.omit = function(obj, iteratee, context) { }; _.defaults = function(obj) { }; _.clone = function(obj) { }; _.tap = function(obj, interceptor) { }; _.isEqual = function(a, b) { }; _.isEmpty = function(obj) { }; _.isElement = function(obj) { }; _.isArray = nativeIsArray || function(obj) { }; // если нет нативной функции - делается своя _.isObject = function(obj) { }; _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { }); // навешиваются функции сгенерированные из названий if (!_.isArguments(arguments)) { _.isArguments = function(obj) { }; // навешивается , если нет уже такой же нормально работающей } if (typeof /./ !== 'function') { _.isFunction = function(obj) { }; } _.isFinite = function(obj) {}; _.isNaN = function(obj) {}; _.isBoolean = function(obj) {}; _.isNull = function(obj) {}; _.isUndefined = function(obj) {}; _.has = function(obj, key) {};
утилиты_.identity = function(value) {}; _.constant = function(value) {}; _.noop = function(){}; _.property = function(key) { }; _.matches = function(attrs) { }; _.times = function(n, iteratee, context) { }; _.random = function(min, max) { }; _.now = Date.now || function() {}; _.escape = createEscaper(escapeMap); _.unescape = createEscaper(unescapeMap); _.result = function(object, property) { }; _.template = function(text, settings, oldSettings) { }; _.chain = function(obj) { };
- обработка функций, чтобы можно было делать так _(объект на обработку).функция(еще аргументы)
код
// проверяет будут ли делать так: _(a).f1().f2().f3() var result = function(obj) { return this._chain ? _(obj).chain() : obj; }; // делает так, чтобы _.mixin = function(obj) { _.each(_.functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { // методы c _ перекочевали на _(х) var args = [this._wrapped]; // будут вызываться методы _, с первым аргументом "х" push.apply(args, arguments); return result.call(this, func.apply(_, args)); // и, если надо, результат опять оборачивается в _(); }; }); }; // собственно, подмешиваются методы _.mixin(_); // тоже, что и миксом, _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; method.apply(obj, arguments); // только this-ом внутри метода будет изначальный объект без обертки. if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; // и. если надо, удаляется первый элемент return result.call(this, obj); }; }); // то же _.each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; // только метод свистнули из прототипа массива _.prototype[name] = function() { return result.call(this, method.apply(this._wrapped, arguments)); }; }); // этим достается сам объект x из вот этого _(х).f1().f2().f3() _.prototype.value = function() { return this._wrapped; };
Выпиливание js-органов
- обработка переменных
код
var root = this // сохранение текущего значения, потому что this штука переменчивая var i, length = obj.length; // раннее создание переменных, которые будут использоваться в двух разных циклах for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj); } for (; i < length; i++) if (array[i] === item) return i; // i заранее присвоили значение slice.call(array, Math.max(array.length - n, 0)); // копия части элементов массива args.concat(slice.call(arguments)) // присоединение к аргсам копии аргументов for (var key in obj) if (_.has(obj, key)) keys.push(key); // перебирает названия свойств объекта obj[prop] = source[prop]; // добавление свойств одного объекта другому
- обработка функций
код
(function() { }.call(this)); // моментальное выполнение, this внутри как снаружи if (!(this instanceof _)) return new _(obj); // когда надо, чтобы функция что-то конструировала var createCallback = function(func, context, argCount) { return function(value) { // когда надо создать другую, внутри которой будет работать наша return func.call(context, value); }; }; _.each = _.forEach = function(obj, iteratee, context) {}; // создание функции и крепление ее на _ _.find = _.detect = function(obj, predicate, context) { predicate = _.iteratee(predicate, context); // подмена пришедшего predicate на созданную из него функцию _.some(); // вызов одной, навешенной на черточку функции, внутри другой }; _.some(obj, function(value, index, list) { }); // создание и передача одной функции как аргумент в другую // создает функцию, var group = function(behavior) { // часть поведения которой передается в аргументах return function(obj, iteratee, context) { // то есть, созданные функции работают одинаково behavior(result, value, key); // только под конец - по разному return result; }; }; _.indexBy = group(function(result, value, key) { // сама генерация result[key] = value; }); // кусок кода, который выполняет часть себя, потом, если надо, запускает свою копию, ждет пока копия выполнится, потом выполняет оставшуюся часть себя var flatten = function(input, shallow, strict, output) { for (var i = 0, length = input.length; i < length; i++) { flatten(value, shallow, strict, output); // подныривает во встреченные вложенные массивы } }; // обертывание одной функции вокруг другой _.flatten = function(array, shallow) { return flatten(array, shallow, false, []); }; // аналог var x = func.bind(obj), означает, что каждый раз, когда делается так: x(), // происходить будет что-то типа obj.func(), то есть this внутри func будет obj nativeBind.apply(func, slice.call(arguments, 1)) // подмена методов объекта на их обертки, которые всегда вызывают оригиналы на этом объекте // то есть нельзя будет вызвать метод объекта и передать ему в качестве this посторонний объект obj[key] = _.bind(obj[key], obj); // создание функции со своим _.memoize = function(func, hasher) { var memoize = function(key) {}; memoize.cache = {}; // личным объектом для запоминания return memoize; }; // задержка выполнения функции setTimeout(function(){ return func.apply(null, args); }, wait); // функция, которая из названия делает другую функцию _.property = function(key) { return function(obj) { return obj[key]; // которая выдергивает из переданного объекта значение названного свойства }; }; // закидывает функцию _.prototype[name] = function() { // с именем name в прототип черточки var args = [this._wrapped]; push.apply(args, arguments); return result.call(this, func.apply(_, args)); }; // после чего будет работать вот это: var x = new _; x[name]();
- проверки
код
typeof exports !== 'undefined' // существования if (length === +length) // является ли числом var keys = obj.length !== +obj.length && _.keys(obj), // если проверка пройдена, тогда вызов _.keys length = (keys || obj).length, // длина из кейса или, если нет, из самого объекта results = Array(length), currentKey; memo = obj[keys ? keys[index++] : index++]; // достаем по названию, если нет, по номеру if (!index) throw new TypeError(reduceError); // нет индекса - вылет с ошибкой if (array == null) return void 0; // несколько проверок для поиска подходящего действия if (n == null || guard) return array[0]; if (n < 0) return []; return slice.call(array, 0, n); if (!(this instanceof bound)) // сделан ли this с помощью new bound _.isBoolean = function(obj) { // является ли объект булевым return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; }; // проверка наличия родного бинда и его равенство тому, который навешен на func if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); hasOwnProperty.call(source, prop) // является ли свойство своим (не прототипа) if (key in obj) result[key] = obj[key]; // проверка наличия свойства в объекте if (obj[prop] === void 0) obj[prop] = source[prop]; // проверка отсутствия свойства // попытка создать функцию из кучи текста try { var render = new Function(settings.variable || 'obj', '_', source); } catch (e) { // перехват ошибки e.source = source; throw e; // и бросание заново с добавкой }
- красивые моменты
код
while (index--) { // быстрый цикл от конца к началу currentKey = keys ? keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } // вызов отдельного метода или метода-свойства value, this - value, аргументы вытряхиваются из args // типа func.apply(x, [1, 2, 3, 4]) похоже на x.func(1, 2, 3, 4), а func.call(x, [1, 2, 3, 4]) - на x.func([1, 2, 3, 4]) (isFunc ? method : value[method]).apply(value, args) var mid = low + high >>> 1; // быстрое деление на 2 // создание и навешивание на _ кучи функций, внутрення реализация которых отличается только задействованным названием _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { _['is' + name] = function(obj) { return toString.call(obj) === '[object ' + name + ']'; }; });
- наборы аргументов
код
// применяет что-то (iteratee) на каждом элементе из obj, this-ом в этом чем-то будет context // obj - коллекция или массив function(obj, iteratee, context) {}; // собственно, что-то что будет применяться на каждом элементе из чего-то типа коллекции, //где акумулятор - хранилище результата, value - элемент из коллекции, index - номер или название элемента function(accumulator, value, index, collection) {}; memo = iteratee(memo, obj[currentKey], currentKey, obj); // predicate вызывается для проверки переданного ему элемента, возвращает true, если проверка пройдена function(obj, predicate, context) {}; // значение, номер/свойство в коллекции, коллекция/массив; function(value, index, list) { } // method либо отдельная функция, либо название ф-ции, которая есть на каждом элементе коллекции function(obj, method) {}; // key - название, которое будет превращено в функцию, которая будет помогать проверять или выдергивать свойство из элемента. function(obj, key) {}; // attrs - типа key, только функция будет проверять наявность всей пачки свойств в переданном элементе function(obj, attrs) { }; // guard говорит функции, что ей дали лишний аргумент function(obj, n, guard) {}; // похоже на предыдущее только первый объект явно массив function(array, n, guard) {}; // скорее всего создает новую функцию, которая внутри использует переданную function(func){}; // скорее всего проверка объекта на что-то или монипуляция со свойствами function(obj){};
Распилить остальное, научиться пользоваться Firebug и документацией, а потом собрать все назад остается Васе в качестве домашнего задания.
Автор: IDriuk