闭包
什么是闭包?
作用域
什么是域?简单的说就是一个被圈起来的地方,也就是变量能够访问的一个范围。变量的作用域分为全局变量和局部变量。定义在函数外部的称为全局变量,在函数内部的称为局部变量。同时这里牵扯到一个知识点,变量提升 这个玩意儿,也就是和"先声明后使用差不多"
- 全局变量和局部变量 在这个示例中,globalVariable 是一个全局变量,它在整个脚本中都可以被访问。而 localVariable 是在 exampleFunction 函数内部声明的局部变量,只能在该函数内部访问。如果你尝试在函数外部访问 localVariable,会抛出 ReferenceError。 需要注意的是,使用 let 和 const 声明的变量具有块级作用域,这意味着它们的作用域被限制在声明它们的块(如 if 语句、for 循环等)内。例如:
js
// 全局变量
let globalLetVariable = '我是用 let 声明的全局变量';
if (true) {
// 块级局部变量
let blockLocalVariable = '我是用 let 声明的块级局部变量';
console.log(globalLetVariable); // 可以在块内访问全局变量
console.log(blockLocalVariable); // 可以在块内访问块级局部变量
}
console.log(globalLetVariable); // 可以在块外访问全局变量
console.log(blockLocalVariable); // 报错:ReferenceError: blockLocalVariable is not defined,不能在块外访问块级局部变量
// 在这个示例中,blockLocalVariable 是在 if 块内部声明的,它的作用域仅限于该块内。在块外部尝试访问它会导致 ReferenceError。
var a='Vi';
function fun(){
var b='Jia';
console.log('这是在函数内部的输出');
console.log(a);
console.log(b);
}
fun();//调用函数
console.log("这是在函数外部的输出");
console.log(a);
console.log(b);
// 运行结果:
// 这是在函数内部的输出
// Vi
// Jia
// 这是在函数外部的输出
// 报错:ReferenceError: b is not defined,不能在块外访问块级局部变量
// 最后一个输出b的时候抛出了一个未定义异常,由此可见,全局变量(a)在函数内部和外部都是能访问的,但是局部变量不是这样的,在函数作用域外是不能访问到函数内部的变量(b)的
- 在 JavaScript 里,变量提升指的是变量和函数的声明会被提升至当前作用域的顶部,这意味着你能在变量声明之前就使用它。不过,变量在声明之前的值为 undefined。下面是一个简单的示例代码:
js
console.log(a); // 输出 undefined
var a = 10;
console.log(a); // 输出 10
在这段代码里,var a 声明被提升到了作用域顶部,所以第一个 console.log(a) 不会报错,只是输出 undefined。待执行 a = 10 赋值操作后,第二个 console.log(a) 就会输出 10。
若使用 let 和 const 来声明变量,就不会有变量提升现象。在声明之前访问 let 或 const 声明的变量,会引发 ReferenceError。示例如下:
js
console.log(b); // 报错:ReferenceError: Cannot access 'b' before initialization
let b = 20;
-
嵌套函数的作用域
-
访问外部函数的变量
jsfunction outerFunction() { // 外部函数的变量 let outerVariable = '我是外部函数的变量'; function innerFunction() { // 嵌套函数可以访问外部函数的变量 console.log(outerVariable); } // 调用内部函数 innerFunction(); } // 调用外部函数 outerFunction(); // 结果: 我是外部函数的变量
在这个例子里,outerFunction 被调用后,内部的 innerFunction 也被调用,而 innerFunction 可以访问 outerFunction 中声明的 outerVariable 并将其打印出来。
-
闭包与作用域
jsfunction outerFunction() { let count = 0; function innerFunction() { // 每次调用内部函数时,count 的值会增加 count++; console.log(count); } return innerFunction; } // 调用 outerFunction 并将返回的 innerFunction 赋值给 increment const increment = outerFunction(); // 多次调用 increment 函数 increment(); // 输出 1 increment(); // 输出 2
这里 outerFunction 返回了 innerFunction 并赋值给 increment ,每次调用 increment 时,由于闭包的存在,它可以访问并修改 outerFunction 中的 count 变量,所以第一次调用输出 1 ,第二次调用输出 2。
-
闭包的基本定义
如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包。 闭包会创建一个包含外部函数作用域变量的环境,并将其保存在内存中,这意味着,即使外部函数已经执行完毕,闭包仍然可以访问和使用外部函数的变量。
js
//闭包实例代码
function fn1() {
let a = 1;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
const fn2 = fn1();
//闭包函数执行完后外部作用域变量仍然存在,并保持状态
fn2() //2
fn2() //3
-
保存变量 闭包能让函数访问并修改其外部函数作用域中的变量,即便外部函数已执行完毕。
jsfunction createMultiplier(factor) { return function (number) { return number * factor; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); console.log(triple(5));
闭包的优缺点及特性
- 闭包的优点:
- 保护变量: 闭包可以将变量封装在函数内部,避免全局污染,保护变量不被外部访问和修改。
- 延长变量生命周期: 闭包使得函数内部的变量在函数执行完后仍然存在,可以在函数外部继续使用。
- 实现模块化: 闭包可以创建私有变量和私有方法,实现模块化的封装和隐藏,提高代码的可维护性和安全性。
- 保持状态: 闭包可以捕获外部函数的变量,并在函数执行时保持其状态。这使得闭包在事件处理、回调函数等场景中非常有用。
- 闭包的缺点:
- 内存占用: 闭包会导致外部函数的变量无法被垃圾回收,从而增加内存占用。如果滥用闭包,会导致内存泄漏问题。
- 性能损耗: 闭包涉及到作用域链的查找过程,会带来一定的性能损耗。在性能要求高的场景下,需要注意闭包的使用。
- 闭包的特性:
- 函数嵌套: 闭包的实现依赖于函数嵌套,即在一个函数内部定义另一个函数。
- 记忆外部变量: 闭包可以记住并访问外部函数的变量,即使外部函数已经执行完毕。
- 延长作用域链: 闭包将外部函数的作用域链延长到内部函数中,使得内部函数可以访问外部函数的变量。
- 返回函数: 闭包通常以函数的形式返回,使得外部函数的变量仍然可以被内部函数引用和使用。
闭包的应用场景
读取函数内部的变量
在 JavaScript 里,函数内部的变量属于局部变量,外部无法直接访问。不过借助闭包,就能读取到函数内部的变量。
ini
```js
function outerFunction() {
let innerVariable = '我在函数内部';
return function() {
return innerVariable;
};
}
let closure = outerFunction();
console.log(closure());
```
在上述代码中,closure 作为闭包函数,能够访问 outerFunction 内部的 innerVariable。
自执行函数
js
let say = (function(){
let val = 'hello world';
function say(){
console.log(val);
}
return say;
})()
// 或者
(function() {
var message = '这是一个自执行函数';
console.log(message);
})();
让这些变量的值始终保持在内存中
闭包会把其外部函数作用域中的变量保存下来,即便外部函数执行完毕,这些变量也不会被销毁。
js
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
let counter = createCounter();
console.log(counter());
console.log(counter());
在这个例子中,createCounter 函数返回的闭包函数可以持续访问并修改 count 变量,每次调用 counter 时,count 的值都会增加。
实现函数私有变量和方法
闭包可以用来创建具有私有变量和方法的对象,这些私有成员只能通过特定的接口进行访问和修改。
js
function createPerson() {
let name = '张三';
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
};
}
let person = createPerson();
console.log(person.getName());
person.setName('李四');
console.log(person.getName());
在上述代码中,name 是 createPerson 函数内部的私有变量,外部无法直接访问,只能通过 getName 和 setName 方法来操作。
模拟块级作用域
在 ES6 之前,JavaScript 没有块级作用域,使用闭包可以模拟块级作用域,避免变量泄露。
js
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, j * 1000);
})(i);
}
通过立即执行函数创建了闭包,每个闭包都有自己独立的变量副本,从而避免了 var 带来的变量提升和循环问题。
节流防抖
防抖(Debounce)
概念 防抖是指在一定时间内,只有最后一次触发事件才会执行相应的处理函数。如果在这个时间间隔内再次触发事件,那么计时会重新开始。
实现思路 利用闭包保存定时器 ID,每次触发事件时,清除之前的定时器,重新设置一个新的定时器。当定时器计时结束后,执行处理函数。
js
function debounce(func, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
// 清除之前的定时器
clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
// 使用示例
function handleInput() {
console.log('输入事件触发');
}
const debouncedInput = debounce(handleInput, 300);
// 模拟高频输入事件
document.getElementById('input').addEventListener('input', debouncedInput);
节流(Throttle) 概念 节流是指在一定时间内,只执行一次事件处理函数。如果在这个时间间隔内多次触发事件,只有第一次触发会执行处理函数,后续的触发会被忽略,直到时间间隔结束。
实现思路 利用闭包保存上一次执行处理函数的时间,每次触发事件时,检查当前时间与上一次执行时间的差值是否大于设定的时间间隔。如果大于,则执行处理函数,并更新上一次执行时间;否则,忽略本次触发。
js
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// 使用示例
function handleScroll() {
console.log('滚动事件触发');
}
const throttledScroll = throttle(handleScroll, 500);
// 模拟高频滚动事件
window.addEventListener('scroll', throttledScroll);
-
防抖:适用于需要避免频繁触发的场景,如搜索框输入提示、窗口缩放等,确保只有用户停止操作一段时间后才执行相应的处理函数。
-
节流:适用于需要限制事件触发频率的场景,如滚动加载、按钮点击等,保证在一定时间内只执行一次处理函数,减轻服务器压力或提高性能。
函数柯里化
- 函数柯里化(Currying)是一种将多个参数的函数转换为一系列接受单个参数的函数的过程。举个简单的例子,我们有一个原始函数add(a, b, c),我们可以将它柯里化为addCurried(a)(b)(c)的形式。(闭包可以用于实现函数柯里化,将一个多参数函数转换为一系列单参数函数。)
js
//柯里化前
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
// add 函数通过闭包实现了柯里化,允许分步骤传递参数。
function add(a, b) {
if (arguments.length === 2) {
return a + b;
} else {
return function(b) {
return a + b;
};
}
}
let addFive = add(5);
console.log(addFive(3));
链式调用
- 利用闭包原理封装一个简单的计算器
js
function calculator() {
let result = 0;
function add(num) {
result += num;
return this;
}
function subtract(num) {
result -= num;
return this;
}
function multiply(num) {
result *= num;
return this;
}
function divide(num) {
result /= num;
return this;
}
function getResult() {
return result;
}
function clear() {
result = 0;
return this;
}
return {
add,
subtract,
multiply,
divide,
getResult,
clear,
};
}
const calc = calculator();
const result = calc.add(5).subtract(2).divide(3).multiply(6).getResult();
console.log(result); // 输出:6
闭包链式调用是一种编程模式,允许你在一个对象上连续调用多个方法,其实现的关键在于每个方法都返回对象本身。在 JavaScript 里,可借助闭包来实现链式调用。下面为你详细介绍实现思路和示例代码。
实现思路
- 对象方法返回自身:每个方法执行完操作后,返回 this(即对象本身),如此一来,后续方法就能继续在该对象上调用。
- 闭包保存状态:闭包能够保存对象的内部状态,使得不同方法可以访问和修改这些状态。
js
function Calculator() {
let result = 0;
// 加法方法
this.add = function (num) {
result += num;
return this;
};
// 减法方法
this.subtract = function (num) {
result -= num;
return this;
};
// 获取结果方法
this.getResult = function () {
return result;
};
}
// 使用示例
const calculator = new Calculator();
const finalResult = calculator.add(5).subtract(3).getResult();
console.log(finalResult);
代码解释
- Calculator 构造函数: 创建一个 Calculator 对象,内部有一个变量 result 用于保存计算结果。
- add 方法: 将传入的数字加到 result 上,然后返回 this,这样就能继续调用其他方法。
- subtract 方法: 从 result 中减去传入的数字,同样返回 this。
- getResult 方法: 返回最终的计算结果。
优点
- 代码简洁: 链式调用让代码更加简洁易读,减少了中间变量的使用。
- 操作连贯: 可以在一行代码中完成多个操作,使代码逻辑更加连贯。 通过闭包实现链式调用,能让代码更加优雅和高效。你还可以根据需求添加更多的方法,扩展链式调用的功能。
发布-订阅模式
js
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
闭包造成的内存泄漏怎么解决呢?
闭包中的内存泄漏指的是在闭包函数中,由于对外部变量的引用而导致这些变量无法被垃圾回收机制释放的情况。当一个函数内部定义了一个闭包,并且这个闭包引用了外部变量时,如果这个闭包被其他地方持有,就会导致外部变量无法被正常释放,从而造成内存泄漏。 解决闭包中的内存泄漏问题通常需要注意解除外部变量和闭包函数的引用,以及解绑函数本身的引用,使得闭包中引用的外部变量和闭包函数能够被垃圾回收机制释放。
- 以下是使用闭包时解决内存泄漏的示例
js
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
注:如有什么不当,或者侵权联系删除。