JavaScript-实现数组更多的高阶函数

JavaScript 实现数组更多的高阶函数场景虽说人人平等,但有些人更加平等。为什么有了 Lodash 这种通用函数工具库,吾辈要写这篇文章呢?吾辈在 SegmentFault 上经常看到关于 JavaScript 数组的相关疑问,甚至于,相同类型的问题,只是数据变化了一些,就直接提出了一个新的问题(实际上,对自身并无帮助)。简单搜索了一下 Array,居然有 2360+ 条的结果,足可见这类问题的频率之高。若是有一篇适合 JavaScript 萌新阅读的自己实现数组更多操作的文章,情况是否会发生变化呢? 下面吾辈便来实现以下几种常见的操作 uniqueBy: 去重sortBy: 排序filterItems: 过滤掉一些元素diffBy: 差异groupBy: 分组递归操作前言:你至少需要了解 ES6 的一些特性你才能愉快的阅读uniqueBy: 去重相关问题 javascript 怎么实现多种数据类型的数组去重?JS 有没有比较高效的数组去重的方法?/** * js 的数组去重方法 * @param arr 要进行去重的数组 * @param kFn 唯一标识元素的方法,默认使用 {@link returnItself} * @returns 进行去重操作之后得到的新的数组 (原数组并未改变) */function uniqueBy(arr, kFn = val => val) { const set = new Set() return arr.filter((v, ...args) => { const k = kFn(v, ...args) if (set.has(k)) { return false } set.add(k) return true })}使用 ...

July 6, 2019 · 6 min · jiezi

JavaScript 之函数式编程

同步发布于 https://github.com/xianshanna…是个程序员都知道函数,但是有些人不一定清楚函数式编程的概念。应用的迭代使程序变得越来越复杂,那么程序员很有必要创造一个结构良好、可读性好、重用性高和可维护性高的代码。函数式编程就是一个良好的代码方式,但是这不代表函数式编程是必须的。你的项目没用到函数式编程,不代表项目不好。什么是函数式编程(FP)?函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程的对立面就是命令式编程。函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。 变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。函数式编程只是一个概念(一致编码方式),并没有严格的定义。本人根据网上的知识点,简单的总结一下函数式编程的定义(本人总结,或许有人会不同意这个观点)。函数式编程就是纯函数的应用,然后把不同的逻辑分离为许多独立功能的纯函数(模块化思想),然后再整合在一起,变成复杂的功能。什么是纯函数?一个函数如果输入确定,那么输出结果是唯一确定的,并且没有副作用,那么它就是纯函数。一般符合上面提到的两点就算纯函数:相同的输入必定产生相同的输出在计算的过程中,不会产生副作用那怎么理解副作用呢?简单的说就是变量的值不可变,包括函数外部变量和函数内部变量。所谓副作用,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。这里说明一下不可变,不可变指的是我们不能改变原来的变量值。或者原来变量值的改变,不能影响到返回结果。不是变量值本来就是不可变。纯函数特性对比例子上面的理论描述对于刚接触这个概念的程序员,或许不好理解。下面会通过纯函数的特点一一举例说明。输入相同返回值相同纯函数function test(pi) { // 只要 pi 确定,返回结果就一定确定。 return pi + 2;}test(3);非纯函数function test(pi) { // 随机数返回值不确定 return pi + Math.random();}test(3);返回值不受外部变量的影响非纯函数,返回值会被其他变量影响(说明有副作用),返回值不确定。let a = 2;function test(pi) { // a 的值可能中途被修改 return pi + a;}a = 3;test(3);非纯函数,返回值受到对象 getter 的影响,返回结果不确定。const obj = Object.create( {}, { bar: { get: function() { return Math.random(); }, }, });function test(obj) { // obj.a 的值是随机数 return obj.a;}test(obj);纯函数,参数唯一,返回值确定。function test(pi) { // 只要 pi 确定,返回结果就一定确定。 return pi + 2;}test(3);输入值是不可以被改变的非纯函数,这个函数已经改变了外面 personInfo 的值了(产生了副作用)。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function revereName(p) { p.lastName = p.lastName .split(’’) .reverse() .join(’’); p.firstName = p.firstName .split(’’) .reverse() .join(’’); return ${p.firstName} ${p.lastName};}revereName(personInfo);console.log(personInfo);// 输出 { firstName: ’nannahs’,lastName: ’naix’ }// personInfo 被修改了纯函数,这个函数不影响外部任意的变量。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function reverseName(p) { const lastName = p.lastName .split(’’) .reverse() .join(’’); const firstName = p.firstName .split(’’) .reverse() .join(’’); return ${firstName} ${lastName};}revereName(personInfo);console.log(personInfo);// 输出 { firstName: ‘shannan’,lastName: ‘xian’ }// personInfo 还是原值那么你们是不是有疑问,personInfo 对象是引用类型,异步操作的时候,中途改变了 personInfo,那么输出结果那就可能不确定了。如果函数存在异步操作,的确有存在这个问题,的确应该确保 personInfo 不能被外部再次改变(可以通过深度拷贝)。但是,这个简单的函数里面并没有异步操作,reverseName 函数运行的那一刻 p 的值已经是确定的了,直到返回结果。下面的异步操作才需要确保 personInfo 中途不会被改变:async function reverseName(p) { await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); const lastName = p.lastName .split(’’) .reverse() .join(’’); const firstName = p.firstName .split(’’) .reverse() .join(’’); return ${firstName} ${lastName};}const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };async function run() { const newName = await reverseName(personInfo); console.log(newName);}run();personInfo.firstName = ’test’;// 输出为 tset naix,因为异步操作的中途 firstName 被改变了修改成下面的方式就可以确保 personInfo 中途的修改不影响异步操作:// 这个才是纯函数async function reverseName(p) { // 浅层拷贝,这个对象并不复杂 const newP = { …p }; await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); const lastName = newP.lastName .split(’’) .reverse() .join(’’); const firstName = newP.firstName .split(’’) .reverse() .join(’’); return ${firstName} ${lastName};}const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };// run 不是纯函数async function run() { const newName = await reverseName(personInfo); console.log(newName);}// 当然小先运行 run,然后再去改 personInfo 对象。run();personInfo.firstName = ’test’;// 输出为 nannahs naix这个还是有个缺点,就是外部 personInfo 对象还是会被改到,但不影响之前已经运行的 run 函数。如果再次运行 run 函数,输入都变了,输出当然也变了。参数和返回值可以是任意类型那么返回函数也是可以的。function addX(y) { return function(x) { return x + y; };}尽量只做一件事当然这个要看实际应用场景,这里举个简单例子。两件事一起做(不太好的做法):function getFilteredTasks(tasks) { let filteredTasks = []; for (let i = 0; i < tasks.length; i++) { let task = tasks[i]; if (task.type === ‘RE’ && !task.completed) { filteredTasks.push({ …task, userName: task.user.name }); } } return filteredTasks;}const filteredTasks = getFilteredTasks(tasks);getFilteredTasks 也是纯函数,但是下面的纯函数更好。两件事分开做(推荐的做法):function isPriorityTask(task) { return task.type === ‘RE’ && !task.completed;}function toTaskView(task) { return { …task, userName: task.user.name };}let filteredTasks = tasks.filter(isPriorityTask).map(toTaskView);isPriorityTask 和 toTaskView 就是纯函数,而且都只做了一件事,也可以单独反复使用。结果可缓存根据纯函数的定义,只要输入确定,那么输出结果就一定确定。我们就可以针对纯函数返回结果进行缓存(缓存代理设计模式)。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function reverseName(firstName, lastName) { const newLastName = lastName .split(’’) .reverse() .join(’’); const newFirstName = firstName .split(’’) .reverse() .join(’’); console.log(‘在 proxyReverseName 中,相同的输入,我只运行了一次’); return ${newFirstName} ${newLastName};}const proxyReverseName = (function() { const cache = {}; return (firstName, lastName) => { const name = firstName + lastName; if (!cache[name]) { cache[name] = reverseName(firstName, lastName); } return cache[name]; };})();函数式编程有什么优点?实施函数式编程的思想,我们应该尽量让我们的函数有以下的优点:更容易理解更容易重复使用更容易测试更容易维护更容易重构更容易优化更容易推理函数式编程有什么缺点?性能可能相对来说较差函数式编程可能会牺牲时间复杂度来换取了可读性和维护性。但是呢,这个对用户来说这个性能十分微小,有些场景甚至可忽略不计。前端一般场景不存在非常大的数据量计算,所以你尽可放心的使用函数式编程。看下上面提到个的例子(数据量要稍微大一点才好对比):首先我们先赋值 10 万条数据:const tasks = [];for (let i = 0; i < 100000; i++) { tasks.push({ user: { name: ‘one’, }, type: ‘RE’, }); tasks.push({ user: { name: ’two’, }, type: ‘’, });}两件事一起做,代码可读性不够好,理论上时间复杂度为 o(n),不考虑 push 的复杂度。(function() { function getFilteredTasks(tasks) { let filteredTasks = []; for (let i = 0; i < tasks.length; i++) { let task = tasks[i]; if (task.type === ‘RE’ && !task.completed) { filteredTasks.push({ …task, userName: task.user.name }); } } return filteredTasks; } const timeConsumings = []; for (let k = 0; k < 100; k++) { const beginTime = +new Date(); getFilteredTasks(tasks); const endTime = +new Date(); timeConsumings.push(endTime - beginTime); } const averageTimeConsuming = timeConsumings.reduce((all, current) => { return all + current; }) / timeConsumings.length; console.log(第一种风格平均耗时:${averageTimeConsuming} 毫秒);})();两件事分开做,代码可读性相对好,理论上时间复杂度接近 o(2n)(function() { function isPriorityTask(task) { return task.type === ‘RE’ && !task.completed; } function toTaskView(task) { return { …task, userName: task.user.name }; } const timeConsumings = []; for (let k = 0; k < 100; k++) { const beginTime = +new Date(); tasks.filter(isPriorityTask).map(toTaskView); const endTime = +new Date(); timeConsumings.push(endTime - beginTime); } const averageTimeConsuming = timeConsumings.reduce((all, current) => { return all + current; }) / timeConsumings.length; console.log(第二种风格平均耗时:${averageTimeConsuming} 毫秒);})();上面的例子多次运行得出耗时平均值,在数据较少和较多的情况下,发现两者平均值并没有多大差别。10 万条数据,运行 100 次取耗时平均值,第二种风格平均多耗时 15 毫秒左右,相当于 10 万条数据多耗时 1.5 秒,1 万条数多据耗时 150 毫秒(150 毫秒用户基本感知不到)。虽然理论上时间复杂度多了一倍,但是在数据不庞大的情况下(会有个临界线的),这个性能相差其实并不大,完全可以牺牲浏览器用户的这点性能换取可读和可维护性。很可能被过度使用过度使用反而是项目维护性变差。有些人可能写着写着,就变成别人看不懂的代码,自己觉得挺高大上的,但是你确定别人能快速的看懂不? 适当的使用才是合理的。应用场景概念是概念,实际应用却是五花八门,没有实际应用,记住了也是死记硬背。这里总结一些常用的函数式编程应用场景。简单使用有时候很多人都用到了函数式的编程思想(最简单的用法),但是没有意识到而已。下面的列子就是最简单的应用,这个不用怎么说明,根据上面的纯函数特点,都应该看的明白。function sum(a, b) { return a + b;}立即执行的匿名函数匿名函数经常用于隔离内外部变量(变量不可变)。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function reverseName(firstName, lastName) { const newLastName = lastName .split(’’) .reverse() .join(’’); const newFirstName = firstName .split(’’) .reverse() .join(’’); console.log(‘在 proxyReverseName 中,相同的输入,我只运行了一次’); return ${newFirstName} ${newLastName};}// 匿名函数const proxyReverseName = (function() { const cache = {}; return (firstName, lastName) => { const name = firstName + lastName; if (!cache[name]) { cache[name] = reverseName(firstName, lastName); } return cache[name]; };})();JavaScript 的一些 API如数组的 forEach、map、reduce、filter 等函数的思想就是函数式编程思想(返回新数组),我们并不需要使用 for 来处理。const arr = [1, 2, ‘’, false];const newArr = arr.filter(Boolean);// 相当于 const newArr = arr.filter(value => Boolean(value))递归递归也是一直常用的编程方式,可以代替 while 来处理一些逻辑,这样的可读性和上手度都比 while 简单。如下二叉树所有节点求和例子:const tree = { value: 0, left: { value: 1, left: { value: 3, }, }, right: { value: 2, right: { value: 4, }, },};while 的计算方式:function sum(tree) { let sumValue = 0; // 使用列队方式处理,使用栈也可以,处理顺序不一样 const stack = [tree]; while (stack.length !== 0) { const currentTree = stack.shift(); sumValue += currentTree.value; if (currentTree.left) { stack.push(currentTree.left); } if (currentTree.right) { stack.push(currentTree.right); } } return sumValue;}递归的计算方式:function sum(tree) { let sumValue = 0; if (tree && tree.value !== undefined) { sumValue += tree.value; if (tree.left) { sumValue += sum(tree.left); } if (tree.right) { sumValue += sum(tree.right); } } return sumValue;}递归会比 while 代码量少,而且可读性更好,更容易理解。链式编程如果接触过 jquery,我们最熟悉的莫过于 jq 的链式便利了。现在 ES6 的数组操作也支持链式操作:const arr = [1, 2, ‘’, false];const newArr = arr.filter(Boolean).map(String);// 输出 “1”, “2”]或者我们自定义链式,加减乘除的链式运算:function createOperation() { let theLastValue = 0; const plusTwoArguments = (a, b) => a + b; const multiplyTwoArguments = (a, b) => a * b; return { plus(…args) { theLastValue += args.reduce(plusTwoArguments); return this; }, subtract(…args) { theLastValue -= args.reduce(plusTwoArguments); return this; }, multiply(…args) { theLastValue *= args.reduce(multiplyTwoArguments); return this; }, divide(…args) { theLastValue /= args.reduce(multiplyTwoArguments); return this; }, valueOf() { const returnValue = theLastValue; // 获取值的时候需要重置 theLastValue = 0; return returnValue; }, };}const operaton = createOperation();const result = operation .plus(1, 2, 3) .subtract(1, 3) .multiply(1, 2, 10) .divide(10, 5) .valueOf();console.log(result);当然上面的例子不完全都是函数式编程,因为 valueOf 的返回值就不确定。高阶函数高阶函数(Higher Order Function),按照维基百科上面的定义,至少满足下列一个条件的函数函数作为参数传入返回值为一个函数简单的例子:function add(a, b, fn) { return fn(a) + fn(b);}function fn(a) { return a * a;}add(2, 3, fn); // 13还有一些我们平时常用高阶的方法,如 map、reduce、filter、sort,以及现在常用的 redux 中的 connect 等高阶组件也是高阶函数。柯里化(闭包)柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。柯里化的作用以下优点:参数复用提前返回延迟计算/运行缓存计算值柯里化实质就是闭包。其实上面的立即执行匿名函数的例子就用到了柯里化。// 柯里化之前function add(x, y) { return x + y;}add(1, 2); // 3// 柯里化之后function addX(y) { return function(x) { return x + y; };}addX(2)(1); // 3高阶组件这是组件化流行后的一个新概念,目前经常用到。ES6 语法中 class 只是个语法糖,实际上还是函数。一个简单例子:class ComponentOne extends React.Component { render() { return <h1>title</h1>; }}function HocComponent(Component) { Component.shouldComponentUpdate = function(nextProps, nextState) { if (this.props.id === nextProps.id) { return false; } return true; }; return Component;}export default HocComponent(ComponentOne);深入理解高阶组件请看这里。无参数风格(Point-free)其实上面的一些例子已经使用了无参数风格。无参数风格不是没参数,只是省略了多余参数的那一步。看下面的一些例子就很容易理解了。范例一:const arr = [1, 2, ‘’, false];const newArr = arr.filter(Boolean).map(String);// 有参数的用法如下:// arr.filter(value => Boolean(value)).map(value => String(value));范例二:const tasks = [];for (let i = 0; i < 1000; i++) { tasks.push({ user: { name: ‘one’, }, type: ‘RE’, }); tasks.push({ user: { name: ’two’, }, type: ‘’, });}function isPriorityTask(task) { return task.type === ‘RE’ && !task.completed;}function toTaskView(task) { return { …task, userName: task.user.name };}tasks.filter(isPriorityTask).map(toTaskView);范例三:// 比如,现成的函数如下:var toUpperCase = function(str) { return str.toUpperCase();};var split = function(str) { return str.split(’’);};var reverse = function(arr) { return arr.reverse();};var join = function(arr) { return arr.join(’’);};// 现要由现成的函数定义一个 point-free 函数toUpperCaseAndReversevar toUpperCaseAndReverse = _.flowRight( join, reverse, split, toUpperCase); // 自右向左流动执行// toUpperCaseAndReverse是一个point-free函数,它定义时并无可识别参数。只是在其子函数中操纵参数。flowRight 是引入了 lodash 库的组合函数,相当于 compose 组合函数console.log(toUpperCaseAndReverse(‘abcd’)); // => DCBA无参数风格优点?参风格的好处就是不需要费心思去给它的参数进行命名,把一些现成的函数按需组合起来使用。更容易理解、代码简小,同时分离的回调函数,是可以复用的。如果使用了原生 js 如数组,还可以利用 Boolean 等构造函数的便捷性进行一些过滤操作。无参数风格缺点?缺点就是需要熟悉无参数风格,刚接触不可能就可以用得得心应手的。对于一些新手,可能第一时间理解起来没那没快。参考文章Learn the fundamentals of functional programming — for free, in your inboxMake your code easier to read with Functional Programming从高阶函数—>高阶组件 ...

April 7, 2019 · 6 min · jiezi

JS基础——高阶函数

定义高阶函数是至少满足下面一个条件的函数:1、接收一个或多个函数作为参数。比如filter函数2、返回一个函数。 比如bind函数举个例子:比如我们要筛数组[1,2,3,4,5]中大于3的所有元素,我们通常的实现方法为:let newArr = [];for(let i = 0,len = arr.length; i < len; i++){ arr[i] > 3 && newArr.push(arr[i])}而使用数组filter方法的话,只需要 let newArr = arr.filter((item) => {return item > 3})。当然我们也可以通过高阶函数来自己实现:Array.prototype.myFilter = function (fn){ let newArr = []; for(let i = 0,len = this.length; i < len; i++){ fn(this[i]) && newArr.push(this[i]) } return newArr;}[1,2,3,4,5].myFilter((item) => { return item > 3})我们可以通过封装高阶函数来复用和简化我们的代码。柯里化柯里化是将一个多参数的函数转换成多个单参数的函数,这个函数会返回一个函数去处理下一个参数。也就是把fn(a,b,c)转换为newFn(a)(b)(c)这种形象。柯里化常见的应用有:参数复用、延迟计算。比如我们有个拼接接口地址的函数:function getUrl(service,context,api){ return service + context + api;}let loginUrl = getUrl(‘http://localhost:8080/’,‘auth’,’/login’) let logoutUrl = getUrl(‘http://localhost:8080/’,‘auth’,’/logout’)每次前两个参数的值都是一样,我们可以柯里化来封装下来达到参数复用:function curry(fn){ let args = Array.prototype.slice.call(arguments,1); return function(){ let innerArgs = Array.prototype.slice.call(arguments); let finalArgs = args.concat(innerArgs); if(finalArgs.length < fn.length){ //fn.length为函数的参数个数 return curry.call(this,fn,…finalArgs) }else{ return fn.apply(null,finalArgs) } }}var getAuthUrl = curry(getUrl,‘http://localhost:8080/’,‘auth’);let loginUrl = getAuthUrl(’/login’)let logoutUrl = getAuthUrl(’/logout’)组合函数组合函数类似于管道,多个函数的执行时,上一个函数的返回值会自动传入到第二个参数继续执行。比如我们替换一个url中的参数:function replaceToken(str){ return str.replace(/{token}/g,‘123455’)}function replaceAccount(str){ return str.replace(/{account}/g,‘xuriliang’)}replaceAccount(replaceToken(‘http://localhost/api/login?token={token}&account={account}’))我们可以利用这种嵌套的写法来实现,但如果嵌套过多,代码可读性就不是很好了。当然我们也可以在一个函数里分过程实现,不过这样函数就不符合单一原则了。利用函数组合我们可以这样写:function compose() { 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; }}compose(replaceToken,replaceAccount)(‘http://localhost/api/login?token={token}&account={account}’)组合函数使得我们可以使用一些通用的函数,组合出各种复杂运算。这也是函数编程中pointfree的概念。 ...

March 16, 2019 · 1 min · jiezi

JavaScript 高阶函数快速入门

翻译:疯狂的技术宅原文:https://medium.freecodecamp.o…本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章把函数以数据的形式去使用,并解锁一些强大的模式。高阶函数接受和/或返回另外一个函数的函数被称为高阶函数。之所以是高阶,是因为它并非字符串、数字或布尔值,而是从更高层次来操作函数。漂亮的元。使用 JavaScript 中的函数,你可以将它们存储为变量在数组中使用它们将它们指定为对象属性(方法)将它们作为参数进行传递将它们从其他函数中返回就像所有的其他数据一样。这是关键所在。函数操作数据字符串是数据sayHi = (name) => Hi, ${name}!;result = sayHi(‘User’);console.log(result); // ‘Hi, User!‘数字是数据double = (x) => x * 2;result = double(4);console.log(result); // 8布尔型是数据getClearance = (allowed) => allowed ? ‘Access granted’ : ‘Access denied’;result1 = getClearance(true);result2 = getClearance(false);console.log(result1); // ‘Access granted’console.log(result2); // ‘Access denied’对象是数据getFirstName = (obj) => obj.firstName;result = getFirstName({ firstName: ‘Yazeed’});console.log(result); // ‘Yazeed’数组是数据len = (array) => array.length;result = len([1, 2, 3]);console.log(result); // 3这5种类型是所有主流语言中的一等公民。是什么使他们成为一等公民的?你可以传递它们,将它们存储在变量和数组中,将它们用作计算的输入。你可以像使用任何数据一样去使用它们。函数也可以作为数据的形式去用在javascript中把函数用作数据的4种方式:把它们当作参数传给其它函数把他们设定成对象属性保存在数组中把它们设为变量的形式作为参数的函数isEven = (num) => num % 2 === 0;result = [1, 2, 3, 4].filter(isEven);console.log(result); // [2, 4]看看 filter 是如何用 isEven 来决定保留哪些数字的? isEven是一个函数,是另一个函数的参数。它由每个数字的 filter 调用,并使用返回值 true 或 false 来确定这个数字是否应该保留或丢弃。返回函数add = (x) => (y) => x + y;add 需要两个参数,但不是一次全部提供。这是一个只需要 x 的函数,它返回一个只需要y的函数。因为 JavaScript 允许函数成为返回值 —— 就像字符串、数字、布尔值那样。如果你愿意,仍然可以立即提供 x 和 y,并进行双重调用result = add(10)(20);console.log(result); // 30或者先 x 后 y:add10 = add(10);result = add10(20);console.log(result); // 30让我们回过头来看最后一个例子。 add10 是用一个参数调用 add 的结果。并控制台中输出它。add10 是一个函数,它接受一个 y 并返回 x + y。在你提供 y 之后,它会马上计算并返回你所要的最终结果。更高的可重用性高阶函数的最大好处可能是更高的可重用性。没有它,JavaScript 数组的主要方法 —— map,filter 和 reduce 将不存在!这是一个用户列表。我们将对他们的信息进行一些计算。users = [{ name: ‘Yazeed’, age: 25}, { name: ‘Sam’, age: 30}, { name: ‘Bill’, age: 20}];Map如果没有高阶函数,我们需要用循环来模仿 map 的功能。getName = (user) => user.name;usernames = [];for (let i = 0; i < users.length; i++) { const name = getName(users[i]); usernames.push(name);}console.log(usernames);// [“Yazeed”, “Sam”, “Bill”]或者我们可以这样做!usernames = users.map(getName);console.log(usernames);// [“Yazeed”, “Sam”, “Bill”]Filter在没有高阶函数的世界中,我们仍然需要循环来重新实现 filter 的功能。startsWithB = (string) => string .toLowerCase() .startsWith(‘b’);namesStartingWithB = [];for (let i = 0; i < users.length; i++) { if (startsWithB(users[i].name)) { namesStartingWithB.push(users[i]); }}console.log(namesStartingWithB);// [{ “name”: “Bill”, “age”: 20 }]或者我们可以这样做!namesStartingWithB = users .filter((user) => startsWithB(user.name));console.log(namesStartingWithB);// [{ “name”: “Bill”, “age”: 20 }]Reduce是的,reduce……没有更高阶的功能,实现不了那么多很酷的东西!!????total = 0;for (let i = 0; i < users.length; i++) { total += users[i].age;}console.log(total);// 75这个怎么样?totalAge = users .reduce((total, user) => user.age + total, 0);console.log(totalAge);// 75总结字符串、数字、bool、数组和对象可以存储为变量,数组和属性或方法。JavaScript 以相同的方式处理函数。这允许在其他函数上运行另外一个函数:高阶函数。Map、filter 和 reduce 是典型的例子 —— 并且使变换、搜索和列表求和等常见模式更加容易!本文首发微信公众号:jingchengyideng欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章欢迎继续阅读本专栏其它高赞文章:12个令人惊叹的CSS实验项目世界顶级公司的前端面试都问些什么CSS Flexbox 可视化手册过节很无聊?还是用 JavaScript 写一个脑力小游戏吧!从设计者的角度看 ReactCSS粘性定位是怎样工作的一步步教你用HTML5 SVG实现动画效果程序员30岁前月薪达不到30K,该何去何从7个开放式的前端面试题React 教程:快速上手指南 ...

March 15, 2019 · 2 min · jiezi

Javascript currying柯里化详解

面试题:实现add(1)(2)(3) //结果 = 6,题的核心就是问的js的柯里化先说说什么是柯里化,看过许多关于柯里化的文章,始终搞不太清楚,例如:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。大多数的blog都是这种说法,说实话我是懵逼的。我的理解是,curry是一个收集参数的方法,收集够了去执行函数。实现前我们先列一下要点1、收集参数(就像面试题多次执行多个参数)是的利用闭包2、每次执行参数有多有少例如add(1)(2,3)(4)3、我们需要知道什么时候参数够了 //如题 //add(1)(2)(3) //逻辑应该是这样add(1)执行收集参数1继续执行收集参数2依次类推直到收集完毕。 function curry(fn) { let arg = []; //用于收集参数 //做一个闭包https://segmentfault.com/a/1190000017824877 return function() { //每执行一次收集一次参数,为什么用concat是因为有时候后是多个参数(2,3) arg = arg.concat([…arguments]); //直到参数收集完成执行fn // 我们需要知道什么时候收集完了,条件就是curry参数fn的参数个数 fn.length //如果收集的参数个数大于等于fn的参数个数执行fn,如果没有递归执行 if (arg.length >= fn.length) { return fn(…arg) } // 参数没有收集完我们需要继续收集,递归 return arguments.callee } } // 测试一下 let testAdd = curry(add1) // console.log(testAdd(1)(2)(3)) // console.log(testAdd(1, 2)(3)) //console.log(testAdd(1)(2, 3))一不小心写完了!不过不能标题党,说好的详解,接下来我们解析一下网上大多数柯里化的实现代码function curry(fn) { function _c(restNum, argsList) { return restNum === 0 ? fn.apply(null, argsList) : function(x) { return _c(restNum - 1, argsList.concat(x)); }; } return _c(fn.length, []);}一眼看不明白没事,我们多看几眼。解析:1、curry也是接收一个参数(fn)这个是必然2、返回了一个函数,接收两个参数,fn.length和一个空数组这个好解释,我写的简版也说过了,fn.length是为了判断参数是否收集够了,参数传一个空数组其实也是闭包的一种实现,用来收集参数。 3、里边是一个三目判断,看着花里胡哨的没那么复杂,判断fn的参数个数如果是0,那就没必要收集了直接执行fn,至于fn.apply(null,argList)我很明白的大声说出来会用个apply就到处用吗?在我看来没有一分钱用,之所以用是因为argList是一个数组,正好apply正好支持第二个参数是数组,主要看起来很牛逼的样子。4、收集参数,fn参数个数不为零,每次收集fn函数参数的个数减一,直到等于0执行fn,这个就没有我写的通用了,我一次传俩就挂了。再来一个例子:bind方法实现Function.prototype.bind = function(context) { //返回一个绑定this的函数,我们需要在此保存this let self = this // 可以支持柯里化传参,保存参数 let arg = […arguments].slice(1) // 返回一个函数 return function() { //同样因为支持柯里化形式传参我们需要再次获取存储参数 let newArg = […arguments] console.log(newArg) // 返回函数绑定this,传入两次保存的参数 //考虑返回函数有返回值做了return return self.apply(context, arg.concat(newArg)) } } // 搞定测试 let fn = Person.say.bind(Person1) fn() fn(18)是的bind方法就是用的柯里化,bind实现详情请移步:https://segmentfault.com/a/11… ...

January 23, 2019 · 1 min · jiezi