一文搞懂函数柯里化+手写及类数组对象-数组的转化
手写函数柯里化原理
柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
函数柯里化的核心在于:函数里面返回函数,从而做到参数复用的目的。
1.最简单的柯里化例子
add 函数
// 普通的add函数
function add(x, y) {
return x + y;
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y;
};
}
add(1, 2); // 3
curryingAdd(1)(2); // 3
看了例子,主要概念就是让函数返回一个 function 可以接受第二个(或者更多)括号里的参数并输出期望值。——> 所以说我们要返回一个函数
2.手写题:限制参数个数的写法
限制为 3 个参数:题干为输出 sum(1,2)(3)的值,也就是说三个参数的情况
针对这个题目,需要做的是:
1、利用闭包创建一个数组保存参数 2、返回一个方法,用于接收下一个括号里的参数 3、全部接收后,返回所有参数的和
如果仅仅针对题目,我们得到下面的代码
// 固定数量参数
function constSum() {
// 因为就三个参数,直接用计数器,定义数组添加参数,判断数组长度也可以
(this.counter = 0), (this.sumNum = 0);
//逗号运算符:这段代码使用逗号运算符在同一行内初始化了两个变量 this.counter 和 this.sumNum。逗号运算符允许在一个表达式中依次执行多个子表达式,并返回最后一个子表达式的结果。(所以说可以用变量去接收值)
//相当于:
// this.counter = 0;
// this.sumNum = 0;
let that = this; //必须要先把this变量定义出来,否则里面箭头函数取不到这个this
// ...rest是ES6中参数解构的写法,函数中的rest为数组
return function innerSum(...rest) {
rest.forEach((item) => {
that.counter++; //记录进来了几个参数
that.sumNum += item;
});
// 判断
if (that.counter === 3) {
//如果已经有3个数字了,直接返回
return that.sumNum;
} else {
return innerSum; //如果还没有3个数字,那么返回一个函数继续接收参数
}
};
}
let sum = new constSum();
var result = sum(1, 2)(3);
var result2 = sum(1, 2, 3);
console.log(result); // 6
console.log(resul2); // 6
这样可以实现题目中的效果,但是并不好,因为限定死 3 个参数,如果我们希望拥有更灵活通用的方法呢?
比如可以支持 sum(1,2,3)(4)(5)......
3.手写题:不限制参数个数的写法
不限定参数数量的高级写法:题干为 sum(1,2)(3) sum(1)(2)(3) sum(1,2,3)
写法一:基本常用写法
针对这个题目,需要做的是:
1、利用闭包创建一个数组保存参数 2、返回一个方法,用于接收下一个括号里的参数 3、全部接收后,返回所有参数的和
有几个括号,就会进行几次递归,并在递归时执行 args.push(...arguments)操作
注意:arguments 就是函数的所有参数内容,可以用 … 进行展开
最后的输出结果是一个函数,控制台输出的函数是被转换为 string 类型的字符串了的,转换为 string 是因为调用了 toString 方法,只要我们改写 toString 方法的内容,让他输出 sum 的结果,而不是简单转义,就可以达到我们的目的了!
function sum() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
// let args = arguments; //用于获取第一个括号里的参数
// 因为arguments本身是一个类数组结构(key值为数字的,具有length属性),因此上述代码还需要进行改进,下面这行才是正确的
let _args = Array.prototype.slice.call(arguments);
// Array.prototype.slice.call(arguments)能将类数组结构转成数组。
// 注意:所有的数组实例都继承于 Array.prototype,所有的数组方法都定义在 Array.prototype 身上
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值(_args会长久的存在)
let _adder = function () {
_args.push(...arguments); // arguments默认就为函数的参数,即使我们没有列出形参
return _adder; //第二次即以后都是走的这里了!
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce((prev, cur) => {
return prev + cur;
});
};
return _adder; //只有第一次的()会走到这里,相当于高阶函数(返回了一个函数)
}
console.log("" + sum(1)(2)(3)); // 6
console.log("" + sum(1, 2, 3)(4)); // 10
console.log("" + sum(1)(2)(3)(4)(5)); // 15
其中arguments
、Array.prototype.slice.call
和 toString隐式转换
其实都是 js 基础知识点
先明白这里 toString 的功能:
在返回最后一次执行结束后,return 了一个_adder
方法,在输出时(不是 console.log 的时候,是在进行各种符号运算的时候),正常会隐式调用
Function.toString()的方法,以字符方式输出方法。因为上面改写了_adder 的 toString 方法,所以最后没有以字符方式输出方法
,而是隐式调用了我们改写的输出了所有参数的和
的 toString 方法。
使用“”+,可以隐式触发 toString 方法,可以输出6
这样的数字,但如果输出一下这个结果,或者这个结果的类型比如:
// ...上略
console.log(sum(1)(2)(3)); //[Function: _adder] { toString: [Function (anonymous)] }
console.log(typeof sum(1)(2)(3)); // function
我们会得到这个结果其实就是一个函数。所以“”+的这个方法其实是很勉强的实现了题目的效果,但得到的数据不能直接作为 Number 来使用。
还是需要显示调用
并 Number.parseInt 强转才能得到 number 类型的结果:
// ...上略
let sumToNumber = Number.parseInt(sum(1)(2)(3));
console.log(sumToNumber); // 6
console.log(typeof sumToNumber); // number
注:什么是 toString 隐式转换?
在 JavaScript 中,隐式转换是指在表达式中涉及不同数据类型的操作时,JavaScript 引擎自动进行的数据类型转换,以便完成操作。其中一个常见的隐式转换是通过 toString
方法实现的。
toString
是 JavaScript 中的一个方法,它存在于所有对象类型中,包括基本数据类型(如数字、字符串等)。当你在一个对象上使用某些操作符(==也会触发)或将其与其他数据类型进行运算时,JavaScript 引擎可能会自动调用对象的 toString
方法,默认将其转换为字符串,以便进行操作。
例如:
const num = 42;
const str = "Hello";
const result = num + str; // num 隐式转换为字符串,然后进行字符串拼接
console.log(result); // 输出 "42Hello"
在这个例子中,num
(一个数字)和 str
(一个字符串)进行了相加。由于操作符是字符串拼接操作,JavaScript 引擎将自动调用 num
的 toString
方法将其转换为字符串,然后进行拼接操作。
同样,toString
隐式转换也可以在其他上下文中发生,例如将对象放入模板字符串中,或者将对象与其他数据类型一起传递给函数等。
需要注意的是,不同的对象类型可能会有不同的 toString
行为。例如,数组的 toString
方法将返回以逗号分隔的元素列表(输出"1,2,3,4,5"),对象的 toString
方法通常会返回一个标识该对象的字符串(输出 "[object Object]")。
总之,toString
隐式转换是 JavaScript 在处理不同数据类型之间的操作时自动发生的一种数据类型转换。
我们可以给对象或者函数去自定义一个 toString 方法,改变默认的调用:
//函数:
let _adder = function () {
_args.push(...arguments);
return _adder;
};
_adder.toString = function () {
return _args.reduce((prev, cur) => {
return prev + cur;
});
};
//对象:
let obj = {
name: "zhangsan",
toString: function () {
return `${this.name}, ${this.age} years old, works as ${this.job}`;
},
};
注:arguments 是个什么东西?
这里我们可以看到 arguments 对象将我传入的五个参数以数组的形式保存在里面,还有保存了我传入函
数的实参的个数(length)。
而且我们可以看到 arguments 对象的 __proto__
是指向 object 的,这也说明了他是个类数组对
象(本质上是一个对象,但是具有 length 属性,很特殊),而不是一个数组。
arguments 是可以在函数中直接访问的,不需要 this.也不需要定义!
怎么把 arguments 转化为数组呢?可以用 Array.prototype.slice.call()方法,也可以用[...arguments]展开 arguments 放到数组里面!
所以说使用对象的扩展运算符也可以展开对象(ES7 新特性),…arguments
就可以把它展开放入数组里面了!
let _args = Array.prototype.slice.call(arguments);
let _args = [...arguments]; //二者等价
注:什么是类数组对象?
类数组对象是指具有类似数组的结构,但不具有数组的方法和属性的对象。这些对象通常具有数字索引(从 0 开始),并且具有 length
属性来表示其长度,但它们不具备数组的方法(如 push
、pop
、forEach
等)。
常见的类数组对象包括:
- DOM 节点列表: 通过
document.querySelectorAll()
或类似方法获取的 DOM 元素列表,例如NodeList
和HTMLCollection
。 - 函数的
arguments
对象: 函数内部的arguments
对象,它是一个类数组对象,包含函数调用时传递的参数。 - 字符串:字符串可以通过索引访问字符,具有类似数组的特性(可以理解为类数组对象,但是它不是对象)。
- TypedArray 对象: TypedArray 是一类类数组对象,用于处理二进制数据,如
Int8Array
、Float32Array
等。 - 自定义对象: 您也可以创建自己的类数组对象,只需满足索引从 0 开始、具有
length
属性的条件。
尽管类数组对象与数组有一些相似之处,但由于它们缺少数组的方法和属性,因此无法直接使用数组的操作。为了使用数组的方法,您通常需要将类数组对象转换为真正的数组,例如使用 Array.from()
或 Array.prototype.slice.call()
方法。
注:Array.prototype.slice.call()方法详解
Array.prototype.slice.call()
方法实际上是将 JavaScript 中数组的 slice
方法应用于其他对象(类数组对象或数组),以创建一个新的数组。这种方法通常用于将类数组对象转换为真正的数组,以便可以使用数组的方法和属性。
Array.prototype.slice.call()
是将 slice
方法与 call
方法结合使用的技巧,因为 slice
本身就是一个函数,那么它的原型上肯定有 call
方法,所以我们利用了 call
方法来在 slice
方法上设置特定的上下文,以便我们可以在类数组对象上调用 slice
方法。因为 slice
方法实际上是 Array 原型上的方法,但我们想要将它用于类数组对象上。
让我们再次强调一下这个结合的过程:
Array.prototype.slice
是数组的原型方法,用于创建数组的浅拷贝。它通常这样使用:array.slice(start, end)
,其中array
是一个真正的数组,start
和end
是起始和结束索引。call
方法是 JavaScript 函数的方法,它允许你指定函数的执行上下文(this
值)以及传递参数列表。通常这样使用:function.call(thisValue, arg1, arg2, ...)
。- 当我们使用
Array.prototype.slice.call(arrayLike)
时,我们将call
方法应用在slice
方法上,并将arrayLike
(类数组对象)作为参数传递给call
方法。这会在slice
方法内部使用arrayLike
作为执行上下文,实际上就是在类数组对象上调用了slice
方法,从而将其转换为真正的数组。
function convertToArray(arrayLike) {
return Array.prototype.slice.call(arrayLike);
}
const args = convertToArray(arguments);
console.log(args);
那么 Array.prototype.slice.call()和[...arguments]两者具体有什么区别呢?
注意:
Array.from()
方法 其实和Array.prototype.slice.call()
方法也是等价的,效果一样,都是转化类数组对象为数组用的!
还有一个重要区别:Array.from()可以把Set集合转化为数组,而Array.prototype.slice.call()方法不可以!
Array.prototype.slice.call(arguments)
和 [...arguments]
都可以用来将类数组对象(如 arguments
对象)转换为真正的数组,但它们在语法和用法上有一些区别。
- 语法差异:
Array.prototype.slice.call(arguments)
使用了call
方法,将slice
方法应用在arguments
对象上。slice
方法接受参数用于指定截取的起始位置和结束位置。[...arguments]
使用了扩展操作符...
,它在数组字面量内使用,将类数组对象展开成一个新的数组。
- 用法:
Array.prototype.slice.call(arguments)
的语法稍微冗长,因为它需要在调用slice
方法时传递起始位置和结束位置。如果你不需要截取部分数组,可以省略这些参数。[...arguments]
更简洁,它会将整个arguments
对象转换为一个新的数组。
示例比较:
function example() {
console.log(Array.prototype.slice.call(arguments)); // 使用 slice 方法
console.log([...arguments]); // 使用扩展操作符
}
example(1, 2, 3);
在此示例中,两种方式都会输出 [1, 2, 3]
,因为它们都将 arguments
对象转换为数组。
总的来说,虽然两种方法都可以用来实现类数组对象到数组的转换,但在大多数情况下,[...arguments]
更为简洁和常见。如果你需要截取数组的一部分,那么 Array.prototype.slice.call(arguments)
可能会更有用。
function example() {
const slicedArguments = Array.prototype.slice.call(arguments, 1, 3);
console.log(slicedArguments);
}
example(1, 2, 3, 4, 5);
注意:Array.isArray 是 JavaScript 中的内置方法,但是
Array.toArray
并不是 JavaScript 中的内置方法!
如果您需要将类数组对象(如 DOM 节点集合,document.querySelectorAll('.my-elements'))或类似数组的对象(如字符串、arguments 对象等)转换为真正的数组,您可以使用一些现有的方法来实现:Array.prototype.slice.call 方法或者 Array.from 方法
类数组对象和对象,两者转化为数组的区别?
将类数组对象转换为数组通常需要使用 Array.from()
方法或 Array.prototype.slice.call()
方法。我们可以将它转换为真正的数组,里面就是每一项(不会带着索引的)。
而对象转换为数组通常需要使用 Object.entries()
方法: 获取属性名和属性值的键值对数组,最后组成数组,这样就会形成一个二维数组,而不是一个普通数组。
当然如果我们想要把对象转化获得一维数组,可以使用 Object.keys() 方法或者 Object.values() 方法,但是这样势必会丢掉 key 或者 value 中的一种!
写法二:用 ES6 特性优化,省略缓存数组的构建
使用ES6 的不定参数 rest的新特性,该特性可以让不定参数
变成一个数组
传入,不需要访问arguments
,也省去了使用Array.prototype.slice.call(arguments);
生成数组,更加优雅。
function sumWithES6(...rest) {
// 这里使用的是ES6数组的解构
var _args = rest;
var _adder = function (...innerRest) {
// 这里使用的是ES6数组的解构
_args.push(...innerRest); // 直接放进缓存数组里面
return _adder;
};
_adder.toString = function () {
let sum = _args.reduce(function (a, b) {
return a + b;
});
return sum;
};
return _adder;
}
console.log("" + sumWithES6(1)(2)(3)); // 6
ES6 的不定参数 rest
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3); // 10
上面代码的add
函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
写法三:优化,改为 total 变量记录和,并将求和函数抽离出来
1.利用闭包的写法:——> 更好,效率略高
const getSum = (args) => {
// 参数求和方法,args一定是个数组
return args.reduce((a, b) => {
return a + b;
});
};
const sum = (...args) => {
let totalParamsSum = getSum(args); // 每次调用sum的时候都计算并作为总和total
const _adder = (...arg2) => {
const sum2 = getSum(arg2); // 将本次括号中的数字求和(这里不需要定义一个数组专门用来存储所有的参数,因为每次都是直接把数字加到total里面了)
//写法一:
totalParamsSum += sum2; //利用闭包直接改变total,不用每次都搞新的sum,而是把total利用闭包缓存起来
return _adder; //后面从这里返回_adder,不需要每次重新初始化_adder
};
_adder.toString = () => {
return totalParamsSum; //直接返回total即可
};
return _adder; //只有第一次调用sum从这里返回_adder
};
console.log(sum(1)(2)(3) == 6);
2.利用累加的写法:
const getSum = (args) => {
// 参数求和方法,args一定是个数组
return args.reduce((a, b) => {
return a + b;
});
};
const sum = (...args) => {
const totalParamsSum = getSum(args); // 每次调用sum的时候都计算并作为总和total
const _adder = (...arg2) => {
const sum2 = getSum(arg2); // 将本次括号中的数字求和(这里不需要定义一个数组专门用来存储所有的参数,因为每次都是直接把数字加到total里面了)
//写法二:
return sum(totalParamsSum + sum2); // 把本次括号和与之前的和进行累加,然后重新构建一个sum,以便给total重新赋值,并返回_adder(这种理解起来较为困难,实际上就是每次都搞一个新的sum,但是这个新的sum会接收之前的所有和作为参数,保证total的累加性)
};
_adder.toString = () => {
return totalParamsSum; //直接返回total即可
};
return _adder; // 每次都从这里返回_adder(每次都是一个重新初始化的_adder)
};
console.log(sum(1)(2)(3) == 6);
最终最简便的写法
//自己手写函数柯里化
const sum = (...rest) => {
let _args = rest; //缓存数组 ——> 第一步
let _adder = (...innerRest) => {
//_adder函数,里面存储参数到缓存数组,并返回一个新的_adder函数 ——> 第二步
_args.push(...innerRest);
return _adder;
};
_adder.toString = () => {
//重写toString方法 ——> 第三步
return _args.reduce((a, b) => a + b);
};
return _adder;
};
console.log("" + sum(1)(2)(3));
注意:如果面试官让输出 sum(1)(2)(3).valueOf(),而不是直接 sum(1)(2)(3),那么就添加一个 valueOf()方法就可以了,就别重写 toString 方法了!
const sum = (...rest) => {
let _args = rest;
let _adder = (...innerRest) => {
_args.push(...innerRest);
return _adder;
};
_adder.valueOf = () => {
return _args.reduce((a, b) => a + b); //默认a的初始值是null
};
return _adder;
};
console.log(sum(1)(2)(3).valueOf());
考法变形
1.参数接收一个累加函数并构造出来函数的形式,然后接收参数(固定个数或者非固定都可以)
使用 slice 和 apply
注意:这样的话,sum 并不直接是一个可以使用的柯里化函数,它的返回值才是一个柯里化函数
所以我们的 sum 这里是一个高阶函数!
function sum(fn) {
return function () {
let _args = Array.prototype.slice.call(arguments);
let _adder = function () {
_args.push(...arguments);
return _adder;
};
_adder.toString = function () {
return fn.apply(null, _args); //这里我们直接调用传入的累加函数
};
/*
之前的写法:
_adder.toString = function(){
return _args.reduce((prev,cur) => {
return prev + cur;
});
}
*/
return _adder;
};
}
function add(a, b, c) {
return a + b + c;
}
const sumAdd = sum(add); //构造出来一个柯里化函数
console.log("" + sumAdd(1)(2)(3)); // 输出 6
console.log("" + sumAdd(1, 2)(3)); // 输出 6
2.接收累加函数,并直接接收参数(固定个数或者非固定都可以)
function add(a, b, c) {
// console.log(a, b, c);
return a + b + c;
}
const curry = (fun, ...args) => {
let arr = [];
// arr.concat(args); //注意:concat是会返回新数组的,而不是修改原数组 ——> 大坑,所以尽量用push(...args)的方式!
arr.push(...args); //push可以改变原数组,并且可以接收多个参数,可以使用push
function sum(...args2) {
// arr.concat(args2);
arr.push(...args2);
return sum;
}
sum.toString = () => {
//return fun(arr[0], arr[1], arr[2]); //这种写法比较低级
return fun.apply(null, arr); //高级写法:apply会自动将数组拆开为单个元素
};
return sum;
};
console.log(curry(add, 1)(2)(3) == 6);
console.log(curry(add)(1, 2)(3) == 6);
console.log(curry(add, 1, 2, 3) == 6);
涉及的知识点:
1.arr.push(...args)是往数组里面合并元素的最佳选择,因为 push 可以改变原数组,并且可以接收多个参数
而 concat 方法是会返回新数组的,而不是修改原数组,所以用 arr.concat(args)合并是不行的,需要写成 arr = arr.concat(args);
2.fn.apply(null, arr)是执行一个接收若干参数的方法的最佳选择,因为 apply 会自动将 arr 拆成单个元素传给 fn 方法
fun(arr[0], arr[1], arr[2])这种写法的普适应不高,并且写法不方便!