在javascipt的学习和编码过程中,闭包是一个特别重要的知识点,学好和弄懂了闭包对于后续的开发过程非常有用。
定义
闭包是指这样的作用域,它包含有一个函数,这个函数可以调用被这个作用域所封闭的变量、函数或者闭包等内容。通常我们通过闭包所对应的函数来获得对闭包的访问。
为了便于理解,我们先来看看下面的demo
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();
函数foo执行后,通常会期待foo的整个内部作用域被销毁,因为引擎有垃圾回收机制来释放不再使用的内存,看上去foo的内容不再被使用,所以会自然的考虑对其进行回收。闭包的神奇之处在于他可以阻止这件事情的发生,事实上执行foo后,其内部作用域依然存在,因为外部的baz保留着对内部作用域中bar函数的引用,并且bar能够访问foo作用域内的变量,函数等。
接下来,我们再看看下面的例子
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar(); //2
}
上面的定义算闭包么,从技术上来看,是闭包,但是考虑到闭包的实际用途,不算闭包。当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行。重点是记住作用域,并在词法作用域之外访问词法作用域内的变量和函数。上面的例子,内部bar的执行在当前词法作用域内就执行了,没有体现出闭包的价值。
继续往下走,你觉得立即执行的函数算闭包么。
(function () {
var a = 1;
function run() {
console.log(a);
}
run();
})();
根据上面的解释,从技术上来说,是闭包,但是考虑到实际的用途,不算闭包,如果做如下更改,那不管怎么说,他都是闭包。
(function () {
var a = 1;
setTimeout(function run() {
console.log(a);
}, 0);
})();
深入到引擎的内部原理中,内置的工具函数 setTimeout(..) 持有对一个参数的引用,这个参数也许叫作 fn 或者 func,或者其他类似的名字。引擎会调用这个函数, 在例子中就是内部的run函数,而词法作用域在这个过程中保持完整。
实际上闭包在我们的代码中随处可见,比如在定时器,事件监听器,ajax请求中都可以找到闭包的影子。
循环和闭包
为了说明闭包,不得不谈谈for循环
for(var i = 0; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
正常情况下,我们期待着能输出0-5,但是事与愿违,我们得到的是6个6。事实上延迟函数中的回调会放在另一个任务队里中,待当前的js任务执行结束之后,才会去执行,换句话说,timer回调的执行时异步的,会在for循环结束以后才执行,那么问题就来了,此时的i是多少呢?没错,就是6,所以我们得到了6个6。
我们会假设循环中的每个迭代在执行时会捕获一个i的副本,但是根据作用域的工作原理,尽管循环中的每个函数都是在迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
这时候聪明的你,或许已经想到了,我们可以通过立即执行的函数来创建一个私有所有用。
for (var i= 1; i<= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, 0);
})(i);
}
通过上面的修改,我们就可以输入6个6了。每次迭代,立即执行函数都会生成一个新的作用域,这个作用域就是一个闭包,通过闭包内的timer函数,我们可以访问到作用域中的保存的变量i.
仔细分析我们的解决方案,每次迭代都创建了一个新的作用域,换句话说,每次迭代我们都需要一个块作用域。ES6中的let申明,正好可以完成这项任务。
for (var i= 1; i<= 5; i++) {
let j = i;
setTimeout(function timer() {
console.log(j);
}, 0);
}
当然了,我们还可以这样写
for (let i= 1; i<= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
不过,后一种写法,有一个特殊的行为。for循环头部的let声明,每次迭代都会声明,以后的每次迭代都会使用上一次迭代结束时的值来初始化这个变量。
模块
模块化可以算得上是闭包最好的应用了
function model() {
var name = 'jason';
function getName() {
return name;
}
function setName(newName) {
name = newName;
}
//public api
return {
getName,
setname
}
}
稍微改造一下,就可以变成单例模式
var model = (function () {
var name = 'jason';
function getName() {
return name;
}
function setName(newName) {
name = newName;
}
//public api
return {
getName,
setname
}
})();
闭包中的this问题
或许你会从各种书籍或者博客中看到,闭包中的this指向window,真的是这样么,真的指向window么?
function foo() {
function() {
console.log(this);
}
}
var bar = foo();
bar() // 输出window对象
function foo() {
var o = { a: 1};
function bar() {
console.log(this);
}
return bar.bind(o);
}
var baz = foo();
baz() // 输出o {a: 1}
this是一个有趣的问题,我们不能简单的认为闭包中的this指向window,除了上面的例子,还有DOM事件回调中的this,也不指向window,关于this的问题,以后会单独讨论,这次就不赘述了。
性能问题
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次,也就是说,为每一个对象的创建。
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;
}
};
贪玩儿的小灰灰感谢您的耐心阅读,如果有问题记得留言哦 =_=
公众号 | 贪玩儿的小灰灰
- 每周都会推送原创、有用、有趣的旅行图文和攻略。