其实闭包这个话题一直也是面试高频题,我在面试当中有 80% 的时候面试官会问我闭包的特性以及实际的应用场景。闭包也确实是 JavaScript 中的核心特性,在实际当中可以说你一直在使用闭包,只不过你并不知道这个是闭包。
举个例子,一个电子科技爱好者会经常做一些电子零件,但是他并没有上过学,也没有系统的学过这个专业。但是当他准备去大学系统的学习电子专业的时候,其实老师讲过的课他不需要听都可以听得懂,只不过是这些名词儿是他在实验中获得的信息,并不知道叫什么而已。随后只不过是一直的顿悟,知道了这些名字。这也是人们的智慧,给相应的事物提取出一个名字来命名,当大家聊起这个名字的时候,就知道,噢~原来他说的是这个...但是如果你不知道的话,当人们聊起的时候,你其实听不懂说的是什么,但其实你是知道这个的。
闭包也是一样,往下看看,其实你可能就知道了,也许是你在做项目的时候写过,也或许是你看到过类似的实现形式。
咳咳。。
用官方的话来说,闭包可以让你从内部函数访问外部函数作用域。
就是这么一句话就是闭包的精髓,但其实是听不懂的(至少我在学习 JavaScript 的时候,理解他的字面意思,但是并不知道是什么),我再说说我的理解。
比较官方的话语来说是,一个函数的内部变量被外部访问。
用白话来举个例子说是,小明买了一辆汽车,然后为了保护爱车(就像大家买了个新的 iPhone 贴膜一样),买了车窗的贴膜,而且为了私密性(没开车),贴了那种酷酷的黑色,而且这个膜是那种只能在里面看到外面,外面看不到里面的。这你可以理解成是个闭包。(核心一句话就是,可以从里面访问到外面,但是外面无法访问到里面)
好,我们接下来就讨论一下刚才开头说的, 在实际当中可以说你一直在使用闭包,只不过你并不知道这个是闭包。 这句话的含义。
大家都用过 function
来声明函数吧,其实呢,你的每一个 function
都是一个闭包,为什么呢?
我们知道,我们写在 JS 环境中直接声明一个变量,这个变量的作用域是在全局作用域下。
我们也知道,当声明了一个 function
时,在 function
中,当前的作用域就在函数作用域下。
所以当你声明一个函数时,并在函数当中声明了变量,处理了一些逻辑,其实这个就是闭包,只不过闭包还有一个要求(我们一会在看),你可以理解为这就是闭包。
所以:你的每一个 function 都可以理解为是闭包。
所以:在实际当中可以说你一直在使用闭包,只不过你并不知道这个是闭包。
闭包可以干嘛呢?
在 Web 中,你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。
编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
我们知道在 JS 中是只有全局作用域和函数作用域的(ES6 开始有块级作用域),如果一个变量只为特定的方法或者类来管理是不可以的,只能通过闭包的形式来做私有化变量。
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment
,Counter.decrement
和 Counter.value
。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter
的变量和名为 changeBy
的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter
变量和 changeBy
函数。
大家应该知道闭包的含义了吧,如果还不知道,那么你在看一遍?
还记得开头说过闭包还有一个要求吗?现在来一起看一下这个要求,定义是 定义的内部函数引用了父级(或更上级)作用域的变量
先来个 MDN 的例子
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
myFunc
是个高阶函数(Higher-Order Function),先定义了 myFunc
为 makeFunc()
,makeFunc()
方法返回了 displayName
函数,这个函数里引用了他的 父级
作用域变量 name
。此时如果调用了 myFunc()
的话,相当于会执行了 displayName()
,然后呢,displayName()
里执行了 alert
来访问父级作用域链的变量 name
,最终呢形成了一个闭包。
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
大家可能都听说过,闭包可能造成内存泄漏,导致程序卡死,崩溃。其实不然,如果你了解闭包,会使用闭包,注意一下闭包的特性,是不会出现这种问题的,这种问题一般都是 JavaScript 新手犯的错误,例如在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
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;
};