Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

underscore 系列之链式调用 #57

Open
mqyqingfeng opened this issue Nov 22, 2017 · 24 comments
Open

underscore 系列之链式调用 #57

mqyqingfeng opened this issue Nov 22, 2017 · 24 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Nov 22, 2017

前言

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

jQuery

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

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

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

function JQuery(selector) {
    this.elements = [];
    var nodeLists = document.getElementsByTagName(selector);
    for (var i = 0; i < nodeLists.length; i++) {
        this.elements.push(nodeLists[i]);
    }
    return this;
}

JQuery.prototype = {
    eq: function(num){
        this.elements = [this.elements[num]];
        return this;
    },
    css: function(prop, val) {
        this.elements.forEach(function(el){
            el.style[prop] = val;
        })
        return this;
    },
    show: function() {
        this.css('display', 'block');
        return this;
    }

}

window.$ = function(selector){
    return new JQuery(selector)
}

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

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

var jQuery = {
    eq: function(){
        console.log('调用 eq 方法');
        return this;
    },
    show: function(){
        console.log('调用 show 方法');
        return this;
    }
}

jQuery.eq().show();

_.chain

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

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

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

_.chain = function (obj) {
    var instance = _(obj);
    instance._chain = true;
    return instance;
};

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

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

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

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

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

写一个精简的 Demo:

var _ = function(obj) {
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
};

_.chain = function (obj) {
    var instance = _(obj);
    instance._chain = true;
    return instance;
};

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

_.prototype.shift = function(num) {
    this._wrapped.shift()
    return this._wrapped
}

var res = _.chain([1, 2, 3]).push(4);
// 将上一个函数的返回值,传入 _.chain,然后再继续调用其他函数
var res2 = _.chain(res).shift();

console.log(res2); // [2, 3, 4]

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

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

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

var _ = function(obj) {
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
};

var chainResult = function (instance, obj) {
    return instance._chain ? _.chain(obj) : obj;
};

_.chain = function (obj) {
    var instance = _(obj);
    instance._chain = true;
    return instance;
};

_.prototype.push = function(num) {
    this._wrapped.push(num);
    return chainResult(this, this._wrapped)
}

_.prototype.shift = function() {
    this._wrapped.shift();
    return chainResult(this, this._wrapped)
}

var res = _.chain([1, 2, 3]).push(4).shift();

console.log(res._wrapped);

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

{
    _wrapped: some value, 
    _chain: true
}

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

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

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

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

// 修改前
var ArrayProto = Array.prototype;
var push = ArrayProto.push;

_.mixin = function(obj) {
    _.each(_.functions(obj), function(name) {
        var func = _[name] = obj[name];
        _.prototype[name] = function() {
            var args = [this._wrapped];
            push.apply(args, arguments);
            return func.apply(_, args);
        };
    });
    return _;
};

_.mixin(_);
// 修改后
var ArrayProto = Array.prototype;
var push = ArrayProto.push;

var chainResult = function (instance, obj) {
    return instance._chain ? _(obj).chain() : obj;
};

_.mixin = function(obj) {
    _.each(_.functions(obj), function(name) {
        var func = _[name] = obj[name];
        _.prototype[name] = function() {
            var args = [this._wrapped];
            push.apply(args, arguments);
            return chainResult(this, func.apply(_, args));
        };
    });
    return _;
};

_.mixin(_);

_.value

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

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

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

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

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

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

此时调用方式为:

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

最终代码

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

(function() {

    var root = (typeof self == 'object' && self.self == self && self) ||
        (typeof global == 'object' && global.global == global && global) ||
        this || {};

    var ArrayProto = Array.prototype;

    var push = ArrayProto.push;

    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
    };

    if (typeof exports != 'undefined' && !exports.nodeType) {
        if (typeof module != 'undefined' && !module.nodeType && module.exports) {
            exports = module.exports = _;
        }
        exports._ = _;
    } else {
        root._ = _;
    }

    _.VERSION = '0.2';

    var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

    var isArrayLike = function(collection) {
        var length = collection.length;
        return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
    };

    _.each = function(obj, callback) {
        var length, i = 0;

        if (isArrayLike(obj)) {
            length = obj.length;
            for (; i < length; i++) {
                if (callback.call(obj[i], obj[i], i) === false) {
                    break;
                }
            }
        } else {
            for (i in obj) {
                if (callback.call(obj[i], obj[i], i) === false) {
                    break;
                }
            }
        }

        return obj;
    }

    _.isFunction = function(obj) {
        return typeof obj == 'function' || false;
    };

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

    /**
     * 在 _.mixin(_) 前添加自己定义的方法
     */
    _.reverse = function(string){
        return string.split('').reverse().join('');
    }

    _.chain = function(obj) {
        var instance = _(obj);
        instance._chain = true;
        return instance;
    };

    var chainResult = function(instance, obj) {
        return instance._chain ? _(obj).chain() : obj;
    };

    _.mixin = function(obj) {
        _.each(_.functions(obj), function(name) {
            var func = _[name] = obj[name];
            _.prototype[name] = function() {
                var args = [this._wrapped];
                push.apply(args, arguments);
                return chainResult(this, func.apply(_, args));
            };
        });
        return _;
    };

    _.mixin(_);

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

})()

underscore 系列

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

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

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

@xxxgitone
Copy link

您好,JQueryeq方法里面是不是应该为

  this.elements = [this.elements[num]];

@mqyqingfeng
Copy link
Owner Author

@xxxgitone 非常感谢指出~ o( ̄▽ ̄)d

@mqyqingfeng
Copy link
Owner Author

本条评论用作修改日志。


  1. 2017 年 11 月 27 日

修改 this.elements = [this.elements[0]];this.elements = [this.elements[num]];

感谢 @xxxgitone

@Soyn
Copy link

Soyn commented Nov 27, 2017

楼主能分享一下你是如何阅读源码的吗?

@mqyqingfeng
Copy link
Owner Author

@Soyn 刚开始是因为写 JavaScript 专题系列会涉及到去重、查找数组元素、数组扁平化等等功能,所以研究了 underscore 中这些功能的实现方式,然后写 underscore 系列才开始正式读源码。

因为 underscore 它是一个功能函数库,所以首先要搞明白的就是那么多的函数,是如何组织的?这其实就是这个系列第一篇和第二篇的内容。

接下来是阅读内部函数如 cb、optimizeCb、restArgs、shallowProperty、deepGet,因为这些函数作为内部函数会被多次用到。

最后再是跟着兴趣,你想了解哪些函数的实现就去研究哪些函数的实现方式。

在具体研究一些函数的实现方式时,可以参考一些已经写过的源码分析的文章是如何解读的,可以事半功倍:

韩子迟源码分析系列
吴晓军源码分析系列

@Soyn
Copy link

Soyn commented Nov 29, 2017

我读underscore代码的时候,第一遍是直接看源码,很多地方看的云里雾里,后面看第二遍的时候,看underscore的单元测试加源码,但是读完之后很快就忘了,和楼主相比实在是惭愧。

@mqyqingfeng
Copy link
Owner Author

@Soyn 不敢当啦,因为要写文章,很多有疑问的地方都要想明白,要不然,被人问了,答错了就尴尬了……

@FrontToEnd
Copy link

跟着大神再看一遍underscore,昨天看到你在知乎回答的如何学习前端,我打算按照推荐的步骤开始学

@mqyqingfeng
Copy link
Owner Author

@FrontToEnd 哈哈,谢谢提醒,补一下我的回答 怎样系统地自学前端?,其实这几个系列就是按照这样的思路去写的~

@wudao370859172
Copy link

为什么不直接return this?

@JerryWu1234
Copy link

instance._chain ? _(obj).chain() : obj; 什么情况下才会返回obj??instance._chain 是肯定有值的呀

@hfutpath
Copy link

感谢感谢,十分感谢!!

@Yanhua67
Copy link

@wulinsheng123 应该是当你不需要 链式调用 的时候吧。

@Gloomysunday28
Copy link

_(obj).chain() 这里不是又创建了一次实例吗? 那么两个实例将不共享同一个属性, 按照道理应该是
instance._chain ? instance : obj

@lazybonee
Copy link

@wudao370859172 应该是因为像push,shift这些函数本身都有返回的值,return this会改变这些方法调用时的返回值的~

@lazybonee
Copy link

@Gloomysunday28 但是函数的返回值不是instance,而是obj呀。考虑代码
_.mixin({ addOne: num => num + 1}) ; _.chain(4).addOne().value()
按照你的写法会返回4而不是5

@angelayun
Copy link

@mqyqingfeng 博主,你二版的代码
var arr = _.chain([1, 2, 3]).push(4).shift().value();
console.log(arr) // [2, 3, 4]
没有办法用这个代码进行测试

@puck1006
Copy link

instance._chain ? _(obj).chain() : obj; 什么情况下才会返回obj??instance._chain 是肯定有值的呀

没有_chain()过的话.instance._chain是undefined

@puck1006
Copy link

_(obj).chain() 这里不是又创建了一次实例吗? 那么两个实例将不共享同一个属性, 按照道理应该是
instance._chain ? instance : obj

你这个写法收集不了 函数返回值

@EricWong1994
Copy link

问题1:我只是在最终版的闭包里加了一个push方法和console.log(.chain([1]).push(2));
就报错了。
222copy.html:97 Uncaught TypeError: Cannot read property 'push' of undefined;
哪位大神能帮忙定位一下啊。
https://codepen.io/rang1994/pen/gOLBPQR
我感觉是push方法中的this 指向的是
,而_没有_wrapped属性。
问题2:还有chainResult方法中的_.chain(obj)为什么要改成 (obj).chain()。
问题3:return chainResult(this, func.apply(
, args));
apply后为什么是_而不是this。
求大神解答

@EricWong1994
Copy link

看了一晚上,debug了源码对自己提的三个问题有了一些了解。
1、自己写的push方法有问题,应该在参数中取array和arguments。跟_没有任何关系
2、chain方法是通过mixin方法复制到原型上的,所以var args = [this.wrapped]; func.apply(, args) 即_.chain(obj)
貌似这两个方法是等价的 _.chain(obj) 与 _(obj).chain()
3、以前看不懂,其实就是复制了构造函数上的方法到原型上,当this肯定还是指向原来的构造函数的

@tyaqing
Copy link

tyaqing commented Apr 12, 2021

我感觉这个版本的链式实现有点冗余了,不停的创建了新的实例,感觉没必要,事实上push直接返回this就好了,顺便写下我的code

function _(obj) {
  // eslint-disable-next-line new-cap
  if (!(this instanceof _)) return new _(obj)
  this._wraps = obj
}
// 遍历所有的函数 返回函数名称数组
_.functions = function (obj) {
  const funcs = []
  for (const key in obj) {
    if (typeof obj[key] === 'function') funcs.push(key)
  }
  return funcs
}

_.chain = function (obj) {
  const instance = _(obj)
  instance._chain = true
  return instance
}
_.prototype.push = function (num) {
  this._wraps.push(num)
  return this
}
_.prototype.shift = function () {
  this._wraps.shift()
  return this
}
_.prototype.value = function () {
  return this._wraps
}
// 将underscore的函数放入到新的实例中去
_.mixin = function (obj) {
  _.functions(obj).forEach((name) => {
    const func = (_[name] = obj[name])
    _.prototype[name] = function (arg) {
      // 这里 context是underscore 是因为this需要访问underscore的成员变量和函数
      // 如果是this的话, allarg 的参数会有问题
      const allArg = [this._wraps].concat([arg])
      return func.apply(_, allArg)
    }
  })
  return _
}
_.mixin(_)

// underscore.version('yakir')

// underscore('laige').version('add').version()
const res = _.chain([1, 2, 3]).push(4).shift().push(9).value()

console.log(res)

@mqyqingfeng
Copy link
Owner Author

@jxccc1998 不清楚你要问的是什么,新增的函数基本都是直接挂载 _ 函数对象上的

@lyric-zemin
Copy link

function JQuery(selector) {
    this.elements = [];
    var nodeLists = document.getElementsByTagName(selector);
    for (var i = 0; i < nodeLists.length; i++) {
        this.elements.push(nodeLists[i]);
    }
    return this;
}

JQuery.prototype = {
    eq: function(num){
        this.elements = [this.elements[num]];
        return this;
    },
    css: function(prop, val) {
        this.elements.forEach(function(el){
            el.style[prop] = val;
        })
        return this;
    },
    show: function() {
        this.css('display', 'block');
        return this;
    }

}

@mqyqingfeng 很奇怪为啥这段代码原型上没有constructor为啥也能正常实例化

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests