【Javascript】作用域与预编译

作用域一般理解,是指一个变量的作用范围

作用域分为:

  • 全局作用域
  •  函数作用域

注:在ES5中没有块级作用域,但通过let声明的变量具有块级作用域,除作用域外,它的其他方面与通过var进行声明的变量没有区别。

全局作用域

  1. 全局作用域在页面打开时被创建,页面关闭时被销毁
  2. 编写在script标签中的变量和函数,作用域为全局,在页面任意位置都可以访问到
  3. 在全局作用域中有全局对象window,代表一个浏览器窗口,由浏览器创建,可以直接调用
  4. 全局作用域中声明的变量和函数作为window对象的属性和方法保存

函数作用域

  1. 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁
  2. 每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的
  3. 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量
  4. 在函数作用域中访问变量、函数时,会先在自身作用域中寻找,如果没找到,则会到函数上一级的作用域中寻找,一直找到全局作用域

 

执行期的上下文

当函数代码执行的前期,会创建一个执行期上下文的内部对象AO(作用域)

这个内部的对象是预编译的时候创造出来的

在全局代码执行的前期会创建一个执行期的上下文对象GO(作用域)

 

ps: 作用域链会被保存到一个隐式的属性[scope]中去,这个属性是给js引擎来访问的。


预编译:作用域的创建阶段

函数作用域预编译:

1、创建AO对象

2、找形参和变量的声明,作为AO对象的属性名, 它们的值是undefined

3、实参和形参相统一

4、找函数声明,如果函数声明和变量声明一致的话会覆盖。

全局作用域的预编译:

1、创建GO对象

2、找变量声明,将变量名作为GO对象的属性名 值是undefined

3、找函数声明,值赋予函数体


例题:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script>
      function fn(a, c) {
        console.log(a);
        var a = 123;
        console.log(a);
        console.log(c);
        function a() {};
        if (false) {
          var d = 678;
        }
        console.log(d);
        console.log(b);
        var b = function (){};
        console.log(b);
        function c () {};
        console.log(c);
      }
      fn(1, 2)
    </script>
  </body>
</html>

 

上面的例题不难看出变量声明使用的都是var,它们的作用域有函数内的,也有全局的。这里我们要提到一个变量提升机制。

变量提升机制:在函数作用域或全局作用域中通过关键字var声明的变量,无论是在哪里声明的,都会被当成在当前作用域顶部声明的变量。

function getValue(condition) {
    if (condition) {
        var value= "aa"

        return value
    } else {
        //此处可访问变量value,值为undefined
        return null
    }

    //此处可访问变量value,值为undefined
}

在预编译阶段,Javascript引擎会将bule的声明提升到函数顶部,初始化操作依旧在原处执行,所以在else和if之外可以访问到变量value,但无法获得它的初始值。

但这样的变量提升有时却会导致程序出现Bug,所以ES6将引入块级作用域来强化变量声明周期的控制。

下面我们来看下ES6新引入的块级作用域


块级声明

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于:函数内部、块中(字符{和}之间的作用域)

let声明

let声明的用法与var相同,用let代替var来声明变量,就可以把变量作用域限制在当前代码块中,由于let声明不会被提升,因此需要将let声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。

function getValue(condition) {
    if (condition) {
        let value = 'aa'

        return value
    } else {
        //变量value在此处不存在
        return null
    }

    //变量value在此处不存在
}

 

假设作用域中已经存在某个标识符,此时再使用let关键字它就会抛出错误。

所以同一作用域中不能用let重复定义已经存在的标识符,那么不同作用域呢?看下例子

由于这里的let是在if块内声明了新变量value,因此不会抛出错误,内部块中的value会遮蔽全局作用域中的value,后者只有在if块外才能访问到。

 

const声明

ES6提供了const关键字,使用const声明的是常量,值一旦被设定后不可更改,因此使用const声明的常量必须进行初始化。

 

const与let

const和let声明的都是块级标识符,与let一样不会被提升到作用域顶部。它们之间的不同在于,无论是严格模式还是非严格模式,都不可以为const定义的常量再赋值,但如果const定义了一个对象,这个对象的值却可以修改。

注:绑定person的值是一个包含一个属性的对象,改变person.name是修改person包含的值,不会抛出任何错误。但如果直接给person赋值,等于要改变person的绑定,就会抛出错误。const声明不允许修改绑定,但允许修改绑定的值。


临时死区(Temporal Dead Zone)

TDZ常用于描述描述let和const的不提升效果。

Javascript引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到var声明),要么将声明放到TDZ中(遇到let和const)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移除,才能正常访问。

TDZ只是块级绑定的特色之一,下面我们看看循环中使用块级绑定特色。


循环中的块作用域绑定

先来看个bug

显然这段代码的预期输出结果应当是0~9,但控制台却打印了10次10,这是因为循环里的每次迭代同时共享着i,循环内部创建的函数全都保留对相同变量的引用。循环结束时变量i的值为10,所以每次调用console.log(i)时都会打印10。

先来看第一种解决办法,使用立即调用函数表达式(IIFE),强制生成计数器变量的副本

在循环内部,IIFE为接受的每一个变量i都创建一个副本并存储为变量value,这个变量的值就是响应迭代创建的函数所使用的值。

而ES6仅仅需要将bug代码中的var换成let。

每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于它们自己的i的副本。


全局作用域的绑定

let和const与var的另一个区别是它们在全局作用域中的行为。当var被用于全局作用域时,它会创建一个全局变量作为全局对象的属性,这就意味着使用var可能会无意中覆盖一个已经存在的全局属性,但在跨frame或跨window访问代码时我们仍会使用var定义全局变量。

如果你在全局作用域中使用let或const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性,只是遮蔽了全局变量。


其他:

函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的。也就是说参数的默认值不可访问函数体内声明的变量。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值