JavaScript作用域链、预解析、闭包

作用域及执行环境

变量的作用域有全局变量和局部变量两种,这一点与其他语言(如C)的概念是非常相似的。

我们先了解一下几个概念:

  • 执行环境(execution context)定义了变量或者函数有权访问其他数据,决定他们各自的行为。
  • 每个执行环境都有关联的变量对象(VO),环境中定义的变量和函数都会保存在这个对象中。
  • 作用域链(scope chain)保证对执行环境有权访问的所有变量和函数的有序访问
  • 全局执行环境(GO)在web浏览器中通常为window对象,始终处于作用域链的最后一位。
  • 执行函数的时候,会创建一个活动对象(AO)作为变量对象置于作用域链的前端。

ES5没有块级作用域

ES5时代,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引入了新的关键字letlet替代var可以申明一个块级作用域的变量

for (let i=0; i<100; i++) 
{
     sum += i;
}

预解析

预解析的简单规则:

  1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。

  2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。

  3. 先提升函数,再提升变量

理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。  ——《JavaScript高级程序设计》 

sayHi(); //错误:函数还不存在
var sayHi = function(){
    alert("Hi!");
}

这段代码在执行的时候因为预解析,会先声明sayHi这个变量,然后再提升匿名函数的声明,然后是执行sayHi,最后才是赋值

//变量声明
var sayHi;
//函数声明
function(){
  alert("Hi!");
}
//函数执行,报错
sayHi(); 
//赋值
var sayHi = function(){
    alert("Hi!");
}

预解析的详细步骤:

以上提到的GO和AO,GO是始终存在并处于作用域底端的,当一个函数开始预解析的时候严格按照以下顺序:

  1. 创建AO对象AO{     }   (AO也称执行期上下文)
  2. 找形参和变量声明;将变量和形参名作为AO的属性名,值为undefined
  3. 将实参值与形参统一
  4. 在函数体里面找函数声明,值赋予函数体

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

 

闭包的用途

  1. 可以读取函数内部的变量,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
  2. 让变量的值始终保持在内存中

延长作用域链

有些语句可以在作用域链前端添加一个变量对象,该变量对象会在代码执行后被移除。

  • try-catch语句的catch块

对于catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

  • with语句

with的作用是将代码的作用域设置到一个特定的对象中。

这一部分以后在详细说。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值