一 概念
闭包是指有权访问另一个函数作用域中的变量的函数。
在JavaScript中,就是在一个函数内部创建另一个函数,内部函数为闭包。内部函数可以访问外部函数环境内的变量。
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
以上f2函数为闭包。 f2从f1中被返回后,它的作用域链被初始化为包含f1函数的活动对象和全局变量对象,而且,f1执行完后它的活动对象也不会被销毁,因为f2的作用域链还在引用这个活动对象(f1的作用域链被销毁,但是活动对象还在内存中),直到f2被销毁,f1的活动对象才会被销毁。
f1 = null; // 释放内存
function f1() {
var n = 999;
nAdd = function () { n += 1 }
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
n没有被销毁,所以再次调用result()会增加。
另nAdd定义时没有用var或者let和const,因此是全局变量,也是一个闭包,可以在外部调用。
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
二 用途
1. 使用闭包解决递归调用问题
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1)
}
}
var anotherFactorial = factorial
factorial = null
anotherFactorial(4) // 报错 ,因为最好是return num* arguments.callee(num-1),arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错,所以借助闭包来实现
//arguments.callee是一个指向正在执行函数的指针。可以解决以上问题,但是在严格模式下会报错。
function factorial(num) {
if(num<=1){
return 1;
}else{
return num * arguments.callee(num-1)
}
}
// 使用闭包实现递归
var newFactorial = (function f(num) {
if (num < 1) { return 1 }
else {
return num * f(num - 1)
}
}) //这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial
2. 模仿块级作用域
(function () {
// 这里是块级作用域
})()
现在多用es6的let和const
3. 访问私有变量
在函数中定义的变量为私有变量,闭包可以创建用于访问私有变量的公有方法,这种方法叫特权方法。
私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
3.1 使用构造函数/原型模式实现自定义类型的特权方法
构造函数:私有变量name在Person的每一个实例中都不同, 因此每次调用构造函数都会重新创建getName和setName。构造函数模式的缺点是针对每个实例都会创建同样一组方法,而使用静态私有变量来实现特权方法就可以避免这个问题。
function Person(name) {
this.getName = function () {
return name;
}
this.setName = function (vlaue) {
name = value;
}
}
var person = new Person("aaa")
console.log(person.getName()); //aaa
person.setName('bbb')
console.log(person.getName()); //bbb
原型模式:私有变量和函数是由实例共享的。变量name变成了静态的,由所有实例共享的属性。
(function () {
var name = "";
Person = function (value) {
name = value
}
Person.prototype.getName = function () {
return name;
}
Person.prototype.setName = function (value) {
name = value;
}
})();
var person1 = new Person("aaa");
console.log(person1.getName()); //aaa
person1.setName('bbb')
console.log(person1.getName()); //bbb
var person2 = new Person('ccc')
console.log(person1.getName()); //ccc
console.log(person2.getName()); //ccc
3.2 使用模块模式/增强的模块模式实现单例的特权方法
单例:只有一个实例的对象。
模块模式:
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
增强的模块模式: 适合那种实例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增强的情况。
var singleton = function () {
//私有变量和私有属性
var privateVatiable = 10;
function privateFunction() {
return false;
}
//创建对象
var object = new CustomType();
//添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function () {
privateVatiable++;
return privateFunction();
}
return object
}
三 使用闭包需要注意
1. 闭包只能取得外部函数中任何变量的最后一个值
function outer() {
var result = [];
for (var i = 0; i < 5; i++) {
result[i] = function () {
return i;
}
}
return result
}
var a = outer();
a[0]() //5
a[1]() //5
a[2]() //5
a[3]() //5
a[4]() //5
这里闭包函数引用了变量i,但是并没有立即执行。变量i用var声明的,作用域在外部函数outer中,等到闭包函数执行的时候,i已经变成了5。所以result中每一项打印的都是5。
解决方案1: 多用一层闭包
function outer() {
var result = [];
for (var i = 0; i < 5; i++) {
result[i] = (function (num) {
return function(){
return num;
}
})(i)
}
return result
}
var a = outer();
a[0]() //0
a[1]() //1
a[2]() //2
a[3]() //3
a[4]() //4
解决方案2:
function outer() {
var result = [];
for (var i = 0; i < 5; i++) {
(function(){
var item = i
result[item] = function(){
return item;
}
})()
}
return result
}
var a = outer();
a[0]() //0
a[1]() //1
a[2]() //2
a[3]() //3
a[4]() //4
解决方案3: i 用let来声明
function outer() {
var result = [];
for (let i = 0; i < 5; i++) {
result[i] = function(){
return i;
}
}
return result
}
var a = outer();
a[0]() //0
a[1]() //1
a[2]() //2
a[3]() //3
a[4]() //4
解决方案4: 用forEach
function outer() {
var result = [];
var arr = [0,1,2,3,4]
result = arr.forEach(function(i){
return i;
})
return result
}
2. this指向问题
匿名函数的执行环境具有全局性,因此其this对象通常指向window。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
}
};
alert(object.getNameFunc()()); //The Window
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
}
};
alert(object.getNameFunc()()); //My Object
3. 内存泄漏问题
function showId() {
var el = document.getElementById("app")
el.onclick = function () {
alert(el.id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
}
}
// 改成下面
function showId() {
var el = document.getElementById("app")
var id = el.id
el.onclick = function () {
alert(id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
}
el = null // 主动释放el
}
四 性能
因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,在处理速度和内存消耗方面对脚本性能具有负面影响,所以如果不是某些特定任务需要使用闭包,尽量避免闭包的使用。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
eg:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
但我们不建议重新定义原型。可改成如下例子:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
在前面的两个示例中,继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。
五 总结
当在函数内部定义了其他函数,就创建了闭包。
闭包的作用域包含它自己的作用域,包含函数的作用域和全局作用域。
使用闭包可以模仿块级作用域。
闭包可以用于在对象中创建私有变量。可以使用构造函数模式,原型模式实现自定义类型的特权方法,也可以使用模块模式,增强的模块模式实现单例的特权方法。
网友评论