闭包的定义
MDN定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。MDN闭包网址
闭包的定义并没有多好理解,但它一定是描述闭包最准确的话,我们就来逐字逐句的理解理解它。
函数在JS中的地位
定义的第一句话,头四个字是“一个函数”,我们就先说函数。
函数在js中是一等公民。一等公民(一级状态)意味着可以作为函数参数、函数返回值,也可以赋值给变量。 这极大的增加函数使用的灵活性。闭包、剪头函数、匿名函数都是函数是一等公民思想的应用。
所以说,函数的一等公民的地位,为闭包奠定了基础。
词法环境是什么
定义里说的是“一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起,这样的组合,就是闭包”。关键词“组合”,闭包分两部分,一个是函数,一个是周围状态的引用。所以接下来我们要研究,周围状态是什么?周围状态的引用又是什么?
周围状态又解释为词法环境。意思就是这里的周围是指函数在编写时的周围,而不是函数在运行时的周围。这样的周围状态叫做词法环境。“周围状态的引用”是指函数引用了外层函数中声明的变量。
闭包的能力
“也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。”
闭包可以做到这点,是因为在代码执行的过程中,执行到闭包所在的函数,会创建该函数的执行上下文,执行上下文会创建环境记录对象(以前叫变量对象),当前函数执行上下文中还会有一个作用域链,上面串着当前环境记录对象、外层环境记录对象、直到全局环境记录对象。注意函数的作用域链上有几个对象是写代码时就确定的,所以叫词法作用域,只是在代码真正运行的时候,环境记录对象们才真的被加载到内存中。
作用域和执行上下文的概念区别:作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的环境。也就是说作用域定义了执行上下文中的变量的访问规则,执行上下文在这个作用域规则的前提下进行变量查找,函数引用等具体操作。
闭包被创建的时机
“在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。”
定义中的这句话,说明了闭包被创建的时机。
词法作用域
MDN在介绍完定义后,紧接着介绍词法作用域,MDN应该是认为我们已经理解什么是作用域(在函数声明的时候就确定的一套变量访问规则),但是要强调一下,这个作用域是词法作用域。
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() {
// displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
这个例子,主要说明嵌套函数可访问声明于它们外部作用域的变量。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。
闭包示例
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
这个示例也是闭包的经典用法,displayName函数作为返回值给myFunc,myFunc是一个函数,它还维持了词法环境(变量 name 存在于其中)的引用,所以myFunc是个闭包——完美符合定义。
闭包的用处
闭包可以看成是一个只有一个方法的对象。对外暴露一个方法供他人使用,对内可以封装属性和方法不被外界看到。
1,对外:复用。
vue源码中有这样一句代码patch = createPatchFunction({ nodeOps, modules })。patch函数是 createPatchFunction的返回值。那么patch函数和它的词法环境就组成了一个闭包。patch我们熟悉,是用来把VNode 转换成真正的 DOM 节点。nodeOps, modules是跟平台相关的对象,比如nodeOps是操作界面上节点的对象。
- 在浏览器平台:vue就给 createPatchFunction传递一个操作浏览器DOM的nodeOps,生产一个可以转化成浏览器DOM的patch方法。
- 在Weex平台:vue就给 createPatchFunction传递一个操作手机界面DOM的nodeOps,生产一个可以转化成手机界面DOM的patch方法。 这个patch方法再交给Diff算法去使用,从而达到复用的效果。
2,对内:封装私有属性和私有方法。
下面例子中的name变量,在全局环境是访问不到的,要想访问,就得在makeFunc内部提供getName() { return name; }这样的方法。
function makeFunc() {
var name = "Mozilla";
function getName() {
return name;
}
return { getName };
}
var myFunc = makeFunc();
console.log(myFunc.getName()) // Mozilla
这就达到了封装的效果。
在循环中创建闭包
在循环中有一个常见的闭包创建问题。
for(var i = 0; i < 4 ; i++){
setTimeout(function(){
console.log(i)
}, i * 1000);
} // 因为有闭包,所以引用的变量i一直是外层环境的i。所以会输出4 4 4 4
如果想让输出结果是0 1 2 3
// 1. 使用es6的let,利用let可以利用块级作用域
for(let i = 0; i < 4; i++){
setTimeout(function(){
console.log(i)
}, i*1000)
}
// 2. 使用立即执行函数。
for (var i = 0 ; i <=4 ; i++) {
setTimeout((function(res){
return function(){
console.log(res);
};
})(i) , i * 1000);
}
总结
闭包之所以难以理解,是因为理解它还是要了解js其他方面的知识,对作用域、作用域链、执行上下文这些知识都了解后,在来看闭包,就容易的多。了解这些知识,也就是对js的运行,有了清楚的认识,闭包是建立在这些知识之上的。
这篇文章是为了加深我们对闭包的了解,它是一个组合(函数和其周围状态的引用)。了解它的形态和用处,就先介绍到这里,以后我有更深的了解再作补充。