前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • JavaScript Part 1

    • Ajax总结篇
    • Canvas 绘制八大行星
    • DOM编程之API学习总结篇
    • JavaScript事件机制
    • JavaScript代码片段100个
    • JavaScript作用域分析总结
    • JavaScript原型链回顾
    • JavaScript及jQuery中的各种宽高属性图解
    • JavaScript工程项目的一系列最佳实践
    • JavaScript常用API合集
    • JavaScript数组、字符串、对象常用方法
    • JavaScript数组方法总结篇
    • JavaScript深浅拷贝
    • JavaScript运动框架之速度时间版本
    • JavaScript运行机制Event Loop
    • JavaScript防抖节流原理
    • Javascript中的复制粘贴功能
    • Javascript数组详解
    • OOP之原型与原型链
    • Object.defineProperty详解
    • V8源码浅析JS数组常见方法
    • await 在 forEach 中不生效解决方案
    • iframe+表单跨域提交POST请求
    • javascript笔记总结篇
    • 业务中处理数据结构常用的JS方法
    • 作用域
    • 你真的掌握变量和类型了吗
    • 前后端分离之数据Mock
    • 原型与原型链
    • 原生JS补给(上)
    • 如何写出一个惊艳面试官的深拷贝
    • 带你填一些JS容易出错的坑
    • 彻底弄懂 JavaScript 执行机制
    • 执行上下文 执行栈
    • 正则回顾总结
    • 正则基础知识
    • 正则完整篇
    • 正则表达式
    • 浅析Promise原理
    • 浅谈JavaScript中的异步处理
    • 深拷贝 vs 浅拷贝
    • 聊一聊typeof instanceof 实现原理.
    • 聊一聊闭包
    • 高阶函数map reduce filter
  • JavaScript Part 2

  • CSS

  • HTML

  • Jquery

  • ES6

  • 小程序

  • Vue

  • React

  • 深入React

  • React Native

  • NodeJS

  • Angular

  • TypeScript

  • Webpack

  • 浏览器

  • 移动端

  • 前端工程化

  • Electron

  • HTTP

  • Nginx

  • Linux

  • 数据结构与算法

  • LeetCode算法题

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

进阶柯里化|博客系列

# 只要一个实参

# unary函数

先来看一个奇怪的例子:

// example1
["1","2","3"].map( parseFloat );
// [1,2,3]

["1","2","3"].map( parseInt );
// [1,NaN,NaN]
@前端进阶之旅: 代码已经复制到剪贴板

在上面的例子中,我想要将一组字符串全部设置成数字类型,但是在调用parseInt的时候,却出现了这样怪异的事情。这是为什么呢?

首先我们来说一下parseInt(str,radix)这个函数,它接受两个参数,第一个是要被解析的字符串,第二个为可选参数,表示要解析的数字的基数。该值介于 2 ~ 36 之间,比如:

parseInt("10");			//返回 10
parseInt("19",10);		//返回 19 (10+9)
parseInt("11",2);		//返回 3 (2+1)
@前端进阶之旅: 代码已经复制到剪贴板
  • 当参数 radix 的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。
  • 如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

而在案例1中,由于我们在调用map的时候,函数时会传入三个实参:value、index 和 list。parseInt()又会接收2个参数,所以每次都相当于是将value和index传入进去了。这样就造成后面2个NaN的情况。

根据上面,我们可以看到,在实际开发中,我们会面临这样一个问题:在设计一个实用函数传入一个函数,而这个实用函数会把多个实参传入函数,但可能你只希望你的函数接收单一实参。如上面的parseInt()函数,我在调用它的时候希望它每次只接收一个参数。

根据上面的需要,我们是不是可以来实现这么一个简单的实用函数,它包装一个函数调用,让这个函数在每次调用的时候只接收一个参数:

function unary(fn) {
	return function onlyOneArg(arg) {
		return fn(arg)
	}
}

// ES6
var unary = fn => 
							arg => 
										fn(arg);
@前端进阶之旅: 代码已经复制到剪贴板

很简单的一层封装,也很好理解。

现在我们用它来配合上面的案例1:

["1","2","3"].map( parseFloat );
// [1,2,3]

["1","2","3"].map( parseInt );
// [1,NaN,NaN]

["1","2","3"].map( unary( parseInt ) );
// [1,2,3]
@前端进阶之旅: 代码已经复制到剪贴板

# sum案例

了解了上面的unary函数之后,我们来看一个复杂一些的案例。还是用map函数来进行举例,不过要用到我们在第二章讲到的松散型柯里化looseCurry。

        function looseCurry(fn, arity = fn.length) { // 松散型柯里化
            return (function nextCurried(prevArgs) {
                return function curried(...nextArgs) {
                    var args = prevArgs.concat(nextArgs)
                    if (args.length >= arity) {
                        return fn(...args)
                    } else {
                        return nextCurried(args)
                    }
                }
            })([])
        }
        function sum(...args) {
            var sum = 0;
            for (let i = 0; i < args.length; i++) {
                sum += args[i];
            }
            return sum;
        }
        let adder = looseCurry(sum, 2) // 第一步 传2表示至少接收2个参数才可以
        let arr = [1, 2, 3, 4, 5]
        let arr2 = arr.map(adder(3)) // 第二步
        console.log(arr2)
        // 生成的结果很有意思
        // ["41,2,3,4,5", "61,2,3,4,5", "81,2,3,4,5", "101, ...
@前端进阶之旅: 代码已经复制到剪贴板

大家可以先将这段代码在本地跑一下,并思考为什么会出现这样的情况?🤔

其实原理很简单:

在第一步的时候,创建adder函数,只有在接收2个及以上参数才会运行sum(),

第二步的时候,由于map()会传入3个实参:分别是value,index和list,而松散型的柯里化是可以接收比预期(这里也就是2)多的参数的。

所以在每次执行sum函数的时候,实际传入的都是3个参数,比如arr执行第一次的时候:

第一次传入:
firstArg: 3,   // 调用adder()时传入的3
value: 1, // 数组第一项的值
index: 0, // 数组第一项的索引
list: [1, 2, 3, 4, 5] // 整个数组
// 前面3项相加为数字4,之后数字4与数组做字符串相加
=> 3 + 1 + 0 + [1, 2, 3, 4, 5]
=> "41,2,3,4,5"

第二次传入
firstArg: 3,
value: 2,
index: 1,
list: [1, 2, 3, 4, 5]
@前端进阶之旅: 代码已经复制到剪贴板

此时,使用我们的unary函数就可以解决上面的问题了:

let arr2 = arr.map( unary( adder(3) ) )
// [4,5,6,7,8]
@前端进阶之旅: 代码已经复制到剪贴板

# 传一个返回一个

# identity函数

说到只传一个实参的函数,在函数式编程工具库中有另一种通用的基础函数:该函数接收一个实参,然后什么都不做,原封不动地返回实参值。

function identity(v) {
	return v;
}

// ES6 箭头函数形式
var identity =
	v =>
		v;
@前端进阶之旅: 代码已经复制到剪贴板

举个例子,想象一下你要用正则表达式拆分(split up)一个字符串,但输出的数组中可能包含一些空值。我们可以使用 filter(..) 数组方法(下文会详细说到这个方法)来筛除空值,而我们将 identity(..) 函数作为 filter(..) 的断言:

var words = "   Now is the time for all...  ".split( /\s|\b/ );
words;
// ["","Now","is","the","time","for","all","...",""]

words.filter( identity );
// ["Now","is","the","time","for","all","..."]
@前端进阶之旅: 代码已经复制到剪贴板

# 转换函数的妙用

另一个使用 identity(..) 的示例就是将其作为替代一个转换函数(译者注:transformation,这里指的是对传入值进行修改或调整,返回新值的函数)的默认函数:

function output(msg,formatFn = identity) {
	msg = formatFn( msg );
	console.log( msg );
}

function upper(txt) {
	return txt.toUpperCase();
}

output( "Hello World", upper );		// HELLO WORLD
output( "Hello World" );			// Hello World
@前端进阶之旅: 代码已经复制到剪贴板

上面的例子,相当于是给output函数一个默认的函数,若是没有传的话,则原封不动的返回传进来的msg。

# 恒定参数

# Certain API

Certain API 禁止直接给方法传值,而要求我们传入一个函数,就算这个函数只是返回一个值。JS Promise 中的 then(..)方法就是一个 Certain API。

then(..)中必须要传入一个函数,而有时候我们可能不需要传入一个有实际用处的函数,而是直接返回then在上一步中获取到的值。

或许你可以用ES6的箭头函数解决这个问题:

p1.then( foo ).then( () => p2 ).then( bar )
@前端进阶之旅: 代码已经复制到剪贴板

在第二个then中直接将p2返回。

# constant函数

我们可以来构造一个实用函数来实现上面的功能:

function constant(v) {
	return function value(){
		return v;
	};
}

// or the ES6 => form
var constant =
	v =>
		() =>
			v;
@前端进阶之旅: 代码已经复制到剪贴板

constant函数功能也很简单,返回传入的参数。

对比:

p1.then( foo ).then( () => p2 ).then( bar );

// 对比:

p1.then( foo ).then( constant( p2 ) ).then( bar );
@前端进阶之旅: 代码已经复制到剪贴板

两种方式都可以解决Certain API的问题。但是我更建议用第二种方式,该箭头函数返回了一个来自外作用域的值,这和 函数式编程的理念有些矛盾。在后面“减少副作用”中会提到该行为的不足。

# 扩展在参数中的妙用

在第一章中,我们提到了形参数组解构:

function foo( [x,y,...args] ) {
	console.log(x, y, args)
	// 1, 2, [3]
}

foo( [1,2,3] );
@前端进阶之旅: 代码已经复制到剪贴板

当函数必须接收一个数组,而你却想把数组内容当成单独形参来处理的时候,这个技巧十分有用。

然而,有的时候,你无法改变原函数的定义,但想使用形参数组解构。

比如下面这个例子:

function foo(x, y) {
	console.log( x + y );
}

function bar(fn) {
	fn( [ 3, 9 ] );
}

bar( foo );
// 3,9undefined
@前端进阶之旅: 代码已经复制到剪贴板

由于fn会将实参[3, 9]作为x传入,那么y就是undefined,所以达不到我们期望的效果。

在这种情况下,我们可能想要改变bar()函数的行为,将foo([3, 9])改为foo(…[3, 9]),这样就能将 3 和 9 分别传入 foo(..) 函数了。

为了调整一个函数,让它能把接收的单一数组扩展成各自独立的实参,我们可以定义一个辅助函数:

function spreadArgs(fn) {
	return function spreadFn(argsArr) {
		return fn( ...argsArr );
	};
}

// ES6 箭头函数的形式:
var spreadArgs =
	fn =>
		argsArr =>
			fn( ...argsArr );
@前端进阶之旅: 代码已经复制到剪贴板

注意⚠️:

在我参考的教材中,将这个辅助函数叫为spreadArgs,但一些库,比如 Ramda,经常把它叫做 apply(..)。

现在我们可以使用 spreadArgs(..) 来调整 foo(..) 函数,使其作为一个合适的输入参数并正常地工作:

bar( spreadArgs( foo ) );			// 12
@前端进阶之旅: 代码已经复制到剪贴板

本质上,spreadArgs(..) 函数使我们能够定义一个借助数组 return 多个值的函数,不过,它让这些值仍然能分别作为其他函数的输入参数来处理。

一个函数的输出作为另外一个函数的输入被称作组合(composition),这个在后面的章节中会详细说明。

← 高阶函数偏函数和柯里化 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • JavaScript Part 1

    • Ajax总结篇
    • Canvas 绘制八大行星
    • DOM编程之API学习总结篇
    • JavaScript事件机制
    • JavaScript代码片段100个
    • JavaScript作用域分析总结
    • JavaScript原型链回顾
    • JavaScript及jQuery中的各种宽高属性图解
    • JavaScript工程项目的一系列最佳实践
    • JavaScript常用API合集
    • JavaScript数组、字符串、对象常用方法
    • JavaScript数组方法总结篇
    • JavaScript深浅拷贝
    • JavaScript运动框架之速度时间版本
    • JavaScript运行机制Event Loop
    • JavaScript防抖节流原理
    • Javascript中的复制粘贴功能
    • Javascript数组详解
    • OOP之原型与原型链
    • Object.defineProperty详解
    • V8源码浅析JS数组常见方法
    • await 在 forEach 中不生效解决方案
    • iframe+表单跨域提交POST请求
    • javascript笔记总结篇
    • 业务中处理数据结构常用的JS方法
    • 作用域
    • 你真的掌握变量和类型了吗
    • 前后端分离之数据Mock
    • 原型与原型链
    • 原生JS补给(上)
    • 如何写出一个惊艳面试官的深拷贝
    • 带你填一些JS容易出错的坑
    • 彻底弄懂 JavaScript 执行机制
    • 执行上下文 执行栈
    • 正则回顾总结
    • 正则基础知识
    • 正则完整篇
    • 正则表达式
    • 浅析Promise原理
    • 浅谈JavaScript中的异步处理
    • 深拷贝 vs 浅拷贝
    • 聊一聊typeof instanceof 实现原理.
    • 聊一聊闭包
    • 高阶函数map reduce filter
  • JavaScript Part 2

  • CSS

  • HTML

  • Jquery

  • ES6

  • 小程序

  • Vue

  • React

  • 深入React

  • React Native

  • NodeJS

  • Angular

  • TypeScript

  • Webpack

  • 浏览器

  • 移动端

  • 前端工程化

  • Electron

  • HTTP

  • Nginx

  • Linux

  • 数据结构与算法

  • LeetCode算法题

  • 综合