underscore-诞生记二-链式调用与混入mixin

上篇文章讲述了 underscore 的基本结构搭建,本文继续讲链式调用与混入。 如果你还没看过第一篇文章,请点击 “underscore 诞生记(一)—— 基本结构搭建” 链式调用在 JQuery 中,我们经常使用到链式调用,如: $('.div') .css('color', 'red') .show();那么在 underscore 中,是否支持链式调用呢?答案是支持的,只不过默认不开启链式调用罢了。 想要实现链式调用,通常我们会在支持链式调用的函数中返回对象本身: let car = { run(name) { console.log(`${name}老司机开车啦喂!`); return this; }, stop() { console.log('车停了'); },};car.run('奔驰').stop();// 奔驰老司机开车啦喂!// 车停了那么在每个 _ 方法下都 return this , 显然不大优雅缺乏可控性!尝试着写个通用方法 chain() 开启链式调用。 _.chain = function(obj) { // 获得一个经underscore包裹后的实例 var instance = _(obj); // 标识当前实例支持链式调用 instance._chain = true; return instance;};// 小试牛刀_.chain([1, 2, 3]);/* { _chain: true, _wrapped: [1, 2, 3]} */返回的为一个实例对象,后面的方法判断 _chain 属性是否为 true,为 true 的话再调用一次 chain() 方法再返回原来实例即可。我们在之前用于给 prototype 复制方法的 each() 函数加入判断吧 ...

April 29, 2019 · 4 min · jiezi

跟underscore一起学数组去重

引子数组去重是一个老生常谈的话题,在面试中也经常会被问道。对于去重,有两种主流思想:先排序,线性遍历后去重,时间复杂度O(n*log2n);使用哈希,空间换时间,时间复杂度O(n);上一篇文章,我分析了underscore的函数是如何组织的,我们能够依照这种方法书写自己的函数库,这篇文章,来看看关于函数去重underscore是如何做的?Underscore的去重功能介绍underscore的去重是指数组(Arrays)中uniq函数,其API如下:uniq .uniq(array, [isSorted], [iteratee]) 别名: unique 说明:返回 array去重后的副本, 使用 === 做相等测试. 如果您确定 array 已经排序, 那么给 isSorted 参数传递 true值, 此函数将运行的更快的算法. 如果要处理对象元素, 传参 iterator 来获取要对比的属性.上述API主要想说明几点:返回数组副本,不影响原数组相等的标准是a===b,表明不仅要值相等,类型也需要相等如果数组是排序的,去重运算效率更高uniq也可以比较对象,前提是需要指定比较的对象属性我们简单使用以下.uniq(array, [isSorted], [iteratee]),如下: console.log(.uniq([1,4,2,2,3,3])); console.log(.uniq([1,2,2,2,3,3],true)); console.log(.uniq([{ name:1, gender:“male” },{ name:2, gender:“female” },{ name:2, gender:“male” },{ name:4, gender:“male” }],true,“gender”));结果如下:去重思想及实现underscore去重的核心思想:新建结果集数组res,遍历待去重数组,将每个遍历值在res数组中遍历检查,将不存在当前res中的遍历值压入res中,最后输出res数组。 function uniq(array){ var res = []; array.forEach(function(element) { if(res.indexOf(element)<0){ res.push(element); } }, this); return res; } console.log(uniq([1,4,2,2,3,3])); //[1,4,2,3]其中如果数组是排序的,去重运算效率更高,因为排序能够将相同的数排列在一起,方便前后比较。 function uniq(array, isSorted) { var res = []; var seen = null; array.forEach(function (element,index) { if (isSorted) { //当数组有序 if(!index || seen !== element) res.push(element); seen = element; } else { if (res.indexOf(element) < 0) { res.push(element); } } }, this); return res; } console.log(uniq([1,2,“2”,3,3,3,5],true)); //(5) [1, 2, “2”, 3, 5]对于对象的去重,我们知道{}==={}为false,所以使用===比较对象在实际场景中没有意义。 在这里我举个实际场景的例子:我要在小组中选择一名男生(male)和一名女生(female),小组组员情况如下:var array = [{ name:“Tom”, gender:“female”},{ name:“Lucy”, gender:“female”},{ name:“Edward”, gender:“male”},{ name:“Molly”, gender:“female”}] 我们修改上面的uniq: function uniq(array, isSorted, iteratee) { var res = []; var seen = []; array.forEach(function (element, index) { if (iteratee) { //判断iteratee是否存在,存在的话,取出真正要比较的属性 var computed = element[iteratee]; if (seen.indexOf(computed) < 0) { seen.push(computed); res.push(element); } } else if (isSorted) { //当数组有序 if (!index || seen !== element) res.push(element); seen = element; } else { if (res.indexOf(element) < 0) { res.push(element); } } }, this); return res; } console.log(uniq([{ name:“Tom”, gender:“female” },{ name:“Lucy”, gender:“female” },{ name:“Edward”, gender:“male” },{ name:“Molly”, gender:“female” }],true,“gender”)); 结果如下: underscore的uniq的实现,基本上使用的上述思想。在附录中我附上了源码和一些注释。关于去重的思考上述我分析了underscore的uniq函数实现,在这之前我也看过诸如《JavaScript去重的N种方法》…之类的文章,underscore中的uniq函数实现方法并不是最优解,至少从时间复杂度来讲不是最优。 那么为什么underscore不用Set对象来解决去重问题,使用indexof查找的时间复杂度是O(n),而hash查询是O(1)。 我个人认为Set是ES6中引的对象,underscore是为了考虑兼容性问题。 那为什么不用obj作为Set的替代方案呢? 这里我猜是underscore的设计者只想用自己内部实现的.indexOf函数。此处是我的猜测,大家如果有想法,欢迎大家留言! 下面我附上ES6的实现(大家最熟悉的):var a = [1,1,2,3,4,4];var res = […new Set(a)];再附上obj的实现: function uniq(array,iteratee){ var res = []; var obj = {}; array.forEach(function(element) { var computed = element; if(iteratee) computed = element[iteratee]; if(!obj.hasOwnProperty(computed)) obj[(typeof computed)+""+JSON.stringify(computed)] = element; }, this); for(var p in obj){ res.push(obj[p]); } return res; } uniq([1,“1”,2,3,4,4]);// (5) [1, “1”, 2, 3, 4]附录underscore的uniq函数源码及注释: .uniq = .unique = function(array, isSorted, iteratee, context) { if (array == null) return []; if (!.isBoolean(isSorted)) { //如果没有排序 context = iteratee; iteratee = isSorted; isSorted = false; } /** ** 此处.iteratee ** function (key){ * return function(obj){ * return obj[key]; * } ** } ** key就是这里的iteratee(对象的属性),这里使用了闭包 **/ if (iteratee != null) iteratee = .iteratee(iteratee, context); var result = [];//返回去重后的数组(副本) var seen = []; for (var i = 0, length = array.length; i < length; i++) { var value = array[i];//当前比较值 if (isSorted) { //如果i=0时,或者seen(上一个值)不等于当前值,放入去重数组中 if (!i || seen !== value) result.push(value); seen = value;//保存当前值,用于下一次比较 } else if (iteratee) { var computed = iteratee(value, i, array); if (.indexOf(seen, computed) < 0) { seen.push(computed); result.push(value); } } else if (.indexOf(result, value) < 0) { result.push(value); } } return result; }; ...

March 15, 2019 · 2 min · jiezi

underscore中的小技巧

在阅读 underscore 的过程中,发现了它的一些小技巧,对我们平时的编程很有用。在这里向大家介绍一二void 0 代替 underfined首先,看源码:_.isUndefined = function(obj) { return obj === void 0;};这里为啥要用 obj === void 0, 而不是 obj === undefined 呢?因为,在 js 中,undefined 并不是类似关键字(js 关键字有 function,return …),所以,理论上是可以更改的。事实上,在 IE8 上也的确是可以被更改的,var undefined = 1;alert(undefined); // 1 – IE8, undefined — chrome而在 chrome 或高版本的 IE 中,并不能更改全局的 undefined 。但是,局部的 undefined 仍然可以被改变。例如:(function() { var undefined = 1; alert(undefined); // 1 – chrome})();所以, undefined 并不十分可靠,所以才需要 void 0 , void 是 js 的保留字,所以不可被更改。在 MDN 上定义是:The void operator evaluates the given expression and then returns undefined.翻译:void 操作符会对 void 后面的表达式进行运算,然后返回 undefined所以,使用void会一直返回 undefined ,所以,可以用void 0替代undefined.复制数组Array.prototype.slice.call(array); 可用来复制一个数组,或将类数组转换为数组在 js 中,如果我们想复制一个数组,要如何复制呢?也许你会这样做:function copy(array) { var copyArray = []; for (var i = 0, len = array.length; i < len; i++) { copyArray.push(array[i]); } return copyArray;}其实,我们可以利用数组的 slice 和 concat 方法返回新的数组这个特性,来实现复制数组的功能;var newArray = Array.prototype.slice.call(array);var newArray2 = Array.prototype.concat.call(array);而且,性能方面, slice 以及 concat 比单纯使用 for 循环还要更加高效var array = .range(10000000); //.range,是undescore一个方法,用于生成一个从0到10000000的数组console.time(‘for copy push’);var copyArray1 = [];for (var i = 0, length = array.length; i < length; i++) { copyArray1.push(array[i]);}console.timeEnd(‘for copy push’);console.time(‘slice’);var copyArray2 = Array.prototype.slice.call(array);console.timeEnd(‘slice’);console.time(‘concat’);var copyArray3 = Array.prototype.concat.call(array);console.timeEnd(‘concat’);//结果//for copy push: 379.315ms//slice: 109.300ms//concat: 92.852ms另外,也是通过 slice , call 将类数组转换为数组function test() { console.log(Array.prototype.slice.call(arguments));}test(1, 2, 3); //输出[1, 2, 3]使用 Array[length]=value 代替 push 添加元素实际业务代码,除非对性能要求极高,否则还是推荐 push,毕竟更符合习惯首先看源码 .values().values = function(obj) { var keys = .keys(obj); var length = keys.length; var values = Array(length); //等同于new Array(length) for (var i = 0; i < length; i++) { values[i] = obj[keys[i]]; } return values;};一开始看这种写法,并不习惯,我们大多数人可能更习惯这样写(使用 push ):.values = function(obj) { var keys = _.keys(obj); var length = keys.length; var values = []; // for (var i = 0; i < length; i++) { values.push(obj[keys[i]]); //使用push } return values;};实际测试中,第一种写法会比第二种更快。关键在于,我们事先知道要填充的数组 values 的长度,然后预先生成一个对应长度的数组,之后只需要给对应的位置赋值。而第二种在 push 的时候,除了给对应位置赋值,还需要改变 values 数组的 length。所以,建议在已知长度的情况下,使用第一种,而不知道长度的情况下,使用第二种。适当的使用 return function当我们编写两个功能非常相近的函数时,例如,实现复制一个数组的功能,分别是正序和倒序,我们可能会这样子实现(这里只是为了举例子,复制数组推荐第二点提到的使用slice或concat):function copyArray(array, dir) { var copyArray = []; var i = dir > 0 ? 0 : array.length - 1; for (; i >= 0 && i < array.length; i += dir) { copyArray.push(array[i]); } return copyArray;}var copyDesc = function(array) { return copyArray(array, 1);};var copyAsce = function(array) { return copyArray(array, -1);};这样子实现会有什么问题呢?其实对copyDesc,copyAsce,来说,只有 dir 是不同的而已,但是,这种方式实现,却需要将 array 也作为参数传递给 `copyArray。而copyDesc,copyAsce其实只是一个转发的作用而已。我们可以继续优化:function copyArray(dir) { return function(array) { var copyArray = []; var i = dir > 0 ? 0 : array.length - 1; for (; i >= 0 && i < array.length; i += dir) { copyArray.push(array[i]); } return copyArray; };}var copyDesc = copyArray(1);var copyAsce = copyArray(-1);我觉得 return function 这种写法比较优雅一点,你觉得呢?类型判断,使用 Object.prototype.toString()来判断这里只举两个例子,isString,isArray,其他的例如 isArguments , isFunction , 由于有些浏览器兼容问题需要特殊处理,这里就不细说了。而像isNull,isUndefined,这些比较简单的,这里也不细说了:)我们知道:typeof 可能的返回值有:类型结果Undefined"undefined"Null"object"Boolean"boolean"Number"number"String"string"Symbol(ES6 新增)“symbol"宿主对象(由 JS 环境提供)Implementation-dependent函数对象( [[Call]])“function"任何其他对象"object"但是, typeof 却有下面这种问题typeof “test” —> “string"typeof new String(“test”) —> “object"typeof 123 —–> “number"typeof new Number(123) —>“object"跟我们的期望不太一样,Object.prototype.toString 则没有这问题。Object.prototype.toString.call(’test’); //"[object String]“Object.prototype.toString.call(new String(’test’)); //"[object String]“Object.prototype.toString.call(123); //"[object Number]“Object.prototype.toString.call(new Number(123)); //"[object Number]“所以,我们可以通过Object.prototype.toString来进行类型判断function isNumber(obj) { return Object.prototype.toString.call(obj) === ‘[object Number]’;}function isString(obj) { return Object.prototype.toString.call(obj) === ‘[object String]’;}待续。。。 ...

December 21, 2018 · 2 min · jiezi

不定参数(rest 参数 ...)

不定参数如何实现不定参数使用过 underscore.js 的人,肯定都使用过以下几个方法:.without(array, *values) //返回一个删除所有values值后的array副本.union(*arrays) //返回传入的arrays(数组)并集_.difference(array, *others)//返回来自array参数数组,并且不存在于other 数组…这些方法都有一个共同点,就是可以传入不定数量的参数,例如,我想删除掉 array 中的 value1,value2 ,可以这样使用 , .without(array,value1,value2);那么,这个需要怎样才能做到呢?我们知道,js 中 function 里面,有一个 arguments 参数,它是一个类数组,里面包含着调用这个方法的所有参数,所以可以这样处理:.without = function() { if (arguments.length > 0) { var array = arguments[0]; var values = []; for (var i = 1; i < arguments.length; i++) { values.push(arguments[i]); } //这样得到了array,和values数组,便可以进一步处理了 }};上面只是打个比方,想要支持不定参数,我们要做的就是把固定参数和动态参数从 arguments 中分离出来。但是,我们这样写的话,需要在每个支持不定参数的函数里,都 copy 这样一段代码,这样实在不是很优雅。所以需要封装成一个通用的函数。我们直接看看 underscore 是封装的好了。restArgs 源码var restArgs = function(func, startIndex) { //startIndex ,表示几个参数之后便是动态参数 startIndex = startIndex == null ? func.length - 1 : +startIndex; return function() { var length = Math.max(arguments.length - startIndex, 0); //处理arguments,将动态参数保存进rest数组 var rest = Array(length); for (var index = 0; index < length; index++) { rest[index] = arguments[index + startIndex]; } //处理0,1,2三种情况,这里要单独处理,是想优先使用call,因为,call的性能比apply要好一点 switch (startIndex) { case 0: return func.call(this, rest); case 1: return func.call(this, arguments[0], rest); case 2: return func.call(this, arguments[0], arguments[1], rest); } //如果startIndex不是0,1,2三种情况,则使用apply调用方法,将args作为参数,args将为数组[固定参数 ,rest]; var args = Array(startIndex + 1); for (index = 0; index < startIndex; index++) { args[index] = arguments[index]; } args[startIndex] = rest; return func.apply(this, args); };};//这里without主要的逻辑处理方法,作为参数,传给restArgs,在restArgs中处理完参数后,使用call或apply调用逻辑处理方法// 这时候接受到参数otherArrays,已经是一个数组了,包含了之前的动态参数。_.without = restArgs(function(array, otherArrays) { //处理without具体事件});underscore.js 中利用 js 高级函数的特性,巧妙的实现了动态参数如果要使某函数支持不定参数,只需要将该函数作为参数,传入 restArgs 中即可,例如:function addByArray(values) { var sum = 0; for (var i = 0; i < values.length; i++) { sum += values[i]; } return sum;}var add = restArgs(addByArray);//调用:addByArray([2, 5, 3, 6]); //16add(2, 5, 3, 6); //16ES6 不定参数 (…)ES6 引入了 rest 参数,(形式为"…变量名"),用于获取多余参数,这样就不需要使用 arguments 对象来实现了function add(…values) { let sum = 0; for (var val of values) { sum += val; } return sum;}add(2, 5, 3); // 10总结在 underscore 中,restArgs 只是为了支持不定参数。实际使用中,也许我们都是直接使用 ES6,或用 babel 将 ES6 转成 ES5 来支持不定参数不过,如果是在非 es6 的环境下,知道有这么一种实现方式,也是挺好的。:) ...

December 21, 2018 · 2 min · jiezi