Underscore.js 源码学习笔记(下)

上接 Underscore.js 源码学习笔记(上)

 

=== 756 行开始 函数部分。

 

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

_.bind = restArguments(function(func, context, args) {
  if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
  var bound = restArguments(function(callArgs) {
    return executeBound(func, bound, context, this, args.concat(callArgs));
  });
  return bound;
});

 _.bind(func, context, args)  就是将 func 的 this 绑定到 context 并且预先传入参数 args (柯里化)

通过  args.concat(callArgs)  实现了柯里化

bound 是绑定 this 后的函数,func 是传入的函数

 if (!(callingContext instanceof boundFunc))  如果 callingContext 不是 boundFunc 的实例 就通过 apply 实现指定函数运行的 this

如果 callingContext 是 boundFunc 的实例,那意味着你可能是这么使用的

function foo() {}
var bindFoo = _.bind(foo, context/*没写定义,随便什么东西*/);
var bindFooInstance = new bindFoo();

此时 bindFoo() 的 this 就是 bindFoo 的一个实例

那么 bindFooInstance 的 this 是应该绑定到 context 还是 bindFoo 的实例还是什么呢?

JavaScript 中 this 一共有四种绑定 默认绑定 < 隐式绑定 < 显示绑定 < new绑定

所以 这里应该优先使用... foo 的实例

思考一下嘛 如果是 ES5 中 new foo.bind(context) 是不是应该先使用 foo 的实例嘛 bound 只是一个中间函数

然后就是判断 foo 是否有返回值 有的话直接返回该值 否则返回 this 也是操作符 new 的规定

 

_.partial = restArguments(function(func, boundArgs) {
  var placeholder = _.partial.placeholder;
  var bound = function() {
    var position = 0, length = boundArgs.length;
    var args = Array(length);
    for (var i = 0; i < length; i++) {
      args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
    }
    while (position < arguments.length) args.push(arguments[position++]);
    return executeBound(func, bound, this, this, args);
  };
  return bound;
});

_.partial.placeholder = _;
// e.g.
function add(a, b) { return a + b; }
var addOne = _.partial(add, 1, _);
addOne(3); // 4

默认占位符是 _ 先给函数指定部分参数 不指定的就用下划线占位 生成一个新的只需要填写剩余参数的函数

 

_.bindAll = restArguments(function(obj, keys) {
  keys = flatten(keys, false, false);
  var index = keys.length;
  if (index < 1) throw new Error('bindAll must be passed function names');
  while (index--) {
    var key = keys[index];
    obj[key] = _.bind(obj[key], obj);
  }
});
// e.g.
var obj = {
  name: 'xiaoming',
  age: '25',
  getName() {
    return this.name;
  },
  getAge() {
    return this.age;
  },
  sayHello() {
    return 'hello, I am ' + this.name + ' and i am ' + this.age + ' years old.';
  }
}
name = 'global name';
_.bindAll(obj, 'getName', 'getAge');
var getName = obj.getName, getAge = obj.getAge, sayHello = obj.sayHello;
getName();  // xiaoming
getAge();   // 25
sayHello(); // hello, I am global name and i am undefined years old.

把一个对象的指定方法绑定到该对象。keys 可以是要绑定的函数数组或函数。

 

_.memoize = function(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    var address = '' + (hasher ? hasher.apply(this, arguments) : key);
    if (!has(cache, address)) cache[address] = func.apply(this, arguments);
    return cache[address];
  };
  memoize.cache = {};
  return memoize;
};

这个函数还是简单实用的,通过缓存一个变量 cache 当传入相同的参数时直接返回上一次的结果即可。

hasher 是入参的哈希函数,来判断多次入参是否相同。如果不传哈希函数的话,默认就用第一个参数判断是否重复。所以如果入参不是只有一个的话,记得传 hasher 函数。

比如在计算斐波那契数列  fib(n) = fib(n - 1) + fib(n - 2) 可以通过记忆化递归防止大量重复计算。

 

_.delay = restArguments(function(func, wait, args) {
  return setTimeout(function() {
    return func.apply(null, args);
  }, wait);
});

封装了一个函数,每次调用时都要等待 wait 毫秒再执行。

 

_.defer = _.partial(_.delay, _, 1);

通过 _.defer 来执行函数 _.defer(log) 可以使函数放到异步调用队列中,防止一些奇怪的错误吧。(确实遇到了一些时候需要  setTimeout(()=>{...}, 0)  来执行函数才有效的情况,但是还不知道怎么总结规律= =)

 

// 在指定时间间隔 wait 内只会被执行一次
// 在某一时间点 函数被执行 那么之后 wait 时间内的调用都不会被立即执行 而是设置一个定时器等到间隔等于 wait 再执行
// 如果在计时器等待的时间又被调用了 那么定时器将执行在等待时间内的最后一次调用
// options 有两个字段可填  { leading: false } 或 { trailing: false }
// { leading: false } 表示调用时不会立即执行 而是等待 wait 毫秒之后执行
// { trailing: false } 表示执行之后的 wait 时间内的调用都忽略掉
// 不要同时设置这两个字段
_.throttle = function(func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  // later 函数是定时器指定执行的函数 context, args 不是设置定时器时指定的 而是执行 later 时决定的
  var later = function() {
    // 如果 options.leading = false 的话就将 previous 设置为 0 作为标记 下一次执行 func 时就不会被立即执行了
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    // 这里判断 !timeout 真的好迷啊...
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var now = _.now();
    // 如果没有上一次调用 或者 之前的调用已经结束 且 leading = false 会设置 previous = 0
    // previous = 0 且 options.leading = false 说明上一次 func 执行完成 此次的 fun 不需要立即执行 等 wait ms 再执行
    if (!previous && options.leading === false) previous = now;
    // 根据当前时间和上一次调用的时间间隔与 wait 比较判断
    var remaining = wait - (now - previous);
    context = this; // 注意每一次调用都会更新 context 和 args 而执行 later 用到的是这两个参数
    args = arguments; // 也就是说设置定时器时对应的参数 不一定是执行对应的参数~
    // remaining <= 0 则证明距离上次调用间隔大于 wait 了 可以被执行
    // 理论上 remaining > wait 不会存在 除非 now < previous 也就是系统时间出错了(被修改了
    if (remaining <= 0 || remaining > wait) {
      // 当设置了 leading 是不会进入这个分支的= =
      // 删除定时器 重置 previous 为当前时间 并执行 func
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    }
    // 否则如果有 timeout 证明隔一段已经设置一段时间后执行 不再设置定时器

    // 间隔小于 wait 而且没有 timeout 的话 就设置一个定时器 到指定时间间隔后再执行
    // 如果 options.trailing = false 则忽略这次调用 因为时间间隔在 timeout 之内
    else if (!timeout && options.trailing !== false) {
      // 设置了 trailing 不会进入这个分支
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
  // 重置 throttled 的状态 同时取消还没有执行的定时器
  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
};
// e.g.
function log(sth) {
  console.log('===> ' + sth + '  ' + new Date().toLocaleTimeString());
}
var tLog = _.throttle(log, 1000);
// === start === 20:29:54
// ===> 1  20:29:54
// ===> 4  20:29:55
var tLog = _.throttle(log, 1000, { leading: false });
// === start === 20:30:15
// ===> 4  20:30:16
var tLog = _.throttle(log, 1000, { trailing: false });
// === start === 20:30:39
// ===> 1  20:30:39
// 不要同时设置 leading 和 trailing ~ 否则永远都不会被执行
// var tLog = _.throttle(log, 1000, { leading: false, trailing: false });

console.log('=== start === ' + new Date().toLocaleTimeString());
tLog(1);
tLog(2);
tLog(3);
tLog(4);

经典的函数来了= =

被称作节流函数 作用是在一定时间范围内只会被调用一次 即使被多次触发

 

_.debounce = function(func, wait, immediate) {
  var timeout, result;

  var later = function(context, args) {
    timeout = null;
    if (args) result = func.apply(context, args);
  };

  var debounced = restArguments(function(args) {
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      var callNow = !timeout;
      // 虽然有 timeout 但是这里的 later 没有传参所以不会执行 func
      // 只是为了标记之后的 wait 时间内都不会再执行函数
      // 如果等待的过程中又被调用 那么就从那个时间点开始再进行 wait 时间的不执行
      timeout = setTimeout(later, wait);
      if (callNow) result = func.apply(this, args);
    } else {
      timeout = _.delay(later, wait, this, args);
    }

    return result;
  });

  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
};

debounce 防抖函数 只有当隔指定时间没有重复调用该函数时才会执行,可应用于输入和页面滑动等情况

可以分成两种情况看 传 immediate 和不传 immediate

不传 immediate 的话 就是调用后设置定时器 wait 秒之后执行 这中间又被调用 那么从调用时刻开始重新计时

传 immediate 表示第一次调用就会被执行 然后标记之后的 wait ms 内不会被执行 这中间又被调用 那么从调用时刻开始重新计时

 

// _.partial(wrapper, func) 是预先给 wrapper 传入参数 func
// 所以 _.wrap(func, wrapper) 就是 返回 wrapper 先传入 func 后返回的函数
_.wrap = function(func, wrapper) {
  return _.partial(wrapper, func);
};
// e.g.
function func(name) {
  return 'hi ' + name;
}
function wrapper(func, ...args) {
  return func(args).toUpperCase();
}
var sayHi = _.wrap(func, wrapper);
sayHi('saber', 'kido'); // HI SABER,KIDO

 

_.compose = function() {
  var args = arguments;
  var start = args.length - 1;
  return function() {
    var i = start;
    // 从最后一个函数开始执行
    var result = args[start].apply(this, arguments);
    // 每一个函数的入参是上一个函数的出参
    while (i--) result = args[i].call(this, result);
    return result;
  };
};
// e.g.
function getName(firstname, lastname) { return firstname + ' ' + lastname; }
function toUpperCase(str) { return str.toUpperCase(); }
function sayHi(str) { return 'Hi ' + str; }
_.compose(sayHi, toUpperCase, getName)('wenruo', 'duan'); // Hi WENRUO DUAN

我记得之前写过这个函数啊= =但是没找到 记忆出错了

就是一个把一堆函数从右到左连起来执行的函数。函数式编程中很重要的函数。

 

_.after = function(times, func) {
  return function() {
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
};
// e.g.
function ajax(url, fn) {
  console.log(`获取 ${url} 资源...`);
  setTimeout(() => {
    console.log(`获取 ${url} 资源完成`);
    fn();
  }, Math.random() * 1000);
}
function finish() {
  console.log('资源全部获取完成 可以进行下一步操作...');
}


var urls = ['urla', 'urlb', 'urlc'];
var finishWithAfter = _.after(urls.length, finish);

for (var i = 0; i < urls.length; i++) {
  ajax(urls[i], finishWithAfter);
}
// 获取 urla 资源...
// 获取 urlb 资源...
// 获取 urlc 资源...
// 获取 urla 资源完成
// 获取 urlc 资源完成
// 获取 urlb 资源完成
// 资源全部获取完成 可以进行下一步操作...

函数调用 times 遍才会被执行

 

_.before = function(times, func) {
  var memo;
  return function() {
    if (--times > 0) {
      memo = func.apply(this, arguments);
    }
    if (times <= 1) func = null;
    return memo;
  };
};

// 调用前 times-1 次执行 之后每一次都返回之前的运行的值
var foo = _.before(3, _.identity);

console.log(foo(1)) // 1
console.log(foo(2)) // 2
console.log(foo(3)) // 2 (第 n 次开始调用不再执行 func 直接返回上一次的结果
console.log(foo(4)) // 2

只有前 times-1 次执行传入的函数 func 后面就直接返回上一次调用的值。

 

_.once = _.partial(_.before, 2);

就是只有一次调用的时候会只执行,后面直接返回之前的值。

使用场景比如……单例模式?

 

_.restArguments = restArguments;

将 restArguments 函数导出。

 

969行===下面是对象相关的函数了

 

// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
  'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];

var collectNonEnumProps = function(obj, keys) {
  var nonEnumIdx = nonEnumerableProps.length;
  var constructor = obj.constructor;
  var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;

  // Constructor is a special case.
  var prop = 'constructor';
  if (has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);

  while (nonEnumIdx--) {
    prop = nonEnumerableProps[nonEnumIdx];
    if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
      keys.push(prop);
    }
  }
};

IE9一下浏览器有bug就是一些属性重写后 不能在 for ... in 中遍历到,所以要单独判断。

 

_.keys = function(obj) {
  if (!_.isObject(obj)) return [];
  if (nativeKeys) return nativeKeys(obj);
  var keys = [];
  for (var key in obj) if (has(obj, key)) keys.push(key);
  // Ahem, IE < 9.
  if (hasEnumBug) collectNonEnumProps(obj, keys);
  return keys;
};

如果ES5的  Object.keys 存在就直接调用,否则通过 for..in 获取所有的属性。

 

_.allKeys = function(obj) {
  if (!_.isObject(obj)) return [];
  var keys = [];
  for (var key in obj) keys.push(key);
  // Ahem, IE < 9.
  if (hasEnumBug) collectNonEnumProps(obj, keys);
  return keys;
};

获取对象的所有属性,包括原型链上的。

 

_.values = function(obj) {
  var keys = _.keys(obj);
  var length = keys.length;
  var values = Array(length);
  for (var i = 0; i < length; i++) {
    values[i] = obj[keys[i]];
  }
  return values;
};

所有对象自有属性的值的集合

 

_.mapObject = function(obj, iteratee, context) {
  iteratee = cb(iteratee, context);
  var keys = _.keys(obj),
      length = keys.length,
      results = {};
  for (var index = 0; index < length; index++) {
    var currentKey = keys[index];
    results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
  }
  return results;
};
// e.g.
var _2camel = str => str.replace(/_(\w)/g, (item, letter) => letter.toUpperCase());
var obj = { first: 'mo_li_xiang_pian', second: 'yong_ren_zi_rao' };
_.mapObject(obj, _2camel); // { first: 'moLiXiangPian', second: 'yongRenZiRao' }

对对象中每一个值执行 iteratee 函数,和 _.map 的区别是它返回的是对象。

 

_.pairs = function(obj) {
  var keys = _.keys(obj);
  var length = keys.length;
  var pairs = Array(length);
  for (var i = 0; i < length; i++) {
    pairs[i] = [keys[i], obj[keys[i]]];
  }
  return pairs;
};

返回一个数组,每一项都是键、值组成的数组。

 

_.invert = function(obj) {
  var result = {};
  var keys = _.keys(obj);
  for (var i = 0, length = keys.length; i < length; i++) {
    result[obj[keys[i]]] = keys[i];
  }
  return result;
};

对象的键值互换,值要变成建,所以确保值是可序列化的。

 

_.functions = _.methods = function(obj) {
  var names = [];
  for (var key in obj) {
    if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort();
};

对象中所有属性值为函数的属性名的集合按照字典序排序后返回。

 

var createAssigner = function(keysFunc, defaults) { // [defaults] {Boolean}
  return function(obj) {
    var length = arguments.length;
    if (defaults) obj = Object(obj); //  把 obj 转成对象
    if (length < 2 || obj == null) return obj;
    for (var index = 1; index < length; index++) {
      var source = arguments[index],
          keys = keysFunc(source), // keysFunc 是获取对象指定的 key 集合的函数
          l = keys.length;
      for (var i = 0; i < l; i++) {
        var key = keys[i];
        // 如果设置 defaults 则只有在在当前对象没有 key 属性的时候 才添加 key 属性
        // 否则就为 obj 添加 key 属性 存在就替换
        if (!defaults || obj[key] === void 0) obj[key] = source[key];
      }
    }
    return obj;
  };
};

_.extend = createAssigner(_.allKeys); 
// _.extend(obj, ...otherObjs)
// 把 otherObjs 上面的所有的属性都添加到 obj 上 相同属性后面会覆盖前面的

_.extendOwn = _.assign = createAssigner(_.keys);
// _.extendOwn(obj, ...otherObjs)
// 把 otherObjs 上面的所有的自有属性都添加到 obj 上 相同属性后面会覆盖前面的

_.defaults = createAssigner(_.allKeys, true);
// _.extend(obj, ...otherObjs)
// 对 otherObjs 上面的所有的属性 如果 obj 不存在相同属性名的话 就添加到 obj 上 相同属性后面被忽略

扩展对象的一些函数。

 

var keyInObj = function(value, key, obj) {
  return key in obj;
};

_.pick = restArguments(function(obj, keys) {
  // 通过 restArguments 传入的参数除了第一个都被合成了一个数组 keys
  var result = {}, iteratee = keys[0];
  if (obj == null) return result;
  if (_.isFunction(iteratee)) {
    // 如果 iteratee (keys[0]) 是一个函数
    // 可以看做是 _.pick(obj, iteratee, context)
    // obj 中符合 iteratee(value, key, obj) 的键值对被返回
    if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]);
    keys = _.allKeys(obj);
  } else {
    // 如果 iteratee (keys[0]) 不是函数
    // 将 keys 数组递归压平 成为一个新数组 keys
    // 对于 obj 中的属性在 keys 中的键值对被返回
    iteratee = keyInObj;
    keys = flatten(keys, false, false);
    obj = Object(obj);
  }
  for (var i = 0, length = keys.length; i < length; i++) {
    var key = keys[i];
    var value = obj[key];
    if (iteratee(value, key, obj)) result[key] = value;
  }
  return result;
});

筛选对象中部分符合条件的属性。

 

_.omit = restArguments(function(obj, keys) {
  var iteratee = keys[0], context;
  if (_.isFunction(iteratee)) {
    iteratee = _.negate(iteratee);
    if (keys.length > 1) context = keys[1];
  } else {
    keys = _.map(flatten(keys, false, false), String);
    iteratee = function(value, key) {
      return !_.contains(keys, key);
    };
  }
  return _.pick(obj, iteratee, context);
});

逻辑同上,相当于反向 pick 了。

 

_.create = function(prototype, props) {
  var result = baseCreate(prototype);
  if (props) _.extendOwn(result, props);
  return result;
};

给定原型和属性创建一个对象。

 

_.clone = function(obj) {
  if (!_.isObject(obj)) return obj;
  return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};

浅克隆一个对象。

 

看到  _.tap 有点没看懂,感觉事情有点不简单……于是向下翻到了 1621 行,看到这有一堆代码……

首先一开始的时候 (42行) 我们看过   _  的定义,_ 是一个函数,_(obj) 返回一个 _ 实例,该实例有一个 _wrapped 属性是传入的 obj 。

 

我们上面的函数都是 _ 的属性,所以 _(obj) 中是没有这些属性的(_.prototype 中的属性才能被获得)

 

// chain 是一个函数 传入一个对象 obj 返回一个下划线的实例,该实例有一个 _wrapped 属性为 obj 同时有 _chain 属性为 true 标记此对象用于链式调用
_.chain = function(obj) {
  var instance = _(obj);
  instance._chain = true;
  return instance;
};

// 返回链式结果 如果当前实例就有 _chain 则将结果包装成链式对象返回 否则就直接返回对象本身
var chainResult = function(instance, obj) {
  return instance._chain ? _(obj).chain() : obj;
};

// 将对象 obj 中的函数添加到 _.prototype
_.mixin = function(obj) {
  // 对于 obj 中每一为函数的属性
  _.each(_.functions(obj), function(name) {
    // 都将该属性赋值给下划线
    var func = _[name] = obj[name];
    // 同时在下划线的原型链上挂这个函数 同时这个函数可以支持链式调用
    _.prototype[name] = function() {
      var args = [this._wrapped];
      push.apply(args, arguments);
      // 将 this._wrapped 添加到 arguments 最前面传入 func
      // 因为 this._wrapped 就是生成的一个下划线实例的原始的值
      // func 运行的 this 是 _ 把 this._wrapped 也就是上一个链式函数的运行结果 传入 func
      // 将 this 和 func 的返回值传入 chainResult 
      // 如果 this 是一个链式对象(有 _chain 属性)就继续返回链式对象
      // 否则直接返回 obj
      return chainResult(this, func.apply(_, args));
    };
  });
  return _;
};

// Add all of the Underscore functions to the wrapper object.
// 将 _ 传入 mixin
// 下划线上每一个函数都会被绑定到 _.prototype 这样这些函数才能被实例访问
_.mixin(_);

// Add all mutator Array functions to the wrapper.
// 把一些数组相关的函数也加到 _.prototype
_.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);
    if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
    return chainResult(this, obj);
  };
});

// Add all accessor Array functions to the wrapper.
_.each(['concat', 'join', 'slice'], function(name) {
  var method = ArrayProto[name];
  _.prototype[name] = function() {
    return chainResult(this, method.apply(this._wrapped, arguments));
  };
});

// 从一个含有链式的 _ 实例中获取本来的值 
_.prototype.value = function() {
  return this._wrapped;
};

在 _.prototype 上添加一个函数,同时支持链式调用。惊叹于其实现的巧妙。

 

现在可以继续看 _.tap 作用就是插入一个链式调用中间,查看中间值。

_.tap = function(obj, interceptor) {
  interceptor(obj);
  return obj;
};
// e.g.
let obj = [1, 2, 3];
let interceptor = (x) => { console.log('中间值是:', x) }
let result = _(obj).chain().map(x => x * x).tap(interceptor).filter(x => x < 5).max().value();
//              [1,2,3]      [1,4,9]        打印中间值         [1,4]            取最大值 4
//  .value() 就是从 _ 实例 这里是 { [Number: 4] _wrapped: 4, _chain: true } 获取本来的数据
console.log(result);  
// 中间值是: [ 1, 4, 9 ]
// 4

通过例子可以感受的更清晰。 接下来_.isMatch 前面看过了,略。

 

// Internal recursive comparison function for `isEqual`.
var eq, deepEq;
eq = function(a, b, aStack, bStack) {
  // Identical objects are equal. `0 === -0`, but they aren't identical.
  // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
  if (a === b) return a !== 0 || 1 / a === 1 / b;
  // `null` or `undefined` only equal to itself (strict comparison).
  if (a == null || b == null) return false;
  // `NaN`s are equivalent, but non-reflexive.
  if (a !== a) return b !== b;
  // Exhaust primitive checks
  var type = typeof a;
  if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
  return deepEq(a, b, aStack, bStack);
};

// Internal recursive comparison function for `isEqual`.
deepEq = function(a, b, aStack, bStack) {
  // Unwrap any wrapped objects.
  if (a instanceof _) a = a._wrapped;
  if (b instanceof _) b = b._wrapped;
  // Compare `[[Class]]` names.
  var className = toString.call(a);
  if (className !== toString.call(b)) return false;
  switch (className) {
    // Strings, numbers, regular expressions, dates, and booleans are compared by value.
    case '[object RegExp]':
    // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
    case '[object String]':
      // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
      // equivalent to `new String("5")`.
      return '' + a === '' + b;
    case '[object Number]':
      // `NaN`s are equivalent, but non-reflexive.
      // Object(NaN) is equivalent to NaN.
      if (+a !== +a) return +b !== +b;
      // An `egal` comparison is performed for other numeric values.
      return +a === 0 ? 1 / +a === 1 / b : +a === +b;
    case '[object Date]':
    case '[object Boolean]':
      // Coerce dates and booleans to numeric primitive values. Dates are compared by their
      // millisecond representations. Note that invalid dates with millisecond representations
      // of `NaN` are not equivalent.
      return +a === +b;
    case '[object Symbol]':
      return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
  }

  var areArrays = className === '[object Array]';
  if (!areArrays) {
    // 如果不是数组也不是对象的话 其他情况都已经比较完了 所以一定是 false
    if (typeof a != 'object' || typeof b != 'object') return false;

    // Objects with different constructors are not equivalent, but `Object`s or `Array`s
    // from different frames are.
    // 如果都是自定义类型的实例 都有 constructor 的话 那么构造函数一定要相等
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
                             _.isFunction(bCtor) && bCtor instanceof bCtor)
                        && ('constructor' in a && 'constructor' in b)) {
      return false;
    }
  }
  // Assume equality for cyclic structures. The algorithm for detecting cyclic
  // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.

  // Initializing stack of traversed objects.
  // It's done here since we only need them for objects and arrays comparison.
  // 比较 stack 是为了防止对象的一个属性是对象本身这种情况
  // let obj = {}; obj.prop = obj;
  // 这种情况下比较对象再比较对象的每一个属性 就会发生死循环
  // 所以比较到每一个属性的时候都要判断和之前的对象有没有相等的
  // 如果相等的话 就判断另一个对象是不是也这样 来判断两个对象是否相等
  // 而不需要继续比较下去了~ 是不是很巧妙~
  aStack = aStack || [];
  bStack = bStack || [];
  var length = aStack.length;
  while (length--) {
    // Linear search. Performance is inversely proportional to the number of
    // unique nested structures.
    if (aStack[length] === a) return bStack[length] === b;
  }

  // Add the first object to the stack of traversed objects.
  aStack.push(a);
  bStack.push(b);

  // Recursively compare objects and arrays.
  if (areArrays) {
    // 如果是数组的话 需要比较其每一项都相等
    // Compare array lengths to determine if a deep comparison is necessary.
    length = a.length;
    if (length !== b.length) return false;
    // Deep compare the contents, ignoring non-numeric properties.
    while (length--) {
      if (!eq(a[length], b[length], aStack, bStack)) return false;
    }
  } else {
    // 如果是对象的话 需要比较其每一个键都相等 对应的值再深度比较
    // Deep compare objects.
    var keys = _.keys(a), key;
    length = keys.length;
    // Ensure that both objects contain the same number of properties before comparing deep equality.
    if (_.keys(b).length !== length) return false;
    while (length--) {
      // Deep compare each member
      key = keys[length];
      if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
    }
  }
  // Remove the first object from the stack of traversed objects.
  // 讨论一个为什么要出栈 这个有点像 dfs 哈
  // obj = { a: { a1: ... }, b: { b1: ... } }
  // 判断属性 a 的时候栈里是 [obj] 然后判断 a != obj
  // 接下来会递归判断 a1 以及其下属性
  // 到 a1 的时候 栈中元素为 [obj, a]
  // 当属性 a 被判断完全相等后 需要继续比较 b 属性
  // 当比较到 b 的时候 栈中应该是 [obj] 而不是 [obj, a]
  // a == b 不会造成死循环 我们不需要对不是父子(或祖先)关系的属性进行比较
  // 综上 这里需要出栈(大概没讲明白...反正我明白了...
  aStack.pop();
  bStack.pop();
  return true;
};

// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
  return eq(a, b);
};

深度比较两个对象是否相等。我已经开始偷懒了,英文有注释的地方不想翻译成中文了。

虽然很长,但是真的,考虑的很全面。

 

_.isEmpty = function(obj) {
  if (obj == null) return true;
  if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
  return _.keys(obj).length === 0;
};

判断一个值是否为空。为 null、undefined、长度为空的(类)数组、空字符串、没有自己可枚举属性的对象。

 

_.isElement = function(obj) {
  return !!(obj && obj.nodeType === 1);
};

判断一个值是否是 DOM 元素。

nodeType 属性返回节点类型。

如果节点是一个元素节点,nodeType 属性返回 1。

如果节点是属性节点, nodeType 属性返回 2。

如果节点是一个文本节点,nodeType 属性返回 3。

如果节点是一个注释节点,nodeType 属性返回 8。

该属性是只读的。

 

_.isArray = nativeIsArray || function(obj) {
  return toString.call(obj) === '[object Array]';
};

// Is a given variable an object?
_.isObject = function(obj) {
  var type = typeof obj;
  return type === 'function' || type === 'object' && !!obj;
};

isArray 判断一个值是否是数组 

isObject 判断对象是否是 object 或 function 注意判断 null

 

_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) {
  _['is' + name] = function(obj) {
    return toString.call(obj) === '[object ' + name + ']';
  };
});

批量增加一些判断类型的函数,逻辑和 isArray 一样呀。Map WeakMap Set WeakSet 都是 ES6 新增的数据类型。WeakSet 和 WeakMap 都没听过。该补习一波了~~~

 

if (!_.isArguments(arguments)) {
  _.isArguments = function(obj) {
    return has(obj, 'callee');
  };
}

一开始看到的,这个文件就是一个大的IIFE所以会有 arguments ,在 IE 低版本有 bug 不能通过

Object.prototype.toString.apply(arguments) === '[object Arguments]'

来判断。callee 是 arguments 对象的一个属性。可以通过该属性来判断。

都 8102 年了 放过 IE 不好吗?Edge 都开始使用 Chromium 内核了~~~~

 

// Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
// IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
var nodelist = root.document && root.document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
  _.isFunction = function(obj) {
    return typeof obj == 'function' || false;
  };
}

优化 isFunction 因为在一些平台会出现bug 看了下提到的 issue #1621 (https://github.com/jashkenas/underscore/issues/1621)也不是很明白……

反正我试了下 nodejs v8 和最新版 Chrome 都进入了这个分支……emmm不管了……

 

// Is a given object a finite number?
_.isFinite = function(obj) {
  return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj));
};

// Is the given value `NaN`?
_.isNaN = function(obj) {
  return _.isNumber(obj) && isNaN(obj);
};

// Is a given value a boolean?
_.isBoolean = function(obj) {
  return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};

// Is a given value equal to null?
_.isNull = function(obj) {
  return obj === null;
};

// Is a given variable undefined?
_.isUndefined = function(obj) {
  return obj === void 0;
};

emmm 显而易见了吧

 

_.has = function(obj, path) {
  if (!_.isArray(path)) {
    return has(obj, path);
  }
  var length = path.length;
  for (var i = 0; i < length; i++) {
    var key = path[i];
    if (obj == null || !hasOwnProperty.call(obj, key)) {
      return false;
    }
    obj = obj[key];
  }
  return !!length;
};
// e.g.
let obj = { a: { b: { c: 1 } } };
_.has(obj, ['a', 'b', 'c']); // true
_.has(obj, ['a', 'b', 'd']); // false
_.has(obj, []); // false

判断一个对象是否有指定属性,如果是数组则判断嵌套属性。空数组返回 false。和前面 deepGet 不同的是这里有 hasOwnProperty 判断是否是自有属性。

 

=== 1390 行 下面是 Utility Functions 一些工具方法 胜利在望✌️

 

_.noConflict = function() {
  root._ = previousUnderscore;
  return this;
};

如果运行在浏览器等环境 不能直接导出变量 只能将 _ 赋值到全局变量 如果之前已经有变量叫做 _ 可以通过  var underscore = _.noConflict();  获得_工具函数同时将 _ 赋值回原来的值。

 

_.identity = function(value) {
  return value;
};

是一个传入什么就返回什么的函数。看起来好像没什么用,但是前面有用到哒,可以作为 map 等函数的默认 iteratee 

var a = [null, null, [1,2,3], null, [10, 12], null];
a.filter(_.identity)

参考 Stack Overflow 上面的一个找到的 >_<

 

_.constant = function(value) {
  return function() {
    return value;
  };
};
// e.g.
// api: image.fill( function(x, y) { return color })
image.fill( _.constant( black ) );

代码不难 同样让人困惑的是用途,在 Stack Overflow 找到一个用法举例。

 

_.noop = function(){};

返回一个空函数。可以用在需要填写函数但又不需要做任何操作的地方。

 

_.propertyOf = function(obj) {
  if (obj == null) {
    return function(){};
  }
  return function(path) {
    return !_.isArray(path) ? obj[path] : deepGet(obj, path);
  };
};

_.propertyOf 返回获取指定对象属性的方法。

 

_.times = function(n, iteratee, context) {
  var accum = Array(Math.max(0, n)); // n 不能小于 0
  iteratee = optimizeCb(iteratee, context, 1);
  for (var i = 0; i < n; i++) accum[i] = iteratee(i);
  return accum;
};
// e.g.
_.times(6, i => i * i); // [ 0, 1, 4, 9, 16, 25 ]
_.times(6, _.identity); // [ 0, 1, 2, 3, 4, 5 ]

运行一个函数 n 次来生成一个数组。每一次参数都是运行的次数,从 0 开始。

 

_.now = Date.now || function() {
  return new Date().getTime();
};

Date.now 是 ES5(还是6)新增的,旧版本没有,通过new Date().getTime()获得

 

// 一些 HTML 的转义字符
var escapeMap = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#x27;',
  '`': '&#x60;'
};
var unescapeMap = _.invert(escapeMap);

// Functions for escaping and unescaping strings to/from HTML interpolation.
var createEscaper = function(map) {
  // 以传入 escapeMap 举例
  var escaper = function(match) {
    // 返回对应的转义后的字符串
    return map[match];
  };
  // 生成一个正则表达式用来匹配所有的需要转义的字符 (?:&|<|>|"|'|`)
  // 正则表达式有两种创建方式 通过 /.../ 字面量直接创建 或者通过 new RegExp(regStr) 创建
  // 这里的 ?: 表示正则表达不捕获分组 如果不添加这个的话 在 replace 中可使用 $i 代替捕获的分组
  // 比如
  // '2015-12-25'.replace(/(\d{4})-(\d{2})-(\d{2})/g,'$2/$3/$1'); --> "12/25/2015"
  // '2015-12-25'.replace(/(?:\d{4})-(\d{2})-(\d{2})/g,'$2/$3/$1'); -->  "25/$3/12"
  // 为了防止 $1 变成捕获的字符串这里使用了 ?: (其实好像也用不到吧= =
  var source = '(?:' + _.keys(map).join('|') + ')';
  var testRegexp = RegExp(source); // 生成的正则表达式 /(?:&|<|>|"|'|`)/
  var replaceRegexp = RegExp(source, 'g'); // 生成的正则表达式 /(?:&|<|>|"|'|`)/g
  return function(string) {
    string = string == null ? '' : '' + string;
    return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
  };
};
_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);
// e.g.
_.escape('<html></html>') // &lt;html&gt;&lt;/html&gt;
_.unescape('&lt;html&gt;&lt;/html&gt;') // <html></html>

html实体字符的一些转义和反转义。

 

_.result = function(obj, path, fallback) {
  if (!_.isArray(path)) path = [path];
  var length = path.length;
  if (!length) {
    return _.isFunction(fallback) ? fallback.call(obj) : fallback;
  }
  for (var i = 0; i < length; i++) {
    var prop = obj == null ? void 0 : obj[path[i]];
    if (prop === void 0) {
      prop = fallback;
      i = length; // Ensure we don't continue iterating.
    }
    obj = _.isFunction(prop) ? prop.call(obj) : prop;
  }
  return obj;
};
// e.g.
_.result({ a: { b: 2 } }, ['a','d'], () => 'failed');   // failed
_.result({ a: { b: 2 } }, ['a','b'], () => 'failed');   // 2
_.result({ a: () => ({ b: 2 }) }, ['a','b'], 'failed'); // 2
_.result({ a: () => ({ b: 2 }) }, ['a','d'], 'failed'); // failed

又是一个看得莫名其妙的函数...

根据 path 获取 obj 的属性值,当获取不到时就返回 fallback 的执行结果。当遇到属性为函数时就把 上一层对象作为 this 传入执行函数然后继续向下查找。

 

var idCounter = 0;
_.uniqueId = function(prefix) {
  var id = ++idCounter + '';
  return prefix ? prefix + id : id;
};
// e.g.
_.uniqueId('DWR'); // DWR1
_.uniqueId('DWR'); // DWR2
_.uniqueId('XIA');   // XIA3

就是通过闭包 返回一个不断递增的 id

 

_.template 我觉得值得用单独一篇博客来讲 = = 但其实我都是胡诌的!

首先要理解一下这个函数的用法

学过 jsp 的同学应该知道 jsp 中表达式可以写在 <%= %> 之间 而脚本可以写在 <% %> 在渲染的时候 会将脚本执行 表达式也会替换成实际值

这里的用法和那个基本一样

let template = `
<lable>用户ID:</lable><span><%= userId %></span>
<lable>用户名:</lable><span><%= username %></span>
<lable>用户密码:</lable><span><%- password %></span>
<% 
if (userId === 1) { console.log('管理员登录...') }
else { console.log('普通用户登录...') } 
%>
`

let render = _.template(template);

render({userId: 1, username: '管理员', password: '<pwd>'});
/* render 返回:
<lable>用户ID:</lable><span>1</span>
<lable>用户名:</lable><span>管理员</span>
<lable>用户密码:</lable><span>&lt;pwd&gt;</span>
*/
// 同时控制台打印: 管理员登录...

前端三门语言中 只有 JavaScript 是图灵完备语言,你以为你写的模板是 html 添加了一些数据、逻辑,实际上 html 并不能处理这些代码

所以我们需要使用 JS 来处理它。处理后在生成对应的 HTML

把模板先生成一个 render 函数 然后为函数传入数据 就能生成对应 html 了。

除了上面的基础用法 我们可以自定义模板的语法 注意 key 要和 underscore 中定义的相等

默认是这样的

_.templateSettings = {
evaluate: /<%([\s\S]+?)%>/g, // <% %> js脚本
interpolate: /<%=([\s\S]+?)%>/g, // <%= %> 表达式
escape: /<%-([\s\S]+?)%>/g // <%- %> 表达式 生成后对 html 字符进行转义 如 < 转义为 &lt; 防止 XSS 攻击
};

我们可以自定义

let settings = { interpolate: /{{([\s\S]+?)}}/ }

现在 Vue 不是很火嘛 用一下 Vue 的语法

let template = `
<div>欢迎{{ data }}登录</div>
`;
let render = _.template(template, { interpolate: /{{([\s\S]+?)}}/, variable: 'data' });
render('OvO'); // <div>欢迎OvO登录</div>

variable 指定了作用域 不指定时传入 render 的参数为 obj 的话 那么插值中 prop 获取到是 obj.prop 的值

variable 指定传入 render 函数参数的名字


理解了用法 现在思考怎样实现 如果让你写程序传入一段 js 代码输出运行结果 你会怎么办

憋说写一个解释器 >_<

大概就两种选择 eval() 和 new Function() (原谅我学艺不精 还有其他办法吗?)而 eval 只能运行一次 function 是生成一个函数 可以运行多次

生成的 render 有一个参数 source 是生成的函数字符串

这样我们可以达到预编译的效果 就像 vue 打包后的文件里面是没有 template 的 都是编译好的 render 函数

为什么要预编译?我们应该不想每一次运行都 new Function 吧 这个效率低大家应该都知道。其次,动态生成的函数,debug 不方便。

我们传入字符串 但这个字符串中不只有 js 代码还有些不相关的字符串。所以需要使用正则表达式将其中的 js 代码找出来,templateSettings 定义的就是这个正则表达式

如果是表达式就把运行结果和前后的字符串连接起来 如果是脚本就执行

具体看代码就好了

// \s  匹配一个空白字符,包括空格、制表符、换页符和换行符。
// \S  匹配一个非空白字符。
// 所以 \s\S 就是匹配所有字符 和 . 比起来它多匹配了换行
_.templateSettings = {
  evaluate: /<%([\s\S]+?)%>/g,      // <%  %>
  interpolate: /<%=([\s\S]+?)%>/g,  // <%= %>
  escape: /<%-([\s\S]+?)%>/g        // <%- %>
};

// 这是一个一定不会匹配的正则表达式
var noMatch = /(.)^/;

// 因为后面要拼接一个函数体 有些字符放到字符串需要被转义 这里定义了需要转义的字符
// \u2028 和 \u2029 不知道是啥 不想查了= =
var escapes = {
  "'": "'",
  '\\': '\\',
  '\r': 'r',
  '\n': 'n',
  '\u2028': 'u2028',
  '\u2029': 'u2029'
};

var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;

var escapeChar = function(match) {
  return '\\' + escapes[match];
};

_.template = function(text, settings, oldSettings) { // oldSettings 为了向下兼容 可以无视
  if (!settings && oldSettings) settings = oldSettings;
  // 可以传入 settings 要和 _.templateSettings 中属性名相同来覆盖 templateSettings
  settings = _.defaults({}, settings, _.templateSettings);

  // reg.source 返回正则表达式两个斜杠之间的字符串 /\d+/g --> "\d+"
  // matcher 就是把三个正则连起来 /<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
  // 加了一个 $ 表示匹配字符串结尾
  var matcher = RegExp([
    (settings.escape || noMatch).source,
    (settings.interpolate || noMatch).source,
    (settings.evaluate || noMatch).source
  ].join('|') + '|$', 'g');

  var index = 0;
  var source = "__p+='";
  // 假设传入的 text 是 '<p><%=x+1%></p>'
  text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
    // 函数的参数分别是:
    // 匹配的字符串
    // 匹配的分组(有三个括号,所以有三个分组,分别表示 escape, interpolate, evaluate 匹配的表达式)
    // 匹配字符串的下标
    // 第一次匹配: "<p><%=x+1%></p>" 会和 interpolate: /<%=([\s\S]+?)%>/g 匹配 interpolate 的值为 "x+1"
    // index = 0, offset 匹配的起始下标 就是截取字符串最前面未匹配的那一段
    // text.slice(index, offset) 就是 "<p>" 此时的 source 就是 "__p+='<p>"
    // replace(escapeRegExp, escapeChar) 的作用是:
    // source 拼接的是一个 '' 包裹的字符串 有些字符放到 ' ' 里需要被转义

    // 第二次匹配:匹配字符串("<p><%=x+1%></p>")结尾
    // text.slice(index, offset) 此时获取的是 "</p>"
    // 拼接后 source 为 "__p+='<p>'+\n((__t=(x+1))==null?'':__t)+\n'</p>"
    source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
    index = offset + match.length; // 匹配的起始下标+匹配字符串长度 就是匹配字符串末尾的下标

    if (escape) {
      // ((__t = (_.escape(escape))) == null ? '' : __t)
      // _.escape 是将生成的表达式中的 html 字符进行转义
      source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
    } else if (interpolate) {
      // ((__t = (interpolate)) == null ? '' : __t)
      source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
    } else if (evaluate) {
      // 前面的字符串加分号 同时执行该脚本
      source += "';\n" + evaluate + "\n__p+='";
    }
    // 第一次匹配后 interpolate 为 "x+1"
    // 此时 source 是 "__p+='<p>'+\n((__t=(x+1))==null?'':__t)+\n'"
    // 第二次匹配 escape、interpolate、evaluate 都不存在 不会改变 source

    // Adobe VMs need the match returned to produce the correct offset.
    // 返回 match 只是为了获取正确的 offset 而替换后的 text 并没有改变
    return match;
  });
  source += "';\n";

  // 如果没有指定 settings.variable 就添加 with 指定作用域
  // 添加 with 之后 source 为 "with(obj||{}){\n__p+='<p>'+\n((__t=(x+1))==null?'':__t)+\n'</p>\';\n}\n"
  if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

  source = "var __t,__p='',__j=Array.prototype.join," +
    "print=function(){__p+=__j.call(arguments,'');};\n" +
    source + 'return __p;\n';
  // 最后生成的 source 为
  // "var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
  // with(obj||{}){
  // __p+='<p>'+
  // ((__t=(x+1))==null?'':__t)+
  // '</p>';\n}\nreturn __p;
  // "

  var render;
  try {
    // 传入的参数1: settings.variable || obj
    // 传入的参数2: _ 使用于可以在插值中使用 _ 里的函数
    // 函数体 source
    render = new Function(settings.variable || 'obj', '_', source);
    /* 生成函数 render
      function anonymous(obj, _) {
        var __t, __p = '',
          __j = Array.prototype.join,
          print = function() {
            __p += __j.call(arguments, '');
          };
        with(obj || {}) {
          __p += '<p>' +
            ((__t = (x + 1)) == null ? '' : __t) +
            '</p>';
        }
        return __p;
      }
    */
  } catch (e) {
    e.source = source;
    throw e;
  }

  var template = function(data) {
    return render.call(this, data, _);
  };

  // Provide the compiled source as a convenience for precompilation.
  var argument = settings.variable || 'obj';
  template.source = 'function(' + argument + '){\n' + source + '}';

  return template;
};

var template = _.template("<p><%=x+1%></p>");
template({x: 'void'}) // <p>void1</p>

尽管我看的一知半解,但是还是感觉学到了好多。

 

再下面就是 OOP 的部分上面已经基本分析过了

_.prototype.value = function() {
  return this._wrapped;
};

_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

_.prototype.toString = function() {
  return String(this._wrapped);
};

重写下划线的实例的 valueOf 、 toJSON 和 toString 函数

 

if (typeof define == 'function' && define.amd) {
  define('underscore', [], function() {
    return _;
  });
}

AMD(异步模块定义,Asynchronous Module Definition),这里为了兼容 amd 规范。

 

 

到此就把 下划线 1693 行全部看完了。

其实这是我第二遍看,到这次才能说勉强看懂,第一次真的是一头雾水。这期间看了点函数式编程的文章,也许有点帮助吧。

也开始理解了大家为什么说新手想阅读源码的话推荐这个,因为短、耦合度低、而且涉及到很多基础知识。

整体看下来,executeBound、OOP部分 和 _.template 这三部分花了很长时间思考。当然抽丝剥茧后搞懂明白的感觉,真的很爽呀哈哈哈哈哈

 

总之,完结撒花吧~

posted @ 2018-12-22 15:33  我不吃饼干呀  阅读(393)  评论(0编辑  收藏  举报