首页 日常生活 社交礼仪 美容健身 电脑网络 手机技巧 电子数码 废物利用
首页 > 电脑网络 > 网站建设 > 闭包前记:JavaScript作用域链详情

闭包前记:JavaScript作用域链

  • 2015-12-21 10:51:49
  • 阅读:...

某天我发现写JavaScript程序,其实就是利用一大堆的变量、函数来完成业务应用;引用这些变量时背后发生了什么事儿呢?看下面一段简单的代码

alert(a);

弹出变量a的值,当程序执行时,会去哪里查找变量a,a的值又是多少呢?看完这篇文章,你就知道作用域链在其中发挥了至关重要的作用。

写这篇文章之前,本来打算是写关于闭包的文章的,结果写着写着里面关于作用域的篇幅越来越大,所以干脆直接先写作用域链。

一、变量作用域

在函数中定义的变量为局部变量,他只在函数局部作用域中有定义,定义在代码最外层即没有定义在函数中的变量为全局变量,全局变量在全局作用域中都是有定义的。

var a = 'global'; //声明一个全局变量
function foo(){
    var b = 'local'; //声明一个局部变量
    console.log(a); //'global',因为a是全局变量,所有地方都能访问它
}
foo();
console.log(b); //not defined,因为b是在函数foo中定义的局部变量,函数外面访问不到

二、什么时候“产生”了作用域

JavaScript是基于词法作用域的语言,也就是说编译器在编译的词法阶段时就确定了作用域。

通常可以这样理解,作用域就是在你编写代码把变量声明写在哪里来确定的。

比如前文中的var b = 'local';写在foo函数内,foo函数所创建的作用域中就有了b的定义。

三、函数作用域

JavaScript没有块级作用域,至少在ES5中是这样的,ES6中的let不在此讨论范围

for(var i = 0; i < 10; i++){
    //...
}
alert(i); //10

for循环结果后,变量i还是在当前的作用域中,而在ES5中要实现类似块级作用域的效果,通常是借助函数作用域来实现的,类似下面这样:

(function(){
	for (var i = 0; i < 10; i++) {    //...
	}
})();
alert(i); //not defined

因为局部变量在函数体外是不能访问的,如前文foo函数中的局部变量b,外部同样也是不能访问的,这样就形成了一个相对封装的空间,可以保护函数内部的变量。

函数作用域除了包含变量声明还包含了其他内容

  1. 变量声明(var ),

  2. 函数内嵌套的函数声明(function),

  3. 函数形参(a, b, c, ....)

  4. 外层作用域

通过下面代码来理解:

var g = 'global: g';
function foo(){
	var a = 'foo: a';
	function bar(x){
		var b = 'bar: b';
		console.log(g); //'global: g'
	}
	(function fn(){

	})();
	console.log(b); //not defined
}
foo();

对应的作用域应该是下面这样的

foo作用域:

  1. 自身作用域:变量声明a、函数声明bar,下面的fn是函数表达式,不是函数声明所以作用域中并不包含fn。

  2. 外层作用域:外层是全局作用域名,里面有变量声明g

bar作用域:

  1. 自身作用域:变量声明b,形参x

  2. 外层作用域:外层是foo作用域,而且foo外层又是全局作用域,这样就包含了a, b两个变量声明

变量解析

当JavaScript引擎查找变量x的值,这个过程叫变量解析。这里的查找就是指在作用域中查找了。

bar中console.log(g)先在自身作用域(b, x)查找,没有找到,再到他外层(foo)作用域(a, bar)中查找但也没找到,最后在foo外层全局作用域中找到了g值为'global: g'

而在foo中的console.log(b); foo和foo的外层都没有b,所以会报not defined。

四、作用域链

全局作用域、函数的作用域、嵌套函数的作用域,由这三个作用域组成作用域链了。作用域链有点象事件冒泡,两者都是逐级的,通过图片可以更加形象的理解作用域链。

[caption id="attachment_189" align="aligncenter" width="606"]作用域链作用域链[/caption]

作用域链特点

1、以从内到外逐级查找的方式在作用域链中查找标识符,只要查找到第一匹配的就会停止

	var a = 2;
	function foo(){
		var a = 1;
		console.log(a); //1
	}
	foo();

在foo的作用域中查找有a的定义,所以查找就停止了,不会再继续向外层查找。

2、作用域链是逐级包含的,内层可以访问外层作用域,反之则不行

五、所谓声明提升

console.log(a); //undefined
var a = 1;
if(a === 2){
	var b = 3;
}
console.log(a); //1
console.log(b); //undefined

初学JavaScript时应该会认为console.log(b)会报not defined,其原因如下:

if(a === 2)永远为false,所以里面的var b = 3不会执行,所以输出b肯定为not defined。而实际输出的却是undefined,这是什么原因呢?

1、作用域在编写代码时就确定了(其实是在编译阶段),不是在在执行的时候

编译阶段找到所有变量声明,不管你在任何位置、是否需要满足任何条件的声明,然后生成一个作用域与之关联,以便后面程序查找、使用。

2、作用域变量在与之关联的代码块中任何地方都是有定义的

上面代码在第一步时找到var a, b两个变量声明,与全局作用域关联,所以在任何地方都有这两个变量的定义。

所以第一个console.log(a)和console.log(b)两个都是有定义的,只是值为undefined而已。

代码var a = 1;从字面上来看就是声明一个变量a并赋值1,但实际JavaScript在处理的时候不是这样的,他被分成了编译和执行两部分,

编译时var a会生成一个作用域并把a与该作用域关联;执行时a = 1;,在作用域中找到a并赋值1。这个过程就是平时常说的声明提升,其实就是作用域的原理而已。

函数声明提升

函数声明也能提升,但他和变量的声明提升有些区别。变量声明提升,只是提升的定义,而函数声明提升除了定义提升包括函数体也提升了,这就是为什么在函数声明前就可以调用函数的原因。

函数表达式不是函数体声明所以不能提升函数体,只能当作变量提升。

foo();
bar(); //bar is not a function
function foo(){
	var a = 1;
	console.log(a); //1
}
var bar = function(){
};

五、标识符重名

function foo(){
}
function bar(){
}
var foo = 2;
var bar;
console.log(typeof(foo)); //'number'
console.log(typeof(bar)); //'function'

函数声明会优先于变量声明,那上面的typeof(foo);怎么是'number'呢?其实还是作用域、编译、执行原因造成的。

全局作用域:函数声明foo、bar;变量声明foo、bar,而函数声明优先于变量声明,所以这里作用域里foo和bar都确定为函数声明

执行 foo = 2,在作用域中查到foo,这个时候foo还是函数,但是把2赋给了他,这里就导致了后面typeof(foo)为'number',而bar没有被赋值所以还是函数。

以上的理解也是自己在写这篇文章时参考其他文章发觉的,以前的理解是“函数声明优先不带赋值变量声明”,上面的代码如果把变量和函数声明的顺序反过来,其结果也是一样的。虽然理解了重名优先的原理,而在实践开发中最好不要这样干哟-_-

var foo = 2;
var bar;
function foo(){

}
function bar(){
}
console.log(typeof(foo)); //'number'
console.log(typeof(bar)); //'function'

另外IE下对具名函数表达式的处理跟其他浏览器不太一样,如 :

var f = function fn(){
};
var fn;
alert(typeof(fn)); //'undefined'

在多数现代浏览器fn的值都为'undefined',但是在IE的值居然是'function',这个问题在IE9中才修复。

草草结尾

如果说原型链是JS中的屠龙刀,那作用域链就是倚天剑了,两者在JavaScript编程中发挥着巨大的作用。本文记录通过自己的方式理解作用域链,文中肯定有失标准规范的地方,希望路过的朋友指出。

网站建设热点文章