Javascript系统复习:闭包,作用域以及ES3执行上下文

Posted by mieruko on 2016-08-27

前言:

闭包和作用域,非常重要的一个点。
闭包的官方定义:

在计算机科学中,闭包(也称词法闭包或函数闭包)是指一个函数或函数的引用,与一个引用环境绑定在一起。这个引用环境是一个存储该函数每个非局部变量(也叫自由变量)的表。

闭包不同于一般的函数,它允许一个函数在立即词法作用域外调用时,仍可访问本地变量。

另外本文还有一个大boss是ES3的执行上下文,我会放在最后说。

闭包

一下子不知道怎么用通俗的语言解释,先写一个🌰:

1
2
3
4
5
6
function outer() {
var localVal = 30;
return localVal;
}

outer();

这就是一个一般的函数。
调用结束之后,局部变量就可以被释放了。

再看一个:

1
2
3
4
5
6
7
8
9
function outer() {
var localVal = 30;
return function() {
return localVal;
}
}

var func = outer();
func(); //30

我们把函数作为了一个返回值,对于这种情况localVal不能被释放。
我们返回的函数仍然可以访问outer函数的局部变量。
这种情况就可以被理解为闭包。

闭包的常见错误: 循环闭包

我们会在很多的闭包文章里面看到这样的代码:

1
2
3
4
5
6
7
document.body.innerHTML = "<div id='div1'>aaa</div>" +
"<div id='div2'>bbb</div>" + "<div id='div3'>bbb</div>";
for(var i=1;i<4;i++) {
document.getElementById("div"+i).addEventListener("click", function(){
console.log(i);
});
}

你的本意是,为每一个divn元素加上一个点击事件的监听函数,然后第一个点上去输出1,第二个点上输出2,第三个点上输出3.
然而事实是,它们全部输出了4.
这是因为,闭包的特点是,只有在你实际调用它的时候,它才会去寻找要用到的变量的值。
在这个例子里面,当我们去调用监听函数的时候,变量i的值已经变成4了啊。
所以这点要注意,如果我们希望实现这样的效果,最好还是把每个值用不同的变量保存一下,或者说,让函数立即执行。

比如这样:

1
2
3
4
5
6
7
8
9
10
document.body.innerHTML = "<div id='div1'>aaa</div>" +
"<div id='div2'>bbb</div>" + "<div id='div3'>bbb</div>";
for(var i=1; i<4; i++) {
!function(i) {
document.getElementById("div"+i).
addEventListener("click", function(){
alert(i); //1,2,3
});
}(i);
}

通过这种每次都单独调用的方法,我们使每一个i的值都能够顺利地保存下来。

闭包和封装

闭包可以帮助我们去封装私有变量:

🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function(){
var _userId = 23492;
var _typeId = "item";
var export = {};

function converter(userId) {
return +userId;
}

export.getUserId = function() {
return converter(_userId);
}
window.export = export;
})();

export.getUserId();
export.getTypeId();

export._userId; //undefined
export._typeId; //undefined
export._converter: // undefined

如此,其实多少有一点构造函数的意思,和构造函数不同的是,我们可以自己决定暴露给用户的接口是什么,私有的属性和方法,只要我们不暴露,那么用户除非去看源码,不然是一定找不到的~~。

闭包的局限性

不合理的闭包使用会导致:

  • 空间浪费
  • 内存泄漏
  • 性能消耗
    其实这些都是因为你在使用闭包的时候,维持了自由变量的不释放,从而占据了内存导致的~~

Javascript的作用域

Javascript的作用域有以下几种:

  • 全局作用域
  • 函数作用域
  • eval作用域
    注意ES5以及之前版本的Js都是没有块级作用域的。

作用域链

作用域链决定了哪些数据能被函数访问。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。
举个例子来说说作用域链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function outer2() {
var local2 = 1;
function outer1() {
var local1 = 1;
// visit local1, local2 or global3
}
outer1();
}
var global3 = 1;
outer2();

function outer() {
var i = 1;
var func = new Function("console.log(typeof i)");
func(); //undefined
}
outer();

在outer1中,我们可以访问到局部变量local1,全局变量global3和自由变量local2。
这个访问有优先顺序是: local1 -> local2 -> global3。
而new出来的函数,函数体中是无权访问自由变量的。

ES3执行上下文(Execution Context)

简单回顾,js作用域有三种:全局,函数,eval。
说是上下文,其实就是做的一些准备工作。
在“准备工作”中完成了这些工作:

变量、函数表达式——变量声明,默认赋值为undefined;
this——赋值;
函数声明——赋值;

这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”
事实上,每次函数调用的时候,都会有一个对应的执行环境,就是执行上下文。
对于全局作用域,在进入第一行代码之前,首先也会有一个全局上下文。
举个🌰来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log("EC0");

function funcEC1() {
console.log("EC1");
var funcEC2 = function() {
console.log("EC2");
var funcEC3 = function() {
console.log("EC3");
};
funcEC3();
}
funcEC2();
}
funcEC1();

这段代码里面,首先我们进入一个全局的EC0,此刻创建出了一个全局上下文EC1,当我们进入EC1的时候,又会进入一个新的上下文EC1,然后我们进入EC2,EC3,当EC3执行完之后,我们会到EC2上下文,EC2执行问之后,再回到EC1上下文,最后回到全局上下文。
就像图里面这样:
执行上下文

变量对象

Js解释器是如何找到我们定义的函数和变量呢?
就是从变量对象中啦!
变量对象(Variable Object)缩写VO,是一个抽象概念中的对象,它用于存储执行上下文中的:

  • 变量
  • 函数声明
  • 函数参数

总结

要理解函数,必须理解作用域,为了理解作用域,本文引出了执行上下文。
理解执行上下文,会对函数各种机制的理解都起到很大的帮助~~