本文已参与「新人创作礼」活动,一起开启掘金创作之路。
柯里化与new关键字
这次,将告诉大家,如何用函数实现柯里化,手写一个new关键字,手写步骤很详细,希望大家耐心阅读,带着自己的疑问去好好思考。
柯里化
柯里化是一种将一个接受多个参数的函数转变为多个接受一个参数的函数的技术
1. 柯里化的作用
柯里化的定义其实不难理解,说白了,就是向一个函数传参时,可以将参数分开来传,使得参数复用(降低了代码的重复量),提高代码适用性,但是降低了代码通用性,下面举个例子来更好的理解柯里化的定义和作用。
这里假设有一个ajax函数(自己随意编写的),它能传三个参数,分别是type(获取数据方式),url(网页地址),data(数据)。
Tips:这里假设的函数并不具有实际的功能,只是一个假设,用于表达我们要实现的目的,从而更好的去理解柯里化的作用
function ajax (type,url,data){
var xhr = new XMLHttpRequest()
xhr.open(type,url,true)
xhr.send(data)
}
如果我们想要调用这个函数对三个不同网站,以相同的获取方式,获取相同的数据进行操作,那么我们会这样调用
ajax('POST','www.baidu.com','name=ll')
ajax('POST','www.alibaba.com','name=ll')
ajax('POST','www.tencent.com','name=ll')
如上可知,三次调用,只有第二个参数网站的名字不同,第一个和第三个参数都是相同的。这样写重复的,如果有很多次不同的调用,就算复制粘贴,也还要一个一个改,就会很麻烦。
那么柯里化来救赎你了:假设我们有一个柯里化curry函数(因为本文是用函数来实现柯里化和手写new),这个curry函数能将刚才的ajax函数的传参方式变成一个一个传,就像下面这样
//这里是调用curry函数,将ajax函数传进去,将结果(返回的是一个函数)赋值给ajaxCurry
var ajaxCurry = curry(ajax)
//以post的方式向各个地址发请求
var post = ajaxCurry('POST')
//调用ajaxCurry,传入POST,那么这里POST,就像是一个固定值一样,将结果(返回的是一个函数)赋值给post
post('www.baidu.com','name=ll')//我们就可以拿着post函数,传剩下的两个值
post('www.alibaba.com','name=ll')
这里就会发现,通过柯里化,我们可以把一个函数拆分开来传参数,这就是柯里化的定义,将一个接受多个参数的函数转变为多个接受一个参数的函数
,上述例子中,传POST参数就只需要写一次var post = ajaxCurry('POST'),剩下的参数调用post函数需要传的参数变成两个了,柯里化就是以这样的方式,去降低代码的重复量,提高代码适用性,虽然降低了代码通用性,但是在一定的场合中是极其方便的。
2. 手写柯里化curry函数
手写柯里化,我们需要知道,柯里化实现的效果是怎么样的,
//z这里有一个add函数
function add(a,b){
return a + b
}
//我们希望手写的柯里化curry函数调用后,使得add函数可以按照下面几种方式调用
//var addCurry = curry(add,1,2)
//addCurry()//返回3
// var addCurry = curry(add,1)
// addCurry(2)//返回3
// var addCurry = curry(add)
// addCurry(1,2)//返回3
这里的实现方法很像显示绑定的bind方法,了解手写bind的小伙伴可以知道(不知道的话,那我们现在一起来知道一下),手写柯里化函数返回的也是个函数,我们一步一步来写,
var curry = function (fn) {//传的参数是个函数
return function () {//返回的也是个函数
}
}
接着,我们继续分析,curry函数返回的值是一个函数,赋值给addCurry,当我们以最后一种方法调用addCurry的时候,即addCurry(1)(2),addCurry(1)后面还能接个(2),说明addCurry(1)执行完之后,返回的还是一个函数。所以在写curry返回的的函数里面,还要返回一个函数,如下:
var curry = function (fn) {//传的参数是个函数
return function () {//返回的也是个函数
return function () {
}
}
}
这里可能会有点绕,大家需要理清的是,首先,我们现在写的是curry函数而不是addCurry函数,其次,curry返回的函数赋值给addCurry,所以addCurry是上述代码中第一个return出来的函数,最后,addCurry(1)后面还能接个(2),说明addCurry(1)执行完之后,返回的还是一个函数,所以在手写的curry函数中,第一个返回的函数里面又返回了一个函数,就是第二个return返回出来的function。
理解了上述问题之后,我们要知道,如果要传很多个参数,比如addCurry(1)(2)(3)(4),那么我们不可能去改动这个curry函数添加好几个return,所以这里做个小小的优化,
var curry = function (fn) {//传的参数是个函数
var args = [].slice.call(arguments,1)
//arguments是curry函数的参数,通过数组slice方法拿到add 该有的参数
return function () {//返回的也是个函数
//newArgs一定代表的是add该有的完整的参数
var newArgs = args.concat([].slice.call(arguments))
return fn.apply(this,newArgs)
}
}
我们来理解一下上面这段代码,考虑到如果我们用的是下面这种方式调用
var addCurry = curry(add,1)
addCurry(2)
那么这个优化代码其实就是为了获取到curry的形参fn的全部完整的参数,
首先是var args = [].slice.call(arguments,1),arguments是函数自带的一个变量,它是一个类数组,里面的值是当前函数的所有参数
,因为,arguments是个类数组,它是不具备数组的slice方法的,所以我们写成[].slice.call(arguments,1),这里是将数组的slice方法里面的this,通过call的显示绑定,绑定到了arguments上,这样就能把arguments用slice方法切割了,因为arguments里面的值是当前函数的所有参数,所以这里的arguments是curry函数的参数,而curry的第一个参数是形参fn(即传入的add函数),所以要从1开始切割(这是slice的使用方法,可自行去了解),所以就有了 var args = [].slice.call(arguments,1)。这里获取到了curry函数除了第一个传值的函数以外的所有参数,在curry(add,1)中就是获取到了一个数组,里面有一个1,即 [1], 并将其赋值给变量args。
其次是var newArgs = args.concat([].slice.call(arguments)),方法与刚才的差不多,不同的是,这里的arguments代表的参数已经是return出来的function的参数了,即在addCurry(2)中获取到了一个数组,里面有一个2,即 [2],将args数组通过数组的concat方法将获取到的值合并成一个数组,[1,2],并将其赋值给newArgs,所以newArgs一定代表的是fn(即传入的add函数)该有的完整的参数,然后我们只需要将newArgs传入fn中执行fn即可,这里要注意,add定义在window全局下,而我们将add函数传入curry中,add中的this就不指向window了,但是返回的function会被赋值给其它变量,且在全局调用,我们将fn的this用apply显示绑定,将其绑定到返回的function的this下,使其指向全局。这里使用apply方法,是因为newArgs是个数组,而apply接收的参数也正好是数组。
写到这里,柯里化最基本的功能实现了,我们一起看一下运行结果:
这是第一种调用方法,打印是3
这是第二种调用方法,打印还是3
这是第三种调用方法,打印还是3
其实在这整个例子中还有美中不足,因为还没有办法将其写成addCurry(1)(2)的调用形式,所以我们迎来了终极优化。
前面写的curry函数已经有了雏形,其实是一个辅助函数,接下来的终极优化就是柯里化该有的样子。我们先把上面的函数拿下来,方便阅读。
var curry = function (fn) {
var args = [].slice.call(arguments,1)
return function () {
var newArgs = args.concat([].slice.call(arguments))
return fn.apply(this,newArgs)
}
}
那么首先,我们要实现的最后一个效果是实现addCurry(1)(2)调用,他有两个括号,这就涉及到递归
function KeLi (fn,length) {
length = length || fn.length
var slice = [].slice
return function () {
}
}
这里逻辑其实很简单,就是多加一个length形参,用来判断长度,如果没有传参,那就默认为fn的length,没错,函数也是有长度属性的,该属性是表示该函数接受形参的个数
将数组的slice方法赋值给slice,这样的话[].slice.call(arguments)就可以简写成slice.call(arguments)了,最后仍然是返回一个函数,接着继续看代码:
function KeLi(fn,length){
length = length || fn.length
var slice = [].slice
return function(){
if(arguments.length<length){
var combined = [fn].concat(slice.call(arguments))//[fn,a,b]
return KeLi(curry.apply(this,combined),length - arguments.length)
}else{
return fn.apply(this,arguments)
}
}
}
这里就是在返回的函数里判断,
如果返回的函数接收的参数个数小于length,那么就将fn(传入的函数,此处仍为前文的add函数)放入数组中(用中括号括起来),在调用数组的concat方法,将参数合并(此处与前文方法相同)得到数组[fn,a,b]并将其赋值给combined,再返回调用自身(递归),但是传的参数不一样,第一个传的是curry函数执行的结果(结果返回的是个函数),curry函数中将combined作为参数转入,第二个传的是length - arguments.length,即剩下的参数个数(也是长度)。这里将curry用apply方法更改this指向,其实是与前文作用相同,目的就是为了使add函数即使柯里化,但是其this始终是指向全局的。这里用到递归就会不断循环递归下去,直到arguments.length不小于length,才会执行else中的语句,该语句与前文curry中的相同。
这里涉及到递归算法,很容易搞混,需要大家自己花时间慢慢的去理清楚(我表示很想帮助你们理解,但是递归这种算法复杂起来一定要大家自己在脑子里去将代码运行一遍,这样才能理解透彻,大家可以拿草稿纸演算),希望大家能多点耐心,习惯里就会发现很容易理解的,最后我们来看一下代码效果。
结果很成功,这就是柯里化函数的完整版,下篇将给大家带来,用柯里化实现手写new关键字,码字不易,点个赞支持一下吧。