JavaScript中作用域与作用域链 闭包

JS闭包与作用域
本文深入探讨JavaScript中的闭包概念,解释闭包如何允许函数访问其外部作用域中的变量,即使在函数调用完成后。文章还介绍了作用域链的工作原理,以及变量提升和函数提升的现象。

JavaScript (阮一峰)
ES6 链接(阮一峰)

1. 执行环境(execution context)

      执行环境 定义了变量和函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有都有与之对应的变量对象(variable object),保存着该环境中定义的所有变量和函数。我们无法通过代码来访问变量对象,但是解析器在处理数据时会在后台用到他。

     执行环境有全局执行环境(也称全局环境)和函数执行环境之分。执行环境如其名是在运行和执行代码的时候才存在的,所以我们运行浏览器的时候会创建全局的执行环境,在调用函数时会创建函数执行环境。

1.1 全局执行环境

      全局执行环境是最外围的一个执行环境,在web浏览器中,我们可以认为他是windows对象,因此所有的全局变量和函数都是作为windows对象的属性和方法创建的。代码载入浏览器时,全局环境被创建,关闭网页或者关闭浏览器时全局环境被销毁。

1.2 函数执行环境

      每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就被推入环境栈中,当函数执行完毕后,栈将其环境弹出。把控制权返回给之前的执行环境。

2. 作用域 作用域链 

2.1 全局作用域(global scope) 和 局部作用域(local scope)

      全局作用域可以在代码中的任何地方被访问;

1 var name1="haha";
2 function changName(){
3     var name2="xixi";
4     console.log(name1); // haha
5     console.log(name2);// xixi
6 } 
7 changName();
8 console.log(name1);//haha
9 console.log(name2);//Uncaught ReferenceError: name2 is not defined复制代码

       其中,name1 具有全局作用域,因此在第4行和第8行都会在控制台上输出 haha 。name2 定义在changeName()函数内部,具有局部作用域,因此在第9行,解析器找不到name2,抛出错误;

      另外,在函数中声明变量时,如果省略 var 操作符,那么声明的变量就是全局变量,拥有全局作用域,但是不推荐这种方法,因为在局部作用域中很难维护定义的全局变量。

      再者,window对象的内置属性都拥有全局作用域。

      局部作用域一般只在固定的代码段内才可以访问到,例如上述代码中的 name2 ,只有在函数内部才能访问得到。

2.2 作用域链

      全局作用域和局部作用域中变量的访问权限,其实是由作用域决定的。

      每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链。作用域链是函数被创建作用域中对象的集合。作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。

      作用域链的最前端始终当前执行的代码所在环境的变量对象(如果该环境是函数,则将其活动对象作为变量对象),下一个变量对象来自包含环境(包含当前执行环境的环境),下一个变量来自包含环境的包含环境,依次往上,直到全局执行环境的变量对象。全局执行环境的变量对象,始终是作用域链中的最后一个对象。

      标识符解析是沿着作用域一级一级地向上搜索标识符的过程。搜索过程始终是从作用域的前端逐地向后回溯,直到找到标识符(找不到,就会导致错误发生)。

例如:

9  var name1 = "haha";
10 function changeName() {
11 	var name2 = "xixi";
12	function swapName() {
13		console.log(name1);//"haha"
14		console.log(name2);//"xixi"
15		var tempName=name2;
16		name2=name1;
17		name1=tempName;
18		console.log(name1);//"xixi"
19		console.log(name2);//"haha"
20		console.log(tempName);//"xixi"
21	}
22	swapName();
23	console.log(name1);//"xixi"
24	console.log(name2);//'haha'
25	// console.log(tempName);// 抛出错误 Uncaught ReferenceError: tempName is not defined
26}
27 changeName();
28 console.log(name1);//"xixi"
29 // console.log(name2);// 抛出错误 Uncaught ReferenceError: tempName is not defined	
30// console.log(tempName) //抛出错误 Uncaught ReferenceError: tempName is not defined复制代码

运行结果如下:


    上述代码中,一共有三个执行环境:全局环境、changeName()的局部环境和 swapName() 的局部环境。所以,

   1.函数 swapName()的作用域链包含三个对象:自己的变量对象----->changeName()局部环境的变量对象 ----->全局环境的变量对象

   2.函数changeName()的作用域包含两个对象:自己的变量对象----->全局环境的变量对象。

就上述程序中出现的变量和函数来讲(不考虑隐形变量):

      1.swapName() 局部环境的变量对象中存放变量 tempName;

   2.changeName() 局部环境的变量对象中存放变量 name2 和 函数swapName();

   3.全局环境的变量对象中存放变量 name1 、函数changeName();

      在swapName()的执行环境中,在执行第5句代码时,解析器沿着函数 swapName()的作用域链一级级向后回溯查找变量 name1,直到在全局环境中找到变量 name1.并输出在控制台上。同样,在执行第6句代码时,解析器沿着函数 swapName()的作用域链一级级向后回溯,在函数changeName()的变量对象中发现变量 name2.通过代码对 name1 和 name2进行交换,并输出在控制台上,根据结果我们发现,这两个变量的值确实交换了。因此我们可以得出结论,函数的局部环境可以访问函数作用域中的变量,也可以访问和操作父环境(包含环境)乃至全局环境中的变量。

      在changeName() 的执行环境中,执行第15行和第16行代码时,可以正确地输出 name1 和 name2 和两个变量的值(调用了函数swapName(),所以俩变量的值已相互交换),那是因为 name1 在changName()的父环境(全局环境)中, name2 在他自己的局部环境中,即 name1 和 name2 都在其作用域链上。但当执行第17行代码是发生错误 tempName is not defined。因为解析器沿着 函数changeName()的作用域链一级级的查找 变量 tempName时,并不能找到该变量的存在(变量 tempName不在其作用域链上),所以抛出错误。因此,我们可以得出结论:父环境只能访问其包含环境和自己环境中的变量和函数,不能访问其子环境中的变量和函数。

      同理,在全局环境中,其变量对象中只存放变量 name1 、函数changeName(); 解析器只能访问变量 name1 和函数 changeName(), 而不能访问和操作 函数 changeName() 和函数 swapName() 中定义的变量或者函数。因此,在执行第21行和第22行代码时抛出变量没有定义的错误。所以说,全局环境只能访问全局环境中的变量和函数,不能直接访问局部环境中的任何数据。

 其实,我们可以把作用域链想象成这样(里面的能访问外面的,外面的不能访问里面的,图为参考):


作用域链相关知识的总结:

1. 执行环境决定了变量的生命周期,以及哪部分代码可以访问其中变量

2. 执行环境有全局执行环境(全局环境)和局部执行环境之分。

3. 每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链

4. 函数的局部环境可以访问函数作用域中的变量和函数,也可以访问其父环境,乃至全局环境    中的变量和环境。

5. 全局环境只能访问全局环境中定义的变量和函数,不能直接访问局部环境中的任何数据。

6. 变量的执行环境有助于确定应该合适释放内存。

3. 提升

提升有变量提升和函数提升之分;下边依次介绍:

3.1 变量提升

上代码:

1  var name="haha";
2  function changeName(){
3      console.log(name);
4      var name="xixi";
5  }
6  changeName();
7  console.log(name);复制代码

      大家认为第6行和第7行代码输出的结果应该是什么?好了,答案是:输出结果结果分别是 undefined haha。为什么是undefined? 按照作用域链的思路思考,输出的结果应该是 haha或者xixi啊? (当然大家都知道 xixi是不可能的,因为解析器在解析第3行代码时,还不知道第4行中的赋值内容)。

      那我们先来分析一下代码 函数changeName() 的作用域链: 自己的变量对象 -----> 全局变量 对象。解析器在函数执行环境中发现变量 name,因此不会再向全局环境的变量对象中寻找。但是大家要注意的是,解析器在解析第3句代码时,还不知道变量name的值,也就是说只知道有变量name,但是不知道它具体的值(因为还没有执行第4句代码),因此输出是 undefined,第7行输出haha大家应该都理解把(作用域问题)。所以上述代码可以写成下面的形式:

1   var name="haha";
2   function changeName(){
3       var name;
4       console.log(name);
5       name="xixi";
6  }
7  changeName();
8  console.log(name);复制代码

这个现象就是变量提升

      变量提升,就是把变量提升到函数的顶部,需要注意的是,变量提升只是提升变量的声明,不会吧变量的值也提升上来!见上述代码,最常见的代码如下,函数example1()和函数example2()是等价的:

1 function example1(){
2     var a="haha";
3     var b="xixi";
4     var c="heihei";
5 }
6 
7 
8 function example2(){
9     var a,b,c;
10     a="haha";
11     b="xixi";
12     c="heihei";
13 }复制代码

3.2 函数提升

      函数提升就是把函数提升到前面。

  在JavaScript中函数的创建方式有三种:函数声明(静态的,像函数example1()的形式)、函数表达式(函数字面量)、函数构造法(动态的,匿名的)。函数表达式的形式如下:

1 var func1 = function(n1,n2){
2     //function body;
3 };复制代码

函数构造法构造函数的形式如下:

var func2 = new Function("para1","para2",...,"function body");  复制代码

在这里需要说明的是:只有函数声明形式才能被提升!例如:

//函数声明
function myTest1(){ 
    func(); 
    function func(){ 
        console.log("我可以被提升"); 
    } 
} 
myTest1();

//函数表达式
function myTest2(){ 
    func(); 
    var func = function(){ 
        console.log("我不能被提升"); 
    } 
} 
myTest2();复制代码

控制台显示结果如下:


我的写作完毕,欢迎指正!

闭包

  闭包(closure)是 Javascript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

  理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。 

var n=999;
function f1() {
    console.log(n)
}
f1();//999复制代码

  上面代码中,函数f1可以读取全局变量n

  但是,函数外部无法读取函数内部声明的变量。

function f1() {
	var n=999;
}
console.log(n) //Uncaught ReferenceError: n is not defined复制代码

  上面代码中,函数f1内部声明的变量n,函数外是无法读取的。

  如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

function f1() {
	var n=999;
	function f2() {
		console.log(n);//999
	}
	return f2;
}
f1()()复制代码

  上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是 JavaScript 语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

  既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1() {
	var n=999;
	function f2() {
		console.log(n)
	}
	return f2;
}
var result=f1();
result();复制代码

  上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

  闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

  闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {
    return function () {
        return start++;
    };
}
var inc = createIncrementor(5);
console.log(inc())//5
console.log(inc())//6
console.log(inc())//7复制代码

  上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

  为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

  闭包的另一个用处,是封装对象的私有属性和私有方法。

function Person(name) {
	var _age;
	function setAge(n) {
		_age = n;
	}
	function getAge() {
		return _age;
	}

	return {
		name: name,
		setAge: setAge,
		getAge: getAge
	}
}

var p1 = Person('张三');
console.log(p1)
p1.setAge(21)
console.log(p1.getAge())复制代码

  上面代码中,函数Person的内部变量_age,通过闭包getAgesetAge,变成了返回对象p1的私有变量。

  注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值