闭包--第一印象

目录

前言

对闭包的定义

我对闭包的理解

一种常见的闭包方式

总结:

面试题举例

前言

       闭包作为一种编码方式经常被使用,能被大众所接受并广泛使用的一定是简单的,那闭包就能以简单的方式进行描述,想借此进一步理解闭包。如果哪里写的不对还请大佬指出。

       引用自我在掘金上写的文章,先在csdn上改,之后再迁移到掘金上

对闭包的定义

红宝书:那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

我对闭包的理解

       网上有两种说法,一种是函数接口需要在其他地方被使用才能算闭包,另一种则认为不需要,我认为两者的差异在于函数接口还没被使用时形成了包的样子,但不被使用的话在离开外层作用域时得到闭包随后释放闭包,无法用到闭包中的特性,所以我更倾向于前者,后文也是基于此进行展开。

什么是闭包:函数在外层作用域中创建,使用外层作用域的成员,并在其他地方被使用。

闭包的效果:函数记住一些成员并能在多次调用之间共享,看起来像是外层作用域形成了一个包,这些成员成了包里的东西,函数成了包上的口子,可以通过这个口子对包里的成员进行操作(图片引用自网络,没找到好的,这张先用着)。

闭包的作用:将上级作用域的成员保留为私有变量,并保护它们不受外部的干扰

闭包为什么起作用

        闭包的目的常是维护一些在函数多次调用间能够共享的对象,但这些对象直接声明在函数中会因为每次调用时重新初始化导致共享失败,闭包是通过作用域来解决。内部上下文能通过作用域链单向访问外部上下文中的成员,所以每次的函数调用都是去操作外层作用域上的对象,来将共享数据保存在公有位置。函数只调用一次也同理。

        至于为何外层作用域上的这些对象在多次调用间保存了下来,是因为还有对它们的引用,js回收机制使用“标记清理”的方式,先将内存中所有变量打上标记(打标记的方式有很多种),再将所有在上下文中的变量,以及它们引用的变量的标记擦除,之后剩余未擦除标记的变量就是需要清理的变量。而这些需要共享的对象,虽然离开了它们的作用域,但在当前上下文中还有对它们和函数的引用,所以标记会被擦除从而不会被执行清理。

        为何“在外层作用域中创建”中的“外层作用域”常是函数作用域,我的理解是目前作用域有全局作用域、函数作用域、块作用域。全局作用域默认全局都可使用,块作用域我暂未找到什么方式能将接口暴露出来。倒是按需引入其他模块中某函数,这个挺像的,不知可否算作全局作用域,还请大佬解惑。

闭包的影响

1. 额外内存占用

解释:闭包的存在使得共享对象有被引用,js回收机制暂时不对其回收,会等待程序员主动释放或离开使用闭包的上下文后才会回收这块内存。

2. this指向问题

解释:使用this是为了以更优雅的方式来切换运行时上下文,但使用闭包会增加写this的难度

一种常见的闭包方式

const changeCount = () => {
  let count = 0;
  const displayCount = () => console.log(`调用${++count}次`);
  return displayCount;
};
const myCountFirst = changeCount();
myCountFirst(); // 调用1次
myCountFirst(); // 调用2次
const myCountSecond = changeCount();
myCountSecond(); // 调用1次
myCountSecond(); // 调用2次

代码解读:

1. 首先声明changeCount命名函数,为闭包提供所需的外部函数作用域,函数作用是显示该函数被调用了几次,所以需要在函数之外维护一个变量表示调用次数,直接放在最外面可能会被其他地方修改,所以这里用一层函数作用域将它包起来。

2. 然后调用changeCount函数创建一个闭包,赋值给常量myCountFirst,指向于changeCount函数的执行结果displayCount。changeCount函数执行时,首先在作用域(包)中创建一个变量count(被接口函数引用的成员),然后创建在其他地方用的函数displayCount(包上的口子),并将这函数作为返回值提供。

3. 执行myCountFirst函数,更新包内的成员并打印,重复调用函数接口myCountFirst会重复操作这个包里的成员count。

4. 再次执行changeCount函数,会创建一个新的闭包,两个闭包互不影响。

总结:

      闭包就是函数在外层作用域中被创建,使用了外层作用域中的成员,并在其他地方被使用,作用是将上级作用域中的成员保存为闭包里的私有成员,并保护它们不受外部影响。效果就像是外层作用域成了一个包,包里装着被引用的成员,引用他们的函数就像包的口子,通过这个口子可以操作那些成员。闭包的实现基于作用域链,内部上下文能通过作用域链单向访问外部上下文中的成员,闭包上的函数接口每次使用这些成员,都是去外层作用域操作,所以函数能将这些成员保存为闭包里的私有成员。

面试题举例

1. add(1,2,3)(4,5,6)().valueOf()

题目解读:需要实现一个add函数,功能是将所有接收到的参数加起来,并提供valueOf方法来获取总和。题目中add首先会接收一组不定长度的参数,之后还能重复接收,所以它会返回一个函数,这个函数能通过闭包存储当前总和,而如果想使用两次add时能够互不影响,所以这个返回的函数不能是add本身,而返回的函数需要支持valueOf方法,可以通过在该函数的原型上添加valueOf方法,或直接作为属性添加。

const add = (...args) => {
  let countMe = args.reduce((count, arg) => count + arg, 0);
  const sum = (...argsSum) => {
    countMe += argsSum.reduce((count, arg) => count + arg, 0);
    return sum;
  };
  sum.valueOf = () => countMe;
  return sum;
};

2. 编写curry.js,实现函数的分步调用

var curry = require('./curry.js');  // <- this is the file you make;
const add = (a, b) => a + b;
var curried = curry(add);
console.log(curried(1)(2)); // 预期结果: 1 + 2 -> 3

题目解读:代码从curry.js(我们实现curried方法的位置)读取curry方法,该方法能够接收一个处理函数,然后逐参数接收待处理的值,最后执行处理函数,因为curry方法的返回需要再接收参数,所以应该返回一个函数。题目中处理函数只有两个,但可以扩展为接收数量不定的参数,这需要curry能识别出处理函数的期望参数数量(可通过方法的size属性实现),并且返回的函数能识别出什么时候参数数量达到。达到之前需要返回自身以支持链式调用,达到时返回处理函数的结果,因为console.log为浏览器自身实现,修改函数的toString方法不一定起作用,返回值较好。

const currySeond = func => {
  let args = [];
  const sum = (...nums) {
    args = [... args, ...nums];
    return args.length === func.length ? func(...args) : sum;
  }
  return sum;
};

3. LazyMan

问题:实现LazyMan

LazyMan('Tony');
// Hi I am Tony

LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

题目要求:LazyMan接收一个字符串,并在之后提供eat、sleep、sleepFirst三种方法的调用,输出顺序是先输出自我介绍,然后如果链式调用中有sleepFirst,无论是在链表那里,都会提前到自我介绍后,而eat和sleep按它们调用顺序执行

题目解读:

    LazyMan(...)返回值设为x,x需要支持链式调用,所以x是对象,并在eat和sleepFrist与sleep执行完后返回自身,三个API是x中的方法。所以LazyMan是一个函数,返回一个包含三个API的对象,对象建议使用class实现。

    对象中需要实现任务的调度顺序,如果没有sleepFrist这种需要调整执行时机的,可以使用Promise的方式依次执行,因为要调整执行时机,可以使用队列的形式处理,思路是模仿中间件的实现,将每次链式调用做成一个回调函数,放到队列合适的位置中,然后利用setTimeout宏任务在所有微任务和同步任务执行完后开始执行的特点,从队列中一次取出每个任务开始执行。以这种方式,也可在调度方式上做些扩展,例如支持收尾工作的clearFinal和延迟几个任务的delayTasks

代码参照:2021-07-15 实现一个LazyMan - 简书

介绍:使用队列形式模拟执行顺序,将其后的eat、sleep、sleepFirst按执行顺序保存在数组对应位置,每个位置保存的是一个闭包,闭包中实现打印操作并取出数组下一个位置中的闭包执行。通过setTimeOut异步执行的方式保证在队列形成后开始执行。

他人在闭包上的探索:

add(1)(2)(3)你会写了,那么add[1][2][3]呢? - 掘金

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值