作用域及执行环境
变量的作用域有全局变量和局部变量两种,这一点与其他语言(如C)的概念是非常相似的。
我们先了解一下几个概念:
- 执行环境(execution context)定义了变量或者函数有权访问其他数据,决定他们各自的行为。
- 每个执行环境都有关联的变量对象(VO),环境中定义的变量和函数都会保存在这个对象中。
- 作用域链(scope chain)保证对执行环境有权访问的所有变量和函数的有序访问。
- 全局执行环境(GO)在web浏览器中通常为window对象,始终处于作用域链的最后一位。
- 执行函数的时候,会创建一个活动对象(AO)作为变量对象置于作用域链的前端。
JS没有块级作用域
JS中没有花括号块级作用域
if(true) {
var color = "red" ;
}
alert (color) ; //red
花括号执行完毕之后并不会销毁变量。
尤其记住for语句中声明的变量for(var i=0;i<10;i++) { } ,变量i在for执行结束的时候仍然会存在于循环外部的执行环境中。
来看看以下代码:
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以访问foo的变量x!
}
var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}
我们知道嵌套函数中,内部函数可以访问外部函数的变量,而外部函数不能访问内部函数的变量。
那么我们又说没有块级作用域,要怎么理解?
在JavaScript中,把这个东西叫做,执行环境。注意,执行环境只分为两种,全局执行环境(除函数之外的环境)和局部执行环境(只有函数内部的区域是局部的。)每个执行环境都有一个与之相关联的变量对象(所有的变量与函数保存在里面)
对于全局执行环境来说,对象就是window对象,而对于局部(函数)环境来说,称为活动对象(最开始至包含arguments对象)
为了解决块级作用域,ES6引入了新的关键字let
,用let
替代var
可以申明一个块级作用域的变量:
for (let i=0; i<100; i++)
{
sum += i;
}
预解析
预解析的简单规则:
把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
先提升函数,再提升变量
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。 ——《JavaScript高级程序设计》
sayHi(); //错误:函数还不存在
var sayHi = function(){
alert("Hi!");
}
这段代码在执行的时候因为预解析,会先声明sayHi这个变量,然后再提升匿名函数的声明,然后是执行sayHi,最后才是赋值
//变量声明
var sayHi;
//函数声明
function(){
alert("Hi!");
}
//函数执行,报错
sayHi();
//赋值
var sayHi = function(){
alert("Hi!");
}
预解析的详细步骤:
以上提到的GO和AO,GO是始终存在并处于作用域底端的,当一个函数开始预解析的时候严格按照以下顺序:
- 创建AO对象AO{ } (AO也称执行期上下文)
- 找形参和变量声明;将变量和形参名作为AO的属性名,值为undefined
- 将实参值与形参统一
- 在函数体里面找函数声明,值赋予函数体
a函数最开始被调用时,会创建一个执行环境,还有它的作用域链
也就是说,在作用域顶端创建了AO,于是函数在执行查找变量的时候,按照作用域顺序开始查找,从0开始,从上到下。
一般来讲,当函数执行完毕之后,局部活动对象AO就会被销毁,内存中仅保存全局作用域。但是嵌套函数的情况又有所不同(在函数中定义另外一个函数),于是闭包出现了。
闭包
先不着急说什么是闭包,上文提到,在函数内部定义函数,内部函数会将外部函数的AO添加到自己的作用域中,因此内部函数的作用域链包含了外部函数的AO。即使外部函数被销毁,倘若内部函数被返回到了外面,在其他地方被调用了,那么它仍然可以访问外部函数的AO中的变量,直到内部函数也被销毁。
**注意上图,a的AO被销毁后,作用域链的指针不再指向AO,但是b仍然包含a的AO。换句话说,a函数执行完毕,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中。 **
说到这里,已经对闭包的概念有所认识了,我们给出闭包的定义:
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另一个函数。
1. 把匿名函数作为返回值,保存在外面的变量,不立即执行
//函数作为返回值
function a(){
var name = "aaa";
return function(){ //匿名函数
return name;
}
}
var b = a();
console.log(b()); //aaa
2.把变量保存到全局执行环境,fn1()每次被调用会重新创建n,而num是全局变量。
function fn() {
var num = 3;
return function(){
var n = 0;
console.log(++n);
console.log(++num);
}
}
var fn1 = fn();
fn1(); //1 4
fn1(); //1 5
3.匿名函数立即执行
(function (x) {
return x * x;
})(3); // 9
闭包的用途
- 可以读取函数内部的变量,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
- 让变量的值始终保持在内存中。
延长作用域链
有些语句可以在作用域链前端添加一个变量对象,该变量对象会在代码执行后被移除。
- try-catch语句的catch块
对于catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
- with语句
with的作用是将代码的作用域设置到一个特定的对象中。
这一部分以后在详细说。