18-3 函数一等公民 /高阶函数 / js 闭包的深刻理解(实例分解:如何实现无限累加的一个函数)
函数的地位:一等公民
在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。
对于 JavaScript 来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此 JavaScript 中函数是一等公民。
在 JavaScript 语言中,函数可以保存到变量、作为参数传递、作为返回值返回。在JavaScript里函数可以出入任何场所,这一点在很多其他语言(比如 Java、C# 等)中是不太容易做到,正是因为如此,使用 JavaScript 可以非常轻松的实现高阶函数。
什么是高阶函数?
- 参数是函数,或者返回值是函数即为高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。
看图:此时fn和func就是高阶函数,函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最经典的就是作为回调函数。
使用高阶函数的意义
- 高阶函数是用来抽象通用的问题
- 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
下面我们来看一个例子:实现一个add方法,执行add(1)(2)(3)(4),求传入参数的和。
function curring() {
var total = 0;
let params = [].slice.call(arguments)[0];
total += params;
return function fn() {
let params = [].slice.call(arguments)[0];
if (params) {
total += params;
}
if (arguments.length) {
return fn;
}
return total;
};
}
let res = curring(1)(2)(3)();
console.log(res);
2. 方式1:高阶函数 -> 在没没有传入参数时还要再执行一次
把求和的业务方法和组织入参的通用方法解耦
// 业务方法
function toolFn() {
let arr = Array.from(arguments);
return arr.reduce((p, c) => {
return p + c;
});
}
// 通用方法,收集参数
function curring1(toolFn) {
let params = [];
return function fn(...args) {
params.push(...args);
if (args.length) {
return fn;
} else {
return toolFn.apply(null, params);
}
};
}
let computed = curring1(toolFn);
let res1 = computed(1)(2)(3)();
console.log(res1, "方式1");
3.方式2: 高阶函数 -> 业务方法入参个数对应curring执行次数,这样就不用像上面那样再多执行一次了
// 业务方法
function sum(x, y, z) {
return x + y + z;
}
// 柯里化函数(轮子);组织入参
function curring2(func) {
let params = [];
return function fn(...args) {
params.push(...args);
if (params.length < func.length) {
return fn;
} else {
return func.call(null, ...params);
}
};
}
let add = curring2(sum);
let res2 = add(1)(2)(3);
console.log(res2, "方式2");
4.方式3: 高阶函数 -> 更简洁和灵动的写法,对于每次执行的入参可以是多个,但与业务方法入参数的总个数要保持一致,我认为很nice
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
? // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
fn(...args)
: /**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
*/
(..._args) => curry(fn, ...args, ..._args);
function total(x, y, z) {
return x + y + z;
}
const addFn = curry(total);
console.log(addFn(1, 2, 3, 4), "方式3");
console.log(addFn(1)(2)(3), "方式3");
console.log(addFn(1, 2)(3), "方式3");
console.log(addFn(1)(2, 3), "方式3");
上面我们写的要么是需要多执行一次,要么是执行次数是要与业务方法的入参是一致的,下面我们实现一个无限执行累加求和的函数
js 闭包的深刻理解(实例分解:如何实现无限累加的一个函数)
js闭包,递归函数,es6结构解析,js高阶函数,拓展运算符等。
题目:实现一个无限累加的js函数
add(1)(2)(3) // 6
add(1, 2, 3)//6 add(2, 3)(2)//7 add(1)(2)(3)(4) //10 add(2)(4, 1)(2) //9
首先,胖子也是一口一口吃出来的,我们一步步拆解
// add(1)
function add(a) {
return a;
}
// add(1)(2)
function add(a) {
return (b) => {
a = a + b;
return a;
};
}
// add(1)(2)(3)
function add(a) {
return (b) => {
a = a + b;
return (c) => {
a = a + c;
return a;
};
};
}
如果分开来看,我们都可以单独实现他,都很简单,那我们怎么一个函数来实现呢?
那我们来分析一下,分析下上面的三个函数实现,是否得出一些规律呢,add函数后面每增加一个“(num)”,实现中都会多一层“ return function(){} ”。
function add(a) {
var num = (b) => {
a = a + b;
return num;
};
return num;
}
看是不是和上面的执行规律一致了,每次调用都会返回一个函数来接受下次的传值,这样你会发现,每次返回的都是函数,可是我想要返回的是值,而不是无限的返回函数。
那我们就来尝试来解决一下
function add(a) {
var num = (b) => {
if (b) {. // 当后面传入有参数时,继续返回函数
a = a + b;
return num;
} else { // //当后面出入无参数时,返回值
return a;
}
};
return num;
}
console.log(add(1)(2)(3)()); //6
显然:我们期望的是add(1)(2)(3)直接就能得到值了,而不需要再去调用一次。
我们这样改造:
function add2(a) {
var num = (b) => {
a = a + b;
return num;
};
num.toString = function () {
console.log("我自动被触发了?结果是:", a);
return a;
};
num.xxx = function () {
return a;
};
return num;
}
console.log(add2(1)(2)(3)); // 是num函数,需要手动调用改写的toString()方法
console.log(add2(1)(2)(3).toString()); // 6
console.log(add2(1)(2)(3).xxx()); // 6 -> 既然是手动触发,那么可以调用num函数自定义的xxx属性,返回结果。之所以写toString()是alert会自动调用
//其实console.log()也能触发toString方法,但是返回不出去,不知道为啥?
alert(add2(1)(2)(3)); // 6 -> alert会自动调用toString()方法,进而触发了改写的num函数的toString方法
小示例说明:
var f = function(name) {
var age = 18;
f.toString = function() {
return `${name} : ${age}`
}
return f
}
alert(f('张三'))
console.log(f('李四'));
不定参,多参写法:
function sum(...a) {
const s = (...b) => sum(...[...a, ...b]); //参数合并
let result = a.reduce((x, y) => {
//当前阶段求值
return x + y;
});
s.toString = function () {
// 虽说console.log不能直接拿到返回出去的result,但是确实在console的时候确实
// 自动执行了toString方法,所以我们在这里得到了result,赋值给别的变量也同样算是
// 拿到了求和的结果
console.log("自动调用了??", result);
//函数出口
return result;
};
return s;
}
console.log(sum(3, 10)(1)(2)(10)); // 输入函数s
alert(sum(3, 10)(1)(2)(10, 1, 3, 8, 2)); //直接输出40
let r = sum(3, 10)(1)(2)(10).toString();
console.log(r, "手动调用toString方法"); // 26
方法与主函数解耦:
(1)执行一个run方法后得到计算结果:
function curring3(fn) {
// var total = 0;
var arr = []
var innerFn = (...args) => {
arr = [...args]
return innerFn.bind(this, ...arr)
}
innerFn.run = function() {
return fn.call(this, ...arr)
}
return innerFn
}
function sum3(...args) {
return args.reduce((x, y) => (x + y))
}
var res3 = curring3(sum3)
res3(1, 2, 3, 4, 5)(6)(1)(2, 3)
console.log(res3.run());
(2)借用alert自动调用了toString()方法
function curring3(fn) {
let result = 0;
var innerFn = (...args) => {
let f = (..._args) => {
result = fn(...args, ..._args)
return innerFn(...args, ..._args)
}
f.toString = function() {
return result
}
return f
}
return innerFn
}
function sum3(...args) {
return args.reduce((x, y) => (x + y))
}
var res3 = curring3(sum3)
let result = res3(1, 2, 3, 4, 5)(6)(1)(2)(3, 3)
alert(result);
console.log(result.toString());