# 作用域和闭包
# 作用域
作用域是一个包含了变量、对象、函数的集合或区域。作用域又分全局作用域和局部作用域,局部可以访问全局和他所有的父级作用域,层层向上访问就形成了一条作用域链。
# 闭包的原理
如果一个函数会在其父级函数返回之后留住对父级作用域的链接的话,闭包就会被创建。如父级函数通过return一个子函数,让这个子函数保留住了对父级函数里变量的访问权。又或者在父级函数里通过把子函数赋值给一个全局变量,子函数能访问到父级函数的变量,这个被赋值的全局变量也能访问到父级函数里的变量,这样也保留了对父级函数变量的访问权。
var a = 'global';
var f = function() {
var b = 'local';
var n = function() {
var c = 'inner local';
return b;
}
return n;
}
var inner = f();
inner() // local
// inner是新的全局函数,是f执行后返回的函数n,n有父级函数b的访问权,自然inner也就包留了对父级函数b的访问权。所以就产生了闭包。
简单点讲闭包就是能够读取其他函数内部变量的函数。闭包保留住了访问父级作用域的链接,所以每个函数本身也是一个闭包,因为它都有访问全局作用域的权限。
闭包产生,保留的对父级作用域访问权,是一条作用域链(一个引用),所访问的是作用域本身,而不是在函数定义时该作用域中的变量或变量当前所返回的值。
function f() {
var arr = [];
for(var i = 0; i < 3; i++) {
arr[i] = function() {
return i;
}
}
return arr;
}
var arr = f();
arr[0](); // 3
arr[1](); // 3
arr[2](); // 3
arr数组里每个元素被赋值为一个函数,每个函数保留了对父级函数f的作用域的访问权,这就创建了三个闭包。最后执行返回的是最终值,却不是i的当前值,因为我只保留了访问的链接不是它的确切值,所以当我们执行arri这个函数时,for循环已经跑完了,i最后的值变为3了,所以最后返回值也都是3了。如果for循环里是立即执行的语句,那么就不会出现这种结果。
# 闭包的优缺点
优点
- 保护数据:闭包可以保护函数内部的数据,使其不受外部干扰,提高代码的安全性。
- 延长函数生命周期:闭包可以使函数的生命周期延长,使得函数内部的变量在函数执行完毕后依然存在,方便之后的调用和使用。
- 实现函数的记忆化:闭包可以将函数的计算结果缓存起来,避免重复计算,提高执行效率。
- 函数递归调用:闭包可以用于实现函数的递归调用,通过自身引用的方式来递归调用函数。
缺点
- 内存占用:闭包会持有外部环境的引用,可能导致内存的占用较多,特别是对于长时间存在的闭包,需要注意内存泄漏的问题。
- 性能消耗:由于闭包涉及到变量的查找和作用域链的操作,相比常规函数调用,会稍微增加一些性能消耗。
- 内存泄漏:由于对外部变量的引用而导致这些变量无法被垃圾回收机制释放,从而造成内存泄漏。。
解决内存泄漏
比如引入父函数中的变量,从而导致这个变量无法被释放,可以在父函数中添加一个函数手动消除这些变量。
function createClosure() {
let value = 'Hello';
// 闭包函数
var closure = function() {
console.log(value);
};
// 解绑定闭包函数,并释放资源
var releaseClosure = function() {
value = null; // 解除外部变量的引用
closure = null; // 解除闭包函数的引用
releaseClosure = null; // 解除解绑函数的引用
};
// 返回闭包函数和解绑函数
return {
closure,
releaseClosure
};
}
// 创建闭包
var closureObj = createClosure();
// 调用闭包函数
closureObj.closure(); // 输出:Hello
// 解绑闭包并释放资源
closureObj.releaseClosure();
// 尝试调用闭包函数,此时已解绑,不再引用外部变量
closureObj.closure(); // 输出:null
# 应用场景
# 1、防抖节流
// 节流函数封装
function throttle(func, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, delay);
}
};
}
// 防抖函数封装
function debounce(func, delay) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
# 2、函数柯里化
函数柯里化是一种将多个参数的函数转换为一系列接受单个参数的函数的过程。比如有一个原始函数add(a, b, c),将它柯里化为addCurried(a)(b)(c)的形式。
//柯里化前
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); //6
//柯里化后
function addCurried1(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
//箭头函数简写
const addCurried2 = (a) => (b) => (c) => a + b + c;
console.log(addCurried1(1)(2)(3)); //6
console.log(addCurried2(1)(2)(3)); //6
# 3、链式调用
一个对象如何重复调用自身方法,或者Promise如何一直.then,他们本质都是在调用方法后通过return返回该对象的实例就可以继续调用了。
// 计算器模拟
const calculator = {
count: 0,
add: function(num) {
this.count += num
return this
},
subtract: function(num) {
this.count -= num
return this
}
}
calculator.add(3).subtract(1)
console.log(calculator.count) // 2
# 4、迭代器
在父级函数维护一个值,每次执行自加1。
function setup(arr) {
var i = 0;
return function() {
return arr[i++] // 先赋值在++
}
}
var next = setup([1, 2, 3, 4, 5]) // setup执行完后,next保留了对setup作用域的访问权。
next() // 1
next() // 2
next() // 3
next() // 4
# 5 、getter和setter
利用产生闭包这一原理,我们可以给一个函数创建私有的getter和setter函数,这个函数的值只能通过这个两个函数来获取和改变。
var getValue, setValue;
(function () {
var secret = 0;
getValue = function() {
return secret;
}
setValue = function(v) {
secret = v;
}
})();
getValue(); // 0
setValue(123);
getValue(); // 123
// secret这个变量就只能通过getValue和setValue获取和设置
# 6、发布-订阅模式
function createPubSub() {
// 存储事件及其对应的订阅者
const subscribers = {};
// 订阅事件
function subscribe(event, callback) {
// 如果事件不存在,则创建一个新的空数组
if (!subscribers[event]) {
subscribers[event] = [];
}
// 将回调函数添加到订阅者数组中
subscribers[event].push(callback);
}
// 发布事件
function publish(event, data) {
// 如果事件不存在,则直接返回
if (!subscribers[event]) {
return;
}
// 遍历订阅者数组,调用每个订阅者的回调函数
subscribers[event].forEach((callback) => {
callback(data);
});
}
// 返回订阅和发布函数
return {
subscribe,
publish,
};
}
// 使用示例
const pubSub = createPubSub();
// 订阅事件
pubSub.subscribe("event1", (data) => {
console.log("订阅者1收到事件1的数据:", data);
});
pubSub.subscribe("event2", (data) => {
console.log("订阅者2收到事件2的数据:", data);
});
// 发布事件
pubSub.publish("event1", "Hello");
// 输出: 订阅者1收到事件1的数据: Hello
pubSub.publish("event2", "World");
// 输出: 订阅者2收到事件2的数据: World