首页 > 其他分享 >从闭包谈到高阶函数

从闭包谈到高阶函数

时间:2023-10-18 16:48:22浏览次数:35  
标签:闭包 function return 函数 谈到 result const 高阶

1 闭包的概念

闭包是由一个函数以及与其相关的引用环境组合而成的实体。闭包可以在函数内部访问外部函数的变量,并且这些变量可以在外部函数执行结束后仍然保持其状态。
听起来可能有点抽象,咱们来段代码:

function outerFunction(x) {
  return function innerFunction(y) {
    return x + y;
  };
}

// 创建闭包函数
var closure = outerFunction(5);

// 调用闭包函数
var result = closure(3);

console.log(result); // 输出:8

可以看到 innerFunction 可以获取到传入 outerFunction 的参数,可见原本在外部函数执行结束后本该销毁的地址得到了保留。
在这里我们总结一下闭包的特点:

  • 闭包可以访问和修改其创建时捕获的外部环境中的变量值,从而实现状态的保存和共享。
  • 闭包可以私有化变量,从而避免命名冲突污染全局作用域
  • 闭包可以延长变量的生命周期,从而实现回调、事件处理等高级操作。

有的同学可能会联想到高阶函数,或许对高阶函数闭包存在不清晰的认知,那么我们就再来讲一下高阶函数

1.2 高阶函数

高阶函数指的是能够接受函数作为参数或返回函数作为结果的函数,如 Array.prototype.mapArray.prototype.filterArray.prototype.reduce 就是高阶函数的实现。 老样子,我们用代码说明一下:

1.2.1 接收函数

// 定义接收函数的高阶函数
function times(n, f) {
  for (var i = 0; i < n; i++) {
    f(i);
  }
}

// 使用接收函数的高阶函数
times(5, function(x) {
  console.log("Hello, " + x + "!");
});

// 输出:
// Hello, 0!
// Hello, 1!
// Hello, 2!
// Hello, 3!
// Hello, 4!

1.2.2 返回函数

// 定义返回函数的高阶函数
function add(x) {
  return function(y) {
    return x + y;
  };
}

// 使用返回函数的高阶函数
var addFive = add(5);
console.log(addFive(3)); // 输出:8
console.log(addFive(7)); // 输出:12

明显可以看出闭包就属于返回函数的一类,由此得知,闭包高阶函数的一种特殊形式。

二、常见应用

2.1 防抖

防抖的原理是在一段连续触发的时间内,只执行最后一次操作。

function debounce(func, delay) {
  let timer;

  return function () {
    const context = this;
    const args = arguments;

    // 再次调用时,清除time,重新计时
    clearTimeout(timer);
    timer = setTimeout(() => {
      // 通过apply执行传入的函数
      func.apply(context, args);
    }, delay);
  };
}

2.2 节流

节流的原理是在一段时间内,固定执行操作的频率。

function throttle(func, interval) {
  let timer;

  return function () {
    const context = this;
    const args = arguments;

    if (!timer) {
      timer = setTimeout(function () {
        func.apply(context, args);
        // 触发完成后清除timer,进入下一周期
        timer = null;
      }, interval);
    }
  };
}

2.3 curry函数

柯里化(Currying)是一种将多个参数的函数转换为接受单个参数的函数序列的技术。

// 实现一个add函数 // return 1 + 2 + 3 + 2 传参为undefined时返回sum
function add() { // 创建空数组来维护所有要 add 的值 const args = [] // curry 函数,存入每次调用传入的参数 function curried(...nums) { if (nums.length === 0) { // 长度为0,说明调用结束,返回 args 的 sum return args.reduce((pre, cur) => pre + cur, 0); } else { // 长度不为0,将传入的参数存入 args,返回 curried函数给下一次调用 args.push(...nums); return curried; } } // 一开始给 curried 传递 add 接收到的参数 arguments return curried(...Array.from(arguments)); } console.log(add(1, 2)(1)()); // 输出:4 console.log(add(1)(2)(3)(4)()); // 输出:10 console.log(add(5)()); // 输出:5

 

2.4 迭代器

在 JavaScript 中,可以使用生成器函数(generator function)来实现迭代器。我们按照这个思路,来实现一个迭代器,满足遍历给定范围内的数字的功能

function rangeIterator(start, end) {
  // current 用于维护当前遍历到的值
  let current = start;
  
  return {
    next: function() {
      if (current <= end) {
        return { value: current++, done: false };
      } else {
        return { done: true };
      }
    }
  };
}

// 使用示例
const iter = rangeIterator(1, 5);

console.log(iter.next()); // 输出:{ value: 1, done: false }
console.log(iter.next()); // 输出:{ value: 2, done: false }
console.log(iter.next()); // 输出:{ value: 3, done: false }
console.log(iter.next()); // 输出:{ value: 4, done: false }
console.log(iter.next()); // 输出:{ value: 5, done: false }
console.log(iter.next()); // 输出:{ done: true }

2.5 链式调用

链式调用是一种编程风格,通过在对象上连续调用多个方法,使得代码看起来像是一条链条。每个方法都会返回对象本身,使得可以在同一个表达式中连续调用多个方法。如 Promiselodash库都有体现这样的风格。
这里给出一个链式调用函数,包含了 二次方、三次方、取反、随机数、取值等操作。

function Chainable(value) {
  // result 维护当前的运算值
  let result = value;

  this.square = function() {
    result = Math.pow(result, 2);
    return this;
  };

  this.cube = function() {
    result = Math.pow(result, 3);
    return this;
  };

  this.negate = function() {
    result = -result;
    return this;
  };

  this.random = function() {
    result = Math.random() * result;
    return this;
  };

  this.value = function() {
    return result;
  };
}

// 创建可链式调用对象
const chain = new Chainable(5);

const value = chain.square().cube().negate().random().value();
console.log(value); // 示例输出:-239.40167432539412

当然也可以换一种写法:

function chainable(val) {
  // result 维护当前的运算值
  let result = val;

  const square = function () {
    result = Math.pow(result, 2);
    return this;
  };

  const cube = function () {
    result = Math.pow(result, 3);
    return this;
  };

  const negate = function () {
    result = -result;
    return this;
  };

  const random = function () {
    result = Math.random() * result;
    return this;
  };

  const value = function () {
    return result;
  };

  return {
    square,
    cube,
    negate,
    random,
    value
  }
}

const value = chainable(5).square().cube().negate().random().value();
console.log(value); // 示例输出:-239.40167432539412

2.6 发布订阅模式

发布订阅模式是一种常见的设计模式,它用于在不同的对象之间建立松散耦合的联系。该模式包含两个核心概念:发布者(Publisher)订阅者(Subscriber)。发布者负责发布事件和通知订阅者,而订阅者则负责订阅事件并接收通知。
下面我通过闭包实现如上功能,因为结构比较复杂,所以注释写的非常详细,如果大家还看不懂的话,可以移步其他大佬的文章~

function eventEmitter() {
  // events 维护各事件以及对应的订阅者
  const events = {};

  // on 函数绑定订阅者(callback)至相应的事件(eventName)
  function on(eventName, callback) {
    // 如果 events 不存在该事件则创建该事件并赋值一个空数组存放订阅者
    events[eventName] = events[eventName] || [];
    // 存入订阅者
    events[eventName].push(callback);
  }

  // emit 函数发布事件(eventName),并传递相关参数(args)给订阅者
  function emit(eventName, ...args) {
    // 赋值订阅者数组
    const callbacks = events[eventName];
    if (callbacks) {
      callbacks.forEach(callback => callback(...args));
    }
  }

  // off 函数解除订阅关系
  function off(eventName, callback) {
    if (!callback) {
      // 订阅者为空,直接删除事件
      delete events[eventName];
    } else {
      // 订阅者不为空,筛选订阅者
      events[eventName] = events[eventName].filter(cb => cb !== callback);
    }
  }

  return { on, emit, off };
}

// 使用示例
const emitter = eventEmitter();

function handler1(name) {
  console.log(`${name} says hello from handler1`);
}

function handler2(name) {
  console.log(`${name} says hello from handler2`);
}

emitter.on('hello', handler1);
emitter.on('hello', handler2);

emitter.emit('hello', 'Alice'); // 输出 "Alice says hello from handler1" 和 "Alice says hello from handler2"

emitter.off('hello', handler1);
emitter.emit('hello', 'Bob'); // 只输出 "Bob says hello from handler2"

2.7 缓存

闭包还可以实现缓存的效果,下面的例子我用 timeGap 来体现一下缓存的作用

function memoize(fn) {
  const cache = {}; // 缓存对象,用于存储函数执行结果

  return function (...args) {
    const startTime = performance.now()
    const key = JSON.stringify(args); // 将参数序列化为字符串作为缓存的键

    if (cache[key] === undefined) {
      // 如果缓存中没有该键对应的结果,则执行函数并将结果缓存起来
      cache[key] = fn.apply(this, args);
    }

    // 获取时间差 判断是否有缓存
    console.log('timeGap', performance.now() - startTime)
    return cache[key]; // 返回缓存的结果
  };
}

// 没啥意义的函数,纯属跑循环延长函数运行时间
const getTime = memoize((times) => {
  let a = 0
  for (let i = 0; i < times; i++) {
    a++
  }
  return times
})

getTime(10000000) // timeGap 4.4000000059604645
getTime(20000000) // timeGap 8
getTime(20000000) // timeGap 0

三、闭包的缺点

先上结论:

  • 闭包对外部函数有引用时,若闭包被调用且未及时解绑,则会造成外部函数的变量无法被释放,导致内存泄露
  • 闭包涉及作用域链查找,性能相较直接访问局部、全局变量要低一些,在一些频繁调用或要求高性能的场景不适用
  • 闭包可以访问外部函数中的私有变量,这可能导致信息泄露和安全问题。如果闭包被滥用或不当使用,可能会导致数据被意外泄露给未授权的代码。

就这三个问题,我们依次还原案例并给出解决方案

3.1 内存泄露

下面我们仅针对于闭包造成的内存泄露给出案例:

function outer() {
  let count = 0;
  
  function inner() {
    count++;
    console.log(count);
  }
  
  return inner;
}

// 创建闭包
const fn = outer();

// 调用多次,但没有及时解绑闭包
fn();
fn();
fn();
fn();
// ...

// 结果:count 变量无法被释放,造成内存泄漏

解决方案:

  • 将闭包函数设置为 null:fn = null;
  • 将闭包函数重新赋值:fn = outer();

3.2 作用域链查找

作用域和作用域链也是常问的点,这边的例子延用上例的 outer 函数

// 创建闭包 
const fn = outer();
// 调用多次,性能受到影响 
for (let i = 0; i < 10000; i++) {
  fn();
}

解决方案:

  • 考虑使用其他的编程模式或技术替代闭包
  • 将其应用范围限制在必要的情况下,并在可能的情况下将访问局部、全局变量的次数降到最低
  • 将闭包函数的执行结果缓存(这里可以用闭包实现缓存)起来,以便减少性能开销。

3.3 信息泄露与安全

体现的案例与4.1相同,如果 count 为隐私数据,则可能触发问题。
解决方案:

  • 将敏感信息存储在非闭包变量中,并仅向必要的代码公开(例如使用访问控制权限管理技术)
  • 避免其他代码访问到闭包中的变量,可以使用立即执行函数将闭包函数包装起来,并将其返回值设置为一个包含公开接口的对象,只有这些公开接口才能访问到闭包变量,可以有效地保护闭包中的私有信息。

标签:闭包,function,return,函数,谈到,result,const,高阶
From: https://www.cnblogs.com/caihongmin/p/17772699.html

相关文章

  • JavaScript中高阶函数的巧妙运用
    JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数,本文介绍了JS中一些高阶函数的妙用,希望对大家有所帮助目录1.接受函数作为参数的高阶函数2.返回函数的高阶函数3.同时接受和返回函数的高阶函数JavaScript中的高阶函数是指可以接受其他函数作为参......
  • 装饰器、闭包
    用到了老是忘记,还是记录一下吧,装饰器、闭包python的装饰器、闭包是进入Python高级语法的基础,使用装饰器之前,有以下条件:存在闭包存在需要被装饰的函数理解函数地址的概念理解函数的地址值众所周知,我们定义函数后,函数名加()可以调用函数,那么我们尝试调用一下函数名呢?def......
  • Go 匿名函数与闭包
    Go匿名函数与闭包匿名函数和闭包是一些编程语言中的重要概念,它们在Go语言中也有重要的应用。让我们来详细介绍这两个概念,并提供示例代码来帮助理解。目录Go匿名函数与闭包一、匿名函数(AnonymousFunction)二、闭包函数(Closure)一、匿名函数(AnonymousFunction)匿名函数,也称为无......
  • 线段树高阶学习指南
    前置芝士线段树基本框架区间求和constintN=100010;lla[N],st[N*4],f[N*4];intn,q;//向上传voidpushup(llu){st[u]=st[lc]+st[rc];}//向下传voidpushdown(llu,lll,llr,llmid){if(f[u]){st[lc]+=f[u]*(mid-l+1);st[rc]+=f[u]*(r-m......
  • 深入浅出JavaScript闭包
    什么是JS闭包?JS闭包是一个难点也是JS的特色,是JS的高级特性。首先我们知道JS运行函数的时候会在内存中开辟一个存储空间,会把函数体内的代码当作字符串一摸一样的放在这个空间中,把这个空间地址赋值给函数名(变量名),当我们调用函数的时候会根据地址找到这个储存空间,然后执行储存空......
  • 闭包使用场景
    闭包在JavaScript中有许多应用场景,它们可以帮助你解决各种问题,包括封装数据、创建模块、处理异步操作等。以下是一些常见的闭包应用场景:封装私有变量和方法:使用闭包可以创建对象,其中包含私有成员变量和方法,这些成员对外部代码不可见。这有助于实现信息隐藏和数据封装。functi......
  • Day16 函数对象--函数嵌套调用--闭包函数
    1.Day15_复习1: 2.Day15_复习2: 3.Day15_复习3: 4.函数对象_可以赋值_可以当做函数参数传给另外一个函数: 5.函数对象_可以当做函数另外一个函数的返回值_可以当做容器类型的一个元素: 6.函数对象初步实现ATM流程: 7.函数对象应用案例优化: 8.函数的嵌套调用: 9.......
  • Go函数全景:从基础到高阶的深度探索
    在本篇文章中,我们深入探索了Go语言中的函数特性。从基础的函数定义到特殊函数类型,再到高阶函数的使用和函数调用的优化,每一个部分都揭示了Go的设计哲学和其对编程效率的追求。通过详细的代码示例和专业解析,读者不仅可以掌握函数的核心概念,还能了解如何在实践中有效利用这些特性来......
  • [完结16章]React18内核探秘:手写React高质量源码迈向高阶开发
    点击下载——[完结16章]React18内核探秘:手写React高质量源码迈向高阶开发  提取码:8epr手写React高质量源码,迈向高阶开发React18内核探秘:手写React高质量源码迈向高阶开发batching批处理,说的是,可以将回调函数中多个setState事件合并为一次渲染,因此是异步的。解决的问题是......
  • GO数组解密:从基础到高阶全解
    在本文中,我们深入探讨了Go语言中数组的各个方面。从基础概念、常规操作,到高级技巧和特殊操作,我们通过清晰的解释和具体的Go代码示例为读者提供了全面的指南。无论您是初学者还是经验丰富的开发者,这篇文章都将助您更深入地理解和掌握Go数组的实际应用。关注公众号【TechLeadClou......