文章目录
JavaScript 闭包
什么是闭包?
闭包是指函数和其相关的引用环境组合而成的一个整体,其中函数可以访问其外部作用域中的变量和函数,即使在函数执行完毕之后,这些变量和函数依然可以被访问。
简单来说,闭包就是一个函数能够访问定义在其外部的变量和函数,即使这些外部变量和函数已经不在当前作用域中,也依然可以访问和使用它们。
闭包的作用
闭包可以用于实现一些高级的编程技巧,常见的应用场景包括:
- 封装变量:可以利用闭包来封装私有变量,从而实现数据的封装和隐藏。
- 延续局部变量寿命:闭包可以让局部变量的生命周期被延长,将一些相关的功能封装在一个闭包内部,从而避免命名空间的污染和变量的冲突。
闭包的实现
在 JavaScript
中,闭包实际上是由函数和其相关的引用环境组合而成的。
当一个函数被定义时,它会创建一个作用域链,其中包含了函数定义时所在的作用域以及所有包含该函数的作用域。当函数执行时,它会通过作用域链来访问外部的变量和函数。如果一个函数内部定义了另一个函数,并且内部函数可以访问外部函数的变量和函数,那么这个内部函数就形成了一个闭包。
下面是一个简单的闭包示例:
function outerFunction() {
var outerVariable = 'Hello';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
var inner = outerFunction();
inner(); // 输出 'Hello'
这个例子中,innerFunction
内部定义了一个函数 console.log(outerVariable)
,该函数可以访问外部函数 outerFunction
中的变量 outerVariable
。当 outerFunction
被调用时,它会返回 innerFunction
,此时 outerFunction
已经执行完毕,但由于 innerFunction
形成了一个闭包,因此它仍然可以访问 outerVariable
变量。
闭包的应用
封装变量
闭包可以用于封装变量,从而实现数据的封装和隐藏。在 JavaScript
中,变量的作用域是函数级别的,因此可以通过闭包来创建私有变量。
下面是一个简单的封装变量的示例:
function createCounter() {
var count = 0;
return {
increment: function() {
count++;
},
decrement: function() {
count--;
},
getCount: function() {
return count;
}
};
}
var counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出 2
在这个例子中,我们定义了一个 createCounter
函数,该函数返回一个包含三个方法的对象,这三个方法都可以访问 count
变量。由于 count
变量是在 createCounter
函数内部定义的,因此它对于外部代码来说是不可见的,从而实现了数据的封装和隐藏。
延续局部变量寿命
闭包可以让局部变量的生命周期被延长,从而在函数执行完毕后依然可以使用这些变量。这种技巧常常被用于事件处理函数中。
下面是一个简单的延续局部变量寿命的示例:
function createButton() {
var count = 0;
var button = document.createElement('button');
button.innerHTML = '点击我';
button.addEventListener('click', function() {
count++;
console.log('按钮被点击了' + count + '次')
});
return button;
}
var button = createButton();
document.body.appendChild(button);
在这个例子中,定义了一个 createButton
函数,该函数返回一个按钮元素并注册了一个点击事件处理函数。在点击事件处理函数中,我们可以访问 count
变量,并且该变量的生命周期被延长到了事件处理函数执行完毕之后,从而实现了在多次点击按钮的情况下,保持计算器的值在上一次基础上 +1
。
实现模块化
闭包可以用于实现模块化的编程方式,将一些相关的功能封装在一个闭包内部,从而避免命名空间的污染和变量的冲突。
下面是一个简单的模块化示例:
var myModule = (function() {
var count = 0;
function increment() {
count ++;
}
function decrement() {
count --;
}
function getCount() {
return count;
}
return {
increment: increment,
decrement: decrement,
getCount: getCount
};
})();
myModule.increment();
myModule.increment();
console.log(myModule.getCount()); // 输出 2
在这个例子中,我们定义了一个匿名函数,并将其立即执行,从而创建了一个闭包。在闭包中,我们定义了一些私有函数和变量,这些函数和变量对于外部代码来说是不可见的。通过返回一个包含这些私有函数的对象,我们可以将这些函数暴露给外部代码使用,从而实现了模块化的编程方式。
柯里化
柯里化是指将一个接收多个参数的函数转换为一个接收单一参数的函数序列的过程。柯里化可以让函数更加灵活和可重用。
下面是一个简单的柯里化示例:
function add(a, b) {
return a + b;
}
function curry(fn) {
return function(a) {
return function (b) {
return fn(a, b);
};
};
}
var curriedAdd = curry(add);
console.log(curriedAdd(1)(2)); // 输出 3
在这个例子中,我们定义了一个 add
函数,该函数接受两个参数并返回它们的和。我们还定义了一个 curry
函数,该函数接受一个函数并返回一个新的函数,该新函数接受一个参数并返回另一个函数,最终返回结果是调用原始函数并传递所有的参数。通过使用柯里化,我们可以将一个接受多个参数的函数转换为一个接受单一参数的函数序列,从而让代码更灵活和可重用。
函数记忆
函数记忆是指将一个函数的结果缓存起来,以便在后续调用时直接返回缓存的结果,从而提高函数的性能。
下面是一个简单的函数记忆示例:
function fibonacci(n) {
if (n < 2) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
function memoize(fn) {
var cache = {};
return function(n) {
if (cache[n] !== undefined) {
return cache[n];
}
var result = fn(n);
cache[n] = result;
return result;
};
}
var memoizedFibonacci = memoize(fibonacci);
console.log(memoizedFibonacci(10)); // 输出 55
在这个例子中,我们定义了一个 fibonacci
函数,该函数接受一个整数 n
并返回斐波那契数列的第 n
项。我们还定义了一个 memoize
函数,该函数接受一个函数并返回一个新的函数,该新函数会缓存结果并在后续调用时直接返回缓存的结果。通过使用函数记忆,我们可以避免重复计算,从而提高函数的性能。
函数组合
函数组合是指将多个函数组合成一个函数,使得每个函数的输出都可以作为下一个函数的输入。函数组合可以让代码更加简洁和易于理解。
下面是一个简单的函数组合示例:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function compose(fn1, fn2) {
return function(a, b, c) {
return fn2(fn1(a, b), c);
};
}
var addMultiply = compose(add, multiply);
console.log(addMultiply(1, 2, 3)); // 输出 9
在这个例子中,定义了两个函数 add
和 multiply
,分别用于加法和乘法运算。我们还定义了一个 compose
函数,该函数接受两个函数并返回一个新的函数,该新函数会将两个函数组合起来,并依次传递参数。通过使用函数组合,可以将多个函数组合成一个函数,使得代码更加简洁和易于理解。
偏函数
偏函数是指固定一个函数的部分参数,从而得到一个新的函数。偏函数可以让函数更加灵活和可重用。
下面是一个简单的偏函数示例:
function add(a, b, c) {
return a + b + c;
}
function partial(fn, ...args) {
return function(...moreArgs) {
return fn(...args, ...moreArgs);
};
}
var addOne = partial(add, 1);
console.log(addOne(2, 3)); // 输出 6
在这个例子中,我们定义了一个 add
函数,该函数接受三个参数并返回它们的和。我们还定义了一个 partial
函数,该函数接受一个函数和一些参数,并返回一个新的函数,该新函数会将传递的参数和新的参数和并,并调用原始函数。通过使用偏函数,我们可以固定一个函数的部分参数,从而得到一个新的函数,使得代码更加灵活和可重用。
手动实现私有变量
在 JavaScript
中,我们通常使用闭包来实现私有变量。通过将变量定义在函数内部,并将访问变量的函数返回,我们可以实现私有变量的效果。
下面是一个简单的手动实现私有变量的示例:
function createPerson(name) {
var age = 18;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
setAge: function(newAge) {
age = newAge;
}
};
}
var person = createPerson('John');
console.log(person.getName()); // 输出 "John"
console.log(person.getAge()); //输出 18
person.setAge(30);
console.log(person.getAge()); // 输出 30
在这个例子中,我们定义了一个 createPerson
函数,该函数接受一个字符串 name
并返回一个对象,该对象包含一个 getName
方法和一个 getAge
方法,以及一个 setAge
方法用于修改年龄。在函数内部,还定义了一个私有变量 age
,该变量只能通过返回的对象中的方法进行访问和修改。通过使用闭包,我们可以手动实现私有变量,从而保护数据的安全性。
闭包内存泄漏问题
闭包会在以下场景出现内存泄漏问题。
循环引用
当一个函数形成闭包后,它会引用其外部函数中的变量和函数,而这些变量和函数又会引用闭包函数中的变量和函数,从而形成循环引用。如果这些变量和函数被长期占用,就会导致内存泄漏。以下是一个简单的循环引用的示例:
function createClosure() {
var data = 'Hello';
return function() {
console.log(data);
};
}
var closure1 = createClosure();
var closure2 = createClosure();
closure1.otherClosure = closure2;
closure2.otherClosure = closure1;
在这个例子中,closure1
和 closure2
形成了循环引用,从而导致了内存泄漏。
未释放的 DOM 引用
当闭包函数中引用了 DOM
元素,并且这些元素长期被占用时,就会导致内存泄漏。因为 DOM
元素被 JavaScript
引用时,会一直存在于内存中,直到被释放。以下是一个简单的未释放的 DOM
引用的示例:
function createClosure() {
var element = document.getElementById('myElement');
return function() {
console.log(element.innerHTML);
};
}
var closure = createClosure();
在这个例子中,闭包函数 closure
引用了一个 DOM
元素 myElement
,并且这个元素长期被占用,从而导致了内存泄漏。
未释放的定时器和事件监听器
当闭包函数中注册了定时器或事件监听器,并且这些定时器或事件监听器未被正确释放时,就会导致内存泄漏。因为定时器会事件监听器在注册后会一直存在于内存中,直到被取消。以下是一个简单的未释放的定时器和事件监听器的示例:
function createClosure() {
var timerId = setInterval(function() {
console.log('Hello');
}, 1000);
document.addEventListener('click', function() {
console.log('Clicked');
});
return function() {
clearInterval(timerId);
};
}
var closure = createClosure();
在这个例子中,闭包函数 closure
注册了一个定时器和一个点击事件监听器,并且这些定时器和事件监听器未被正确释放,从而导致了内存泄漏。
为了避免闭包导致的内存泄漏问题,应该尽量避免循环引用,释放不再使用的 DOM
引用、及时取消定时器和事件监听器等。(将不需要的闭包赋值为 null
可以让垃圾回收机制回收相关的内存)