简历缺少有技术深度的项目吗?最近在做开源,实现一个脚手架,涉及广泛的工程化知识。
如果你感兴趣参与贡献,或者想加入社区聊聊技术、工作、八卦,可以添加我的联系方式:Tongxx_yj。
GitHub 链接:github.com/xun082/crea…
一、前言
前段时间参与字节面试,一面中问到我 闭包
,我照着自己的理解从闭包的特性到高阶函数,再提到了闭包的应用,如 curry函数
。
或许是因为我巴拉巴拉讲了挺多的,面试官大大在准备第二道面试题的时候,让我实现一个 curry函数
:
// 实现一个add函数
// return 1 + 2 + 3 + 2 传参为undefined时返回sum
add(1,2,3)(2)()
// 8
😶😶😶……本人只懂概念没有实操过啊😭👊
没办法,硬着头皮上,在面试官的循循善诱下终于ac了😭
这篇文章将会帮助大家了解闭包的概念、延伸概念、应用以及优劣,顺带解决这个题目。
二、闭包
2.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
的参数,可见原本在外部函数执行结束后本该销毁的地址得到了保留。
在这里我们总结一下闭包的特点:
闭包
可以访问和修改其创建时捕获的外部环境中的变量值,从而实现状态的保存和共享。闭包
可以私有化变量,从而避免命名冲突
和污染全局作用域
。闭包
可以延长变量的生命周期
,从而实现回调、事件处理等高级操作。
有的同学可能会联想到高阶函数
,或许对高阶函数
、闭包
存在不清晰的认知,那么我们就再来讲一下高阶函数
。
2.2 高阶函数
高阶函数
指的是能够接受函数作为参数或返回函数作为结果的函数,如 Array.prototype.map
,Array.prototype.filter
和 Array.prototype.reduce
就是高阶函数的实现。
老样子,我们用代码说明一下:
2.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!
2.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
明显可以看出闭包
就属于返回函数的一类,由此得知,闭包
是高阶函数
的一种特殊形式。
三、常见应用
接下来我们再来聊一聊关于闭包
的常见应用:
3.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);
};
}
3.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);
}
};
}
3.3 curry函数
柯里化(Currying)
是一种将多个参数的函数转换为接受单个参数的函数序列的技术。借此我们来完成前面提到的add函数(后面了解到这是一类很经典的面试题,纯属小子经验太少了🫠)
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
3.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 }
3.5 链式调用
链式调用
是一种编程风格,通过在对象上连续调用多个方法,使得代码看起来像是一条链条。每个方法都会返回对象本身,使得可以在同一个表达式中连续调用多个方法。如 Promise
、lodash
库都有体现这样的风格。
这里给出一个链式调用函数,包含了 二次方、三次方、取反、随机数、取值等操作。
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
3.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"
发布订阅模式还可以再完善一些,比如“一键清除事件”
、“订阅者只订阅一次”
等功能,有兴趣可以自行实现。
3.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
闭包的应用丰富多样,可以延申很多,如果有其他可以补充的也可以提醒一下我,才疏学浅难免会漏🫠。
那么接下来我们再回来看看闭包存在的一些问题。
四、闭包的缺点
先上结论:
闭包
对外部函数有引用时,若闭包被调用且未及时解绑,则会造成外部函数的变量无法被释放,导致内存泄露
闭包
涉及作用域链查找
,性能相较直接访问局部、全局变量要低一些,在一些频繁调用或要求高性能的场景不适用闭包
可以访问外部函数中的私有变量,这可能导致信息泄露和安全问题
。如果闭包被滥用或不当使用,可能会导致数据被意外泄露给未授权的代码。
就这三个问题,我们依次还原案例并给出解决方案
4.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();
4.2 作用域链查找
作用域和作用域链
也是常问的点(好家伙又埋坑)
这边的例子延用上例的 outer
函数
// 创建闭包
const fn = outer();
// 调用多次,性能受到影响
for (let i = 0; i < 10000; i++) {
fn();
}
解决方案:
- 考虑使用其他的编程模式或技术替代闭包
- 将其应用范围限制在必要的情况下,并在可能的情况下将访问局部、全局变量的次数降到最低
- 将闭包函数的执行结果缓存(这里可以用闭包实现缓存)起来,以便减少性能开销。
4.3 信息泄露与安全
体现的案例与4.1相同,如果 count
为隐私数据,则可能触发问题。
解决方案:
- 将敏感信息存储在非闭包变量中,并仅向必要的代码公开(例如使用
访问控制
和权限管理
技术) - 避免其他代码访问到闭包中的变量,可以使用
立即执行函数
将闭包函数包装起来,并将其返回值设置为一个包含公开接口
的对象,只有这些公开接口才能访问到闭包变量,可以有效地保护闭包中的私有信息。
五、总结
作为最最最经典的面试题之一,闭包的延伸内容确实很多,本文虽然以curry函数
开头,但它只是本文中想要讲述的一个知识点,更多还是因此重新认识到闭包的深度。
⭐⭐⭐与此同时,分享一个深入学习知识的方法——延伸学习
,从一个知识点出发,任何搭边的case都可以作为一个分支去丰富这个 map
,到后面可以发现我们对某领域的认知会越来越贯通,也可以更好的面对各种开发需求、面试官延伸的各类问题。(鄙人写文的思路也是如此~ 有兴趣的话还可以看看俺其他文章哈,感谢大佬们支持😊)
ps:有内推想法的也可以找我呀