su37josephxia/frontend-interview

Day10 - 闭包与科里化、偏应用函数的关系

su37josephxia opened this issue · 30 comments

闭包能够让变量内存永驻,柯理化是使用部分参数生成函数的方式,闭包为柯理化的实现提供前提条件

🌰eg:

//定义
function getTypeFn(type) {
    return (obj) => Object.prototype.toString.call(obj) === `[object ${type}]`
}
//生成新的函数
let isArray = getTypeFn("Array");
let isNumber = getTypeFn("Number");
//使用
console.log(isArray([])); // true
console.log(isNumber(1)); // true

如果这里用getType(type,obj)也是能够达到同样目的,但是为什么需要用方法包裹一层呢?
从表面上看到

  1. 闭包让type变量持久化到getTypeFn作用域中。
  2. 柯理化函数让减少参数调用

总结:闭包是柯理化的前提条件,目的是抽取相对固定的变量,让函数调用更加语义化(也就是声明式编程)。

实际开发中可以用remda.js(全部库都柯理化)或者lodash.js来解决实际业务问题。

  • 闭包是指能够读取其他函数内部变量的函数。

  • 柯理化是将一个多参函数转化为单参函数。

  • 偏应用函数是为一个多元函数预先提供部分参数,从而在调用时可以省略这些参数。

  • 闭包是柯理化、偏应用函数的理论基础,柯理化、偏应用函数是闭包的具体应用形式之一。

  • function add(a,b,c){

      return a+b+c 
    

    }

    add(1,2,3)=6

    //柯里化

    addCurry(1)(2)(3)

    //偏应用

    addPartial(1,2)(3)或addPartial(1)(2,3)

柯里化

柯里化指的是: 将接受n个参数的一个函数转化成接受1个参数的n个函数。

举个例子:

function info(country, province, city) {
  console.log(country + "-" + province + "-" + city);
}

我们想要打印城市信息,对于某个省的城市我们需要重复的输入country,province这两个字段,此时我们可以对该函数进行“柯里化”改造:

function info(country) {
  return function (province) {
    return function (city) {
      console.log(country + "-" + province + "-" + city);
    };
  };
}
const province = info("**")("浙江省");
province("杭州市");
province("温州市");

这么做最大的好处是提高了代码的复用性,我们可以提前缓存一些参数,避免重复输入。

偏函数

偏函数指的是:将接受n个参数的单个函数任意固定a个参数,返回接受剩余n-a个参数的函数。

偏函数和柯里化属于一类操作,区别在于:

  • 柯里化强调函数只能接受“单参数”,有n个参数就要拆解为n个单参数函数;

  • 偏函数更加“随意”,将接受n个参数的函数一分为二,任意固定一个或多个参数。

function info(country) {
  return function (province, city) {
    console.log(country + "-" + province + "-" + city);
  };
}
 
info("**")("浙江省", "杭州市");
info("**")("浙江省", "温州市");

柯里化和偏函数本质没什么区别,只是约束不同。能够实现这类操作的核心有两点:

  • 在于“自由变量”的不回收性,由于垃圾回收不去销毁闭包中引用的自由变量,我们才能缓存部分参数;
  • 在于JS使用静态的词法作用域,词法作用域的划分和查询根据”代码书写“的位置来决定。

柯里化

把接受n个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果是新函数的技术,称之为柯里化。一般的函数返回的就是一个值,而柯里化函数可以接收多个参数“(1)(2)”,并且返回一个新的函数,而这个新的函数接收剩下的参数。

偏应用函数

又称之为部分应用函数,指一个函数有n个参数,为其提供a个参数,返回一个接受n-a个参数的函数。

柯里化和偏应用都是需要利用到闭包的特性,缓存部分参数,使其得到最终想要的效果,如果没有闭包也就没有柯里化和偏应用。

关系
柯里化和偏函数的动机都是为了让函数"记住"一部分的参数,通过封装的方式,更好地为实际的使用场景服务,而封装的方式,用的都是闭包
柯里化
将接受 n 个参数的 1 个函数改为只接受一个参数的 n 个互相嵌套的函数
偏函数
偏函数并不像柯里化那样,十分强调单入参的概念,它的目标仅仅是把入参分解为两个部分,任意固定一个或多个参数。比起柯里化,它更加地随意一些
作用
参数复用,对部分参数的复用,无需重复添加。
提前返回,提前返回可以返回的返回值并可以继续进行参数的接受。
延迟计算,累计传入的参数,最终返回计算结果

  • 偏应用、柯里化函数是闭包应用的一种形式。
  • 偏函数是 JS 函数柯里化运算的一种特定应用场景。简单描述,就是把一个函数的某些参数先固化,也就是设置默认值,返回一个新的函数,在新函数中继续接收剩余参数,这样调用这个新函数会更简单
  • 偏函数的部分参数是固定的;

偏函数的一种应用场景,数据类型判断。

var isType = function (type) {
  return function (obj) {
    console.log(
      'Object.prototype.toString.call(obj)',
      Object.prototype.toString.call(obj)
    )
    return Object.prototype.toString.call(obj) == '[object ' + type + ']'
  }
}
const yinyin = isType('Array')([])
const yinyin2 = isType('Object')({})
const curry = function(fn){
    return function curryFn(...args){
        if(args.length<fn.length){
            return function(...newArgs){
                return curryFn(...args,...newArgs)
            }
        }else{
            return fn(...args)
        }
    }
}

let add = (a,b,c)=>a+b+c
// 柯里化
add = curry(add)

console.log(add(1)(2)(3)) // 输出 6

偏函数是对普通一般的函数的处理,他改变了函数的调用方式。

原来,一个函数我们调用它给它传入参数,他就立马执行了它的函数体。但是经过偏函数处理的函数,它是通过调用分步分几次的去接收参数,当所有参数到位了之后,才会真正的去执行函数体。

而在这个过程中,我们就需要存储每一次给它传入的参数,这就需要用到闭包了,这就是闭包和偏函数的关系。

而柯里化呢,它只是一种更为特殊的偏函数处理,它也是分步的让函数可以去接受它的参数,但它更严格的限制了你每一次传入参数,只能传一个。

const add = (x,y,z) => x + y + z
const curriedAdd = x => y => z => x + y + z // 柯里化,基于高阶函数,高阶函数基于闭包

20220112更新:
固定了某某参数的函数,就是偏某某参数的函数。比如,我们在业务层面对通用函数又做了层封装,新函数如果没有增加任何逻辑,而仅仅是固定了某些参数,就是原函数的偏函数。

const partial = (f, ...args) =>(...moreArgs)=> f(...args,...moreArgs) // 偏函数,基于高阶函数,高阶函数基于闭包

偏应用函数:在计算机科学中,部分应用指的是将一系列参数固定在一个函数上,生成另一个更小元(函数参数的个数)的函数
例如:
function add(a,b){
return a + b;
}
add(1,2); // 3
假如有一个函数partial函数可以做到局部应用,那么使用方法就是
const addPartial = partial(add, 2);
const res = addPartial(4) // res = 6
partial的实现大概为
var _ = {};

function partial(fn) {
console.log('1110', arguments);
var args = [].slice.call(arguments, 1);
return function() {
var position = 0, len = args.length;
for(var i = 0; i < len; i++) {
args[i] = args[i] === _ ? arguments[position++] : args[i]
}
while(position < arguments.length) args.push(argumetns[position++]);
return fn.apply(this, args);
};
};

function add(a, b){
return a + b;
}

const addOne = partial(add, _, 1);
const result = addOne(10);
console.log(result); // 11
偏应用函数也是利用了闭包的原理return了一个函数出来,保存了最开始传入函数的参数
偏应用实际上是固定一个函数的一个或多个参数,也就是将传入n个参数的函数转换成传入n - x 个参数的函数

而柯里化是将一个多参数函数转换成多个单参数的函数

两者实际上都是使用了闭包的原理,在内存中保存了每一个传入的参数,已简化函数调用时的传参

二者都用了闭包的特性,在 return 的函数里拿到了外部函数的变量,缓存起来之后再使用

  • 柯里化 是指将使用多个参数的函数转换为一系列使用一个参数的函数的技术
let curry = reg => {
  return str => {
    return reg.test(str)
  }
}
let hasSpace = curry(/\s+/g/) //是否有空格
hasSpace('hello world') //true
hasSpace('asdfkjasdf') //false
  • 偏函数 是固定一个函数的一个或多个参数,也就是将一个 n 元函数转换成一个 n - x 元的函数
//使用bind
let add = (x, y, z) => x + y + z
let rst = add.bind(null, 1, 2)
rest(3) //6
rest(10) //13
  • 结论:在柯里化和偏函数中都是将先传入函数的参数保存在闭包中,防止被垃圾回收,不可修改并可以在外部访问使用

函数柯里化的含义是将有多个入参的方法变成有一个入参但是层层嵌套的方法,

偏函数就是将有多个入参的方法变成有若干个入参,层层嵌套的方法。

二者的底层原理都是闭包,主要区别是参数的个数不同,偏函数不强制入参必须为一个。

// 柯里化

function sum (a, b) {
 return a + b
}

function sum(a) {
  return function(b) {
  return a + b;
  }
}
  • 科里化
    维基百科中定义:科里化(英文:Currying): 又译为卡瑞化或加里化;
    是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

就是将具有多个参数的函数转换为一个单参数的函数链,这个科里化的过程;

  • 偏函数
    偏函数是指使用一个函数并将其应用一个或多个参数,但不是全部参数;他会创建一个接收剩余参数的函数
  • 柯里化:又称部分求值,一个柯里化参数首先会接受一些参数,接受这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待到合适的时机一起求值。
// 通用的柯里化
var currying = function(fn) {
  var args = [];
  return function() {
    if(arguments.length==0) {
      return fn.apply(this,args);
    } else {
      Array.prototype.push.apply(args,arguments);
      return arguments.callee;
    }
  }
}

var cost = (function(){
  var money = 0;
  return function() {
    for(var i = 0,len = arguments.length;i<len;i++) {
      money +=arguments[i];
    }
    return money;
  }
})()
var cost = currying(cost);
cost(100);
cost(200);
cost(20);
cost(10);
console.log(cost()); // 输出330
  • 偏函数:偏函数是指使用一个函数并将其应用一个或多个参数,但不是全部参数;他会创建一个接收剩余参数的函数
  • 二者的底层原理都是闭包,主要区别是参数的个数不同,偏函数不强制入参必须为一个。

柯里化和偏函数都是用于将多个参数函数,转化为接受更少参数函数的方法。传入部分参数后,处于中间状态的函数能够做为固定值进行复用。可是其中不一样之处在于:

柯里化是将函数转化为多个嵌套的一元函数,也就是每一个函数只接受一个参数。
偏函数能够接受不仅一个参数,它被固定了部分参数做为预设,并能够接受剩余的参数。

函数的柯里化是指将一个n个参数的函数改造为只接受一个参数的n个嵌套函数,而偏函数则是将一个n个参数的函数改造为其中一个或几个参数(但并非所有)固定,返回接受剩余参数的函数。柯里化函数与偏函数实现方式比较类似,只是偏函数不强制入参必须为一个,但其实现原理都是闭包。

  • 柯里化

    柯里化是通过闭包使用场景中的返回函数使用场景,将接受 n 个参数的 1 个函数改为只接受一个参数的 n 个互相嵌套的函数。

    普通函数

    function getPerson(name, age, high){
      return name + age + '岁' + high + '高';
    }
    
    getPerson('song','33','184');     //"song33岁184高"

    柯里化函数

    function getPerson(name){
     return function (age) {
       return function (high) {
         return name + age + '岁' + high + '高';
       }
     }
    }
    
    getPerson('song')('33')('184'); //"song33岁184高"
  • 偏函数

    它其实就是 "随意" 的柯里化,比方说是有10个参数的函数,进行柯里化改造以后,就是一个嵌套10层的每次只传入1个参数的函数,而偏函数改造以后,你可以只固定3个入参,然后返回一个需要7个入参的函数,偏函数并不像柯里化那样,十分强调单入参的概念,它的目标仅仅是把入参分解为两个部分,比起柯里化它更加地随意一些。

    function getPerson(name,age){
      return function (high) {
        return name + age + '岁' + high + '高';
      }
    }
    
    let person = getPerson('song','33');
    person('182');     //"song33岁182高"
    person('183');     //"song33岁183高"
    person('184');     //"song33岁184高"

柯里化函数是将接收N个参数的函数包装成为可以接收单一参数的N个函数进行调用
偏函数应用与柯里化函数相似,区别是偏函数应用会固定部分参数,将剩下的参数留给用户来调用
这两者都是利用了闭包的特点,就是对函数参数进行保存,再将闭包函数返回给用户调用

柯里化和偏函数都是利用了闭包, 持久的保存参数,每次调用如果总共的参数数量不够,则返回一个保存了之前的参数的新的函数,这个函数里永久保存了之前传入的参数,实现了参数的复用。柯里化可以把参数拆分多次,偏函数只拆分一次。

// 生成柯里化函数
function curry(fn, ...__args) {
             if (__args.length === fn.length) {
                    return fn(...__args)
             } else {
                   return (...args) => {
                         return curry(fn, ...__args, ...args)
                   }
              }
       }
}

里面将之前传进来的参数在新的函数中引用,产生了闭包。然后递归调用直到参数数量足够。

// 偏函数
function partial(fn, ...__args) {
       return (...args) => {
             fn(...__args, ...args)
       }
}

偏函数只做一次拆分,但是也将传入的参数在新的函数中引用,产生了闭包。这一点和 bind 的实现很像,可以说 bind 也是一种偏函数。

// bind
function bind(fn, ctx) {
       return (...args) => {
             return fn.apply(ctx, ...args)
       }
}
  1. 闭包可以通过对函数局部变量的引用,使变量指向的内存区域不会被回收;
  2. 柯里化是将接收多个参数的函数转换成接收单一参数的的多个嵌套一元参数的调用函数,利用闭包维持对多个一元参数的引用,在最终调用时结合计算出结果;
  3. 偏函数可以将多参数函数的部分参数作为预设值,返回一个新函数用来传递剩余的参数,再调用时,利用闭包特性对预设参数的持续引用,结合预设参数和剩余参数求得最终的调用结果;
  4. 闭包是柯里化和偏函数的理论基础,柯里化和偏函数是闭包的两个应用场景,柯里化和偏函数都是将多参数函数通过闭包维持单个参数或多个参数引用的特性转化为单个参数或更少参数的函数

柯里化
将接受n个参数的单个函数转换为接受单个参数的n个函数。
比如下面这个场景:
function info(country, province, city) { console.log(country + "-" + province + "-" + city); }
我们将接受三个参数"country,province,city"的函数转换为三个只接受单个参数的函数,然后根据需要随时生成一个已经内置”部分参数“的函数。
这么做最大的好处是提高了代码的复用性,我们可以提前缓存一些参数,避免重复输入。
function info(country) { return function (province) { return function (city) { console.log(country + "-" + province + "-" + city); }; };}const province = info("**")("浙江省");province("杭州市");province("温州市");
偏应用函数
当一个函数参数列表有多个参数,如果多次调用这个函数,而其中某个或者某几个参数不变,就可以将这几个参数绑定在函数上,生成一个函数表达式(其实就是函数),将这个表达式的值赋给一个变量,计算这个表达式的值的时候传入第二个参数,不同参数

  • 柯里化是把一个有n个参数的函数转换成n个嵌套的函数,每个函数只接受一个参数,并返回一个新函数。也就是把f(a,b,c)转化为f(a)(b)(c)。
function multiply(a) {
    return (b) => {
        return (c) => {
            return a * b * c
        }
    }
}
multiply(1)(2)(3) // 6
  • Partial Application(偏函数应用)很容易和函数柯里化混淆,它是指使一个有N个参数的函数转换成一个或多个参数的函数,封装返回一个函数,该函数可以接受剩余的参数。
function partial(fn, len = fn.length) {
	return _partial.call(this, fn, len)
}

function _partial(fn, len, args) {
	return function(...params) {
  	const arguments = [...args, ...params];
    if (arguments.length < len) {
    	_partial.call(this, fn, arguments.length, ...arguments)
    } else {
    	fn.apply(this, arguments)
    }
  }
}

他们都需要使用闭包去保存之前的参数

答案

函数柯里化和偏函数的实现中都用到了闭包

柯里化

函数柯里化,就是可以将一个接受多个参数的函数分解成多个接收单个参数的函数的技术,直到接收的参数满足了原来所需的数量后,才执行原函数。

var add = function (x,y) {
  return x + y;
}
var add = function (x) { //柯里化
  return function (y) {
    return x + y;
  }
}

偏函数

偏函数是函数柯里化的一种特定应用场景。简单描述,就是把一个函数的某些参数先固化,也就是设置默认值,返回一个新的函数,在新函数中继续接收剩余参数,这样调用这个新函数会更简单。

const curriedSum = math => eng => geo => math + eng + geo
const partialSum = math => (eng, geo) => math + eng + geo

科里化是指需要传递所需要的个数参数后才能得到完成应用函数
偏应用函数指固定了内部参数后,返回可以直接使用的完整应用函数

柯里化与偏函数都是闭包的一种应用;柯里化单个入参,偏函数则不强制入参必须为一个。

柯里化:

通过闭包的方式,将接收 多个参数 的一个函数,改造为每次接收一个函数的多个互相嵌套的函数。并能持久化保持一部分参数达到避免重复传递相同参数的目的。

偏函数:

原理和柯里化相同,不同点在于不用拘泥于每层函数都只接受一个函数,而是可以接受若干个参数。也是使用闭包作为基础实现。

举例:

//柯里化
function person(city){ //闭包
   return function (school){
     return function(name){//使用闭包,city,school会一直保存在内存中,不会销毁,因此不需要重复传递
        city+school+name;
     }
   }
}

let school=person("成都市")("成都七中"); 
school("小李");//成都市成都七中小李;
//偏函数
function person(city,school){
     return function(name){//使用闭包,city,school会一直保存在内存中,不会销毁,因此不需要重复传递
        city+school+name;
     }
}
let schoole=person("成都市","成都七中")

schoole("小李"); //成都市成都七中小李

柯里化和偏函数都是利用了闭包的特性去缓存了参数。

柯里化是将一个多参数的函数,转成了多个单参数的函数,然后去分步调用。

而偏函数并没有柯里化做的那么极致,它的目的是将先传入的某个或某几个参数缓存起来,返回一个函数,你下次再执行这个新函数的时候传入剩下的参数即可返回结果

他们两者的**高度相似,核心都是利用闭包的特性去缓存参数,返回一个新的函数。
而且偏函数可以看作是柯里化的一种特例。

柯里化

柯里化是将函数进行降维的操作,将一个多参的函数转化成一个每次接收一个参数的函数链

add(a, b, c)    柯里化=>   add(a)(b)(c)

偏应用函数

偏应用函数是当一个函数参数较多时,如果多次调用这个函数,而函数中的某些参数保持不变,就可以先将这些参数进行固化,绑定到函数上,生成一个新的函数表达式,然后将这个表达式赋值给新的变量,调用该函数时只需要传入剩下的参数即可

add(a, b, c)    偏应用函数=>    add(a, b)(c)

这两种函数形式的底层都使用了闭包对函数的参数进行保存,并在后续调用函数时,能够继续访问到之前的参数

JavaScript 采用词法作用域 (lexical scoping), 也就是静态作用域。
闭包就是由于在运行期静态作用域所产生的作用域链的产物。那些既不定义在当前作用域也没有定义在全局作用域的变量被当前函数访问就形成了闭包。
柯里化指的是: 将接受n个参数的一个函数转化成接受1个参数的n个函数。
偏函数指的是:将接受n个参数的单个函数任意固定a个参数,返回接受剩余n-a个参数的函数。
可以看出,无论是偏函数还是科里化,都是高阶函数,而且用到了非当前作用域和全局作用域的变量
所以,闭包是科里化和偏函数的必要条件。科里化和偏函数是属于闭包的应用范畴

柯里化(Currying)是把接受多个参数的函数变换成接受一个参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。

偏函数是将接受n个参数的函数任意固定a个参数,返回接受剩余n-a个参数的函数。

柯里化和偏函数背后的实现机制是闭包,我们想想闭包的定义,函数和函数定义时的词法环境组成闭包,因为是定义时的词法环境,所以柯里化和偏函数返回的新函数才能访问到定义时的变量。