1.闭包
- 一般情况下定义在函数内部的变量在函数外部是不可访问的。但某些时候有又确实有这样的需求,这时就会用到闭包。
- 闭包概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域 。(能够读取其他函数内部变量的函数)通过闭包我们可以在一个函数内部访问另一个函数内部的变量。
- 简单理解:闭包 = 内层函数 + 引用的外层函数变量
1.闭包的形式
下面介绍闭包的形式,也就是访问函数内部变量的常见手段。
1 函数返回值为函数
function foo(){
let name = 'xiaom'
return function (){
return name
}
}
const bar = foo()
console.log(bar())
// 全局变量bar获取到了局部作用域foo的内部变量,这是最常见的形式
2 内部函数赋给外部变量
let num;
function foo(){
const _num = 18;
function bar() {
return _num;
}
num = bar;
}
foo();
console.log(num()); // 18
3 通过立即执行函数行成独立作用域,保存变量(es6之后使用let,const替代)
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
// 上述代码的预期输出结果是每隔一秒按顺序输出12345。
// 定时器任务会在同步任务执行完毕后再执行。因此此时的i已经变成6 会直接输出5个6。
// 解决这一问题的关键就是每次循环形成一个独立作用域,这样定时器中的操作执行时会访问对应作用域的变量。
for (var i = 1; i <= 5; i++) {
// 包一层立即执行函数 并传入i 由于内部操作用到了i 因此会形成闭包
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i);
}
闭包函数的其他形式大多是以上形式的变体。
2.闭包的优缺点
- 1.外部可以访问函数内部变量。
- 2.让函数内部变量一直保留在内存中。
function fn1() {
let count = 0;
function fn2() {
count++;
return count;
}
return fn2;
}
let result1 = fn1()
console.log(result1()) // 1
console.log(result1()) // 2
//通常来讲,函数执行完毕后,函数连同它内部的变量会被一同销毁。
//由于函数fn内部变量count被外部引用,因此fn执行完毕后,其内部变量count不会被销毁。因此过度使用闭包会造成内存消耗。
- 3.形成独立作用域。
显然,通过上述第二点也能看出,由于闭包会使函数内部变量一直保存在内存中,造成内存消耗,因此过度使用会造成页面性能问题。解决方法是及时删除不使用的局部变量。
2.高阶函数
闭包的一个典型应用:柯里化函数 。介绍柯里化之前需要先了解 高阶函数 的概念。
定义:接收函数作为参数或者返回函数的函数
简单理解就是:
- 首先是个函数
- 参数或者返回值是函数
举例子
我们这里举两个例子来覆盖下上文的定义,其中,例一为接收函数作为参数的高阶函数,例二为返回函数的高阶函数。
例一:函数作为参数
我们定义了一个叫evaluatesToFive的函数,接收两个参数:第一个参数是一个数字,第二个参数是一个函数。在函数evaluatesToFive中,将参数一(数字)传入参数二(函数)
function evaluatesToFive(num, fn) {
return fn(num) === 5;
}
使用的场景:
function divideByTwo(num) {
return num / 2;
}
evaluatesToFive(10, divideByTwo);
// true
evaluatesToFive(20, divideByTwo);
// false
例二:返回函数
创建一个函数multiplyBy,接收一个数字作为参数,并返回一个新的函数
function multiplyBy(num1) {
return function(num2) {
return num1 * num2;
};
}
使用场景:
const multiplyByThree = multiplyBy(3);
const multiplyByFive = multiplyBy(5);
multipyByThree(10); // 30
multiplyByFive(10); // 50
主要是通过生成新的函数以达到更具体的目的
复杂一点的应用实例
本例中,我们创建一个函数去检测新用户的注册信息是否能通过检验规则:
- 大于18岁
- 密码长度大于8
- 同意用户协议
新用户的注册信息大概是这样:
const newUser = {
age: 24,
password: 'some long password',
agreeToTerms: true,
};
接下来,我们来创建三个验证函数,通过返回true,否则返回false
function oldEnough(user) {
return user.age >= 18;
}
function passwordLongEnough(user) {
return user.password.length >= 8;
}
function agreeToTerms(user) {
return user.agreeToTerms === true;
}
接下来,该主角登场了,我们需要创建一个高阶函数来一次性完成所有的验证。参数一是新用户注册信息,剩下的参数是我们上文创建的三个验证函数。在函数体中依次执行验证:
function validate(obj, ...tests) {
for (let i = 0; i < tests.length; i++) {
if (tests[i](obj) === false) {
return false;
}
}
return true;
}
使用:
const newUser1 = {
age: 40,
password: 'tncy4ty49r2mrx',
agreeToTerms: true,
};
validate(newUser1, oldEnough, passwordLongEnough, agreeToTerms);
// true
const newUser2 = {
age: 40,
password: 'short',
agreeToTerms: true,
};
validate(newUser2, oldEnough, passwordLongEnough, agreeToTerms);
// false
3.柯里化函数
柯里化就是一种特殊的高阶函数,下面介绍柯里化函数的概念。
定义: 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
这里以数字相加为例子做简单说明:
我们通常是这样写一个函数来求得 两数相加 的值:
function sum(a,b){
console.log(a+b)
}
sum(1,2)
没有问题,然后需求变成了:三数相加
修改后变成这样:
function sum(a,b,c){
console.log(a+b+c)
}
sum(1,2,3)
问:这样写,有毛病吗?? 有!
这样一改,既违反了:“开闭原则”、又违反了:“单一职责原则”。
相关解释:
- 什么是“开闭原则”?即:我们编程中要尽可能的避免直接修改函数、类或模块,而是要在原有基础上拓展它;
- 什么是“单一职责原则”?即:每个函数、类或模块,应该只负责一个单一的功能;
首先,我们修改了 sum 函数的传参以及内部的调用 ⇒ 则违反“开闭原则”
其次,sum 函数本来只负责两数相加,修改后,它又负责三数相加,职责已经发生了变化 ⇒ 则违反 “单一职责原则”;
如果正规按照单一责任来写,应该是:
// 负责两数相加
function sum2(a,b){
console.log(a+b)
}
// 负责三数相加
function sum3(a,b,c){
console.log(a+b+c)
}
事实上,是不可能这样去写的,因为如果有一万个数相加,得写一万个函数。
而 加法只有一个!! 不管你最终要加几个值,总是要一个加一个。
于是乎,我们设想,能不能写一个这样的函数:它的功能,就是“加”,参数跟几个,我就加几个。
// 负责“加法”,
function addCurry(){
...
...
...
}
addCurry(1)(2) // 两数相加
addCurry(1)(2)(3) // 三数相加
...
addCurry(1)(2)(3)...(n) // n 数相加
这种就是柯里化 为了能够实现一个加一个,即存储参数的目的,可以用到闭包(实现延迟处理)
举例解释如下:
function directHandle(a,b){
console.log("直接处理",a,b)
}
directHandle(111,222)
// 直接处理 111 222
function delayHandle(a){
return function(b){
console.log("延迟处理",a,b)
}
}
delayHandle(111)
// ƒ (b){
// console.log("延迟处理",a,b)
// }
如上 delayHandle(111) 不像 directHandle(111,222) 直接打印值,而是先返回一个函数 f(b);111 也被临时保存了,delayHandle(111)(222),则得到相同的输出。这就是:延迟处理的思想。
所以这里我们借用闭包来实现最初版的柯里化:
// 两数相加
function addCurry(a){
return function(b){
console.log(a+b)
}
}
addCurry(1)(2)
// 三数相加
function addCurry(a){
return function(b){
return function(c){
console.log(a+b+c)
}
}
}
addCurry(1)(2)(3)
通过写这两个闭包的过程,可以看到有点像递归,当参数是 n 个的时候,需要递归 n-1 次 return function,所以addCurry可以写成这样:
let arr = []
function addCurry() {
// `Array.prototype.slice.call()`方法能够将一个具有`length`属性的对象转换为数组
let arg = Array.prototype.slice.call(arguments); // 递归获取后续参数
arr = arr.concat(arg);
if (arg.length === 0) { // 如果参数为空,则判断递归结束
return arr.reduce((a,b)=>{return a+b}) // 求和
} else {
return addCurry;
}
}
addCurry(1)(2)(3)()
主要有3个作用: 参数复用、提前返回和 延迟执行
用以上例子我们来简单地解释一下:
参数复用:拿上面 addCurry 这个函数举例,只要传入一个参数 z,执行,计算结果就是 1 + 2 + z 的结果,1 和 2 这两个参数就可以直接复用了。
提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。
以上就是柯里化的一种简单实践,待续......