前言

本文接着上篇《underscore 系列之如何写自己的 underscore》,阅读本篇前,希望你已经阅读了上一篇。

jQuery

我们都知道 jQuery 可以链式调用,比如:

  1. $("div").eq(0).css("width", "200px").show();

我们写个简单的 demo 模拟链式调用:

  1. function JQuery(selector) {
  2. this.elements = [];
  3. var nodeLists = document.getElementsByTagName(selector);
  4. for (var i = 0; i < nodeLists.length; i++) {
  5. this.elements.push(nodeLists[i]);
  6. }
  7. return this;
  8. }
  9.  
  10. JQuery.prototype = {
  11. eq: function(num){
  12. this.elements = [this.elements[num]];
  13. return this;
  14. },
  15. css: function(prop, val) {
  16. this.elements.forEach(function(el){
  17. el.style[prop] = val;
  18. })
  19. return this;
  20. },
  21. show: function() {
  22. this.css('display', 'block');
  23. return this;
  24. }
  25.  
  26. }
  27.  
  28. window.$ = function(selector){
  29. return new JQuery(selector)
  30. }
  31.  
  32. $("div").eq(0).css("width", "200px").show();

jQuery 之所以能实现链式调用,关键就在于通过 return this,返回调用对象。再精简下 demo 就是:

  1. var jQuery = {
  2. eq: function(){
  3. console.log('调用 eq 方法');
  4. return this;
  5. },
  6. show: function(){
  7. console.log('调用 show 方法');
  8. return this;
  9. }
  10. }
  11.  
  12. jQuery.eq().show();

_.chain

在 underscore 中,默认不使用链式调用,但是如果你想使用链式调用,你可以通过 _.chain 函数实现:

  1. _.chain([1, 2, 3, 4])
  2. .filter(function(num) { return num % 2 == 0; })
  3. .map(function(num) { return num * num })
  4. .value(); // [4, 16]

我们看看 _.chain 这个方法都做了什么:

  1. _.chain = function (obj) {
  2. var instance = _(obj);
  3. instance._chain = true;
  4. return instance;
  5. };

我们以 [1, 2, 3] 为例,_.chain([1, 2, 3]) 会返回一个对象:

  1. {
  2. _chain: true,
  3. _wrapped: [1, 2, 3]
  4. }

该对象的原型上有着 underscore 的各种方法,我们可以直接调用这些方法。

但是问题在于原型上的这些方法并没有像 jQuery 一样,返回 this ,所以如果你调用了一次方法,就无法接着调用其他方法了……

但是试想下,我们将函数的返回值作为参数再传入 _.chain 函数中,不就可以接着调用其他方法了?

写一个精简的 Demo:

  1. var _ = function(obj) {
  2. if (!(this instanceof _)) return new _(obj);
  3. this._wrapped = obj;
  4. };
  5.  
  6. _.chain = function (obj) {
  7. var instance = _(obj);
  8. instance._chain = true;
  9. return instance;
  10. };
  11.  
  12. _.prototype.push = function(num) {
  13. this._wrapped.push(num);
  14. return this._wrapped
  15. }
  16.  
  17. _.prototype.shift = function(num) {
  18. this._wrapped.shift()
  19. return this._wrapped
  20. }
  21.  
  22. var res = _.chain([1, 2, 3]).push(4);
  23. // 将上一个函数的返回值,传入 _.chain,然后再继续调用其他函数
  24. var res2 = _.chain(res).shift();
  25.  
  26. console.log(res2); // [2, 3, 4]

然而这也太复杂了吧,难道 chain 这个过程不能是自动化的吗?如果我是开发者,我肯定希望直接写成:

  1. _.chain([1, 2, 3]).push(4).shift()

所以我们再优化一下实现方式:

  1. var _ = function(obj) {
  2. if (!(this instanceof _)) return new _(obj);
  3. this._wrapped = obj;
  4. };
  5.  
  6. var chainResult = function (instance, obj) {
  7. return instance._chain ? _.chain(obj) : obj;
  8. };
  9.  
  10. _.chain = function (obj) {
  11. var instance = _(obj);
  12. instance._chain = true;
  13. return instance;
  14. };
  15.  
  16. _.prototype.push = function(num) {
  17. this._wrapped.push(num);
  18. return chainResult(this, this._wrapped)
  19. }
  20.  
  21. _.prototype.shift = function() {
  22. this._wrapped.shift();
  23. return chainResult(this, this._wrapped)
  24. }
  25.  
  26. var res = _.chain([1, 2, 3]).push(4).shift();
  27.  
  28. console.log(res._wrapped);

我们在每个函数中,都用 chainResult 将函数的返回值包裹一遍,再生成一个类似以下这种形式的对象:

  1. {
  2. _wrapped: some value,
  3. _chain: true
  4. }

该对象的原型上有各种函数,而这些函数的返回值作为参数传入了 chainResult,该函数又会返回这样一个对象,函数的返回值就保存在 _wrapped 中,这样就实现了链式调用。

_.chain链式调用原理就是这样,可是这样的话,我们需要对每个函数都进行修改呀……

幸运的是,在 underscore 中,所有的函数是挂载到 函数对象中的,.prototype 上的函数是通过 .mixin 函数将 函数对象中的所有函数复制到 _.prototype 中的。

所以为了实现链式调用,我们还需要对上一篇《underscore 系列之如何写自己的 underscore》 中的 _.mixin 方法进行一定修改:

  1. // 修改前
  2. var ArrayProto = Array.prototype;
  3. var push = ArrayProto.push;
  4.  
  5. _.mixin = function(obj) {
  6. _.each(_.functions(obj), function(name) {
  7. var func = _[name] = obj[name];
  8. _.prototype[name] = function() {
  9. var args = [this._wrapped];
  10. push.apply(args, arguments);
  11. return func.apply(_, args);
  12. };
  13. });
  14. return _;
  15. };
  16.  
  17. _.mixin(_);
  1. // 修改后
  2. var ArrayProto = Array.prototype;
  3. var push = ArrayProto.push;
  4.  
  5. var chainResult = function (instance, obj) {
  6. return instance._chain ? _(obj).chain() : obj;
  7. };
  8.  
  9. _.mixin = function(obj) {
  10. _.each(_.functions(obj), function(name) {
  11. var func = _[name] = obj[name];
  12. _.prototype[name] = function() {
  13. var args = [this._wrapped];
  14. push.apply(args, arguments);
  15. return chainResult(this, func.apply(_, args));
  16. };
  17. });
  18. return _;
  19. };
  20.  
  21. _.mixin(_);

_.value

根据上面的分析过程,我们知道如果我们打印:

  1. console.log(_.chain([1, 2, 3]).push(4).shift());

其实会打印一个对象 {_chain: true, _wrapped: [2, 3, 4] }

可是我希望获得值是 [2, 3, 4] 呀!

所以,我们还需要提供一个 value 方法,当执行 value 方法的时候,就返回当前 _wrapped 的值。

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

此时调用方式为:

  1. var arr = _.chain([1, 2, 3]).push(4).shift().value();
  2. console.log(arr) // [2, 3, 4]

最终代码

结合上一篇文章,最终的 underscore 代码组织结构如下:

  1. (function() {
  2.  
  3. var root = (typeof self == 'object' && self.self == self && self) ||
  4. (typeof global == 'object' && global.global == global && global) ||
  5. this || {};
  6.  
  7. var ArrayProto = Array.prototype;
  8.  
  9. var push = ArrayProto.push;
  10.  
  11. var _ = function(obj) {
  12. if (obj instanceof _) return obj;
  13. if (!(this instanceof _)) return new _(obj);
  14. this._wrapped = obj;
  15. };
  16.  
  17. if (typeof exports != 'undefined' && !exports.nodeType) {
  18. if (typeof module != 'undefined' && !module.nodeType && module.exports) {
  19. exports = module.exports = _;
  20. }
  21. exports._ = _;
  22. } else {
  23. root._ = _;
  24. }
  25.  
  26. _.VERSION = '0.2';
  27.  
  28. var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
  29.  
  30. var isArrayLike = function(collection) {
  31. var length = collection.length;
  32. return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
  33. };
  34.  
  35. _.each = function(obj, callback) {
  36. var length, i = 0;
  37.  
  38. if (isArrayLike(obj)) {
  39. length = obj.length;
  40. for (; i < length; i++) {
  41. if (callback.call(obj[i], obj[i], i) === false) {
  42. break;
  43. }
  44. }
  45. } else {
  46. for (i in obj) {
  47. if (callback.call(obj[i], obj[i], i) === false) {
  48. break;
  49. }
  50. }
  51. }
  52.  
  53. return obj;
  54. }
  55.  
  56. _.isFunction = function(obj) {
  57. return typeof obj == 'function' || false;
  58. };
  59.  
  60. _.functions = function(obj) {
  61. var names = [];
  62. for (var key in obj) {
  63. if (_.isFunction(obj[key])) names.push(key);
  64. }
  65. return names.sort();
  66. };
  67.  
  68. /**
  69. * 在 _.mixin(_) 前添加自己定义的方法
  70. */
  71. _.reverse = function(string){
  72. return string.split('').reverse().join('');
  73. }
  74.  
  75. _.chain = function(obj) {
  76. var instance = _(obj);
  77. instance._chain = true;
  78. return instance;
  79. };
  80.  
  81. var chainResult = function(instance, obj) {
  82. return instance._chain ? _(obj).chain() : obj;
  83. };
  84.  
  85. _.mixin = function(obj) {
  86. _.each(_.functions(obj), function(name) {
  87. var func = _[name] = obj[name];
  88. _.prototype[name] = function() {
  89. var args = [this._wrapped];
  90. push.apply(args, arguments);
  91. return chainResult(this, func.apply(_, args));
  92. };
  93. });
  94. return _;
  95. };
  96.  
  97. _.mixin(_);
  98.  
  99. _.prototype.value = function () {
  100. return this._wrapped;
  101. };
  102.  
  103. })()

underscore 系列

underscore 系列目录地址:https://github.com/mqyqingfeng/Blog

underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助大家阅读源码,以及写出自己的 undercore。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。