Pull to refresh

Разбор Underscore

Reading time 10 min
Views 34K
В этом посте любитель 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 и документацией, а потом собрать все назад остается Васе в качестве домашнего задания.


p.s. небольшой холиварчик
Only registered users can participate in poll. Log in, please.
Какая библиотека лучше?
41.44% Underscore.js 254
33.93% Lo-Dash 208
24.63% свой js-велосипед 151
613 users voted. 427 users abstained.
Tags:
Hubs:
+18
Comments 84
Comments Comments 84

Articles