Skip to content

作用域

  1. 作用域指一个变量其作用的范围。它是静态的(相对于上下文对象),在编写代码时就确定了
  2. 分类
    1. 全局作用域
    2. 函数作用域
    3. 块级作用域(ES6 新增)
  3. 作用:隔离变量,不同作用域下同名变量不会有冲突

全局作用域

  1. 直接编写在 script 标签中的 JS 代码,都在全局作用域中。
  2. 全局作用域在页面打开时创建,在页面关闭时销毁。
  3. 在全局作用域中有一个全局对象 window,它代表的是浏览器的窗口,由浏览器创建,我们可以直接使用。
  4. 在全局作用域中(使用 var)
    1. 创建的变量都会作为 window 对象的属性保存。
    2. 创建的函数都会作为 window 对象的方法保存。
  5. 变量的声明提前:
    1. 使用 var 关键字声明的变量,会在所有的代码执行之前被声明好(提前声明但是不会赋值,赋值还是等程序执行到代码所处的该行时变量才会被赋值),但是如果声明变量时不使用 var 关键字,则变量不会被声明提前。
  6. 函数的声明提前:
    1. 使用函数声明形式创建的函数 (function 函数名(){})它会在所有的代码执行之前就被创建,所以我们可以将调用函数写在函数的声明之前。
    2. 使用函数表达式创建的函数 var 函数名 = function(){} (此种声明方式会把函数当变量声明,只会提前声明函数名,但是函数会在程序执行到该行时才会被当做赋值行为所创建),不会被提前声明,所以不能在声明前调用。
  7. 全局作用域中的变量都是全局变量,在页面的任意部分都可以访问。
JavaScript
//函数声明,会被提前创建
function fun(){
    console.log("我是一个fun函数");
}
//函数表达式,不会被提前创建
var fun2 = function(){
    console.log("我是一个fun2函数");
}

函数作用域

  1. 调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。

  2. 每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的。

  3. 在函数作用域中可以访问到全局作用域的变量(全局变量),但是在全局作用域中无法访问到函数作用域的变量。

  4. 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错 ReferenceError。

  5. 在函数作用域中要访问全局变量可以使用 window 对象(window.a)。

  6. 在函数作用域中也有声明提前的特性,使用 var 关键字声明的变量,会在函数中所有的代码执行之前被声明。

  7. 函数声明也会在函数中所有代码执行之前被提前执行。

    JavaScript
    var a = 10;
    function fun3(){
        console.log(a);  //undefined 由于var关键字声明提前,这里输出的是undefined,而不是10.
        var a = 20;
    }
  8. 在函数中,不使用 var 声明的变量都会成为全局变量。

    JavaScript
    var c = 33;
    function fun5(){
        console.log(c);  //33
        c = 10;  //没有使用var声明,此时使用的c就是全局作用域中的c
        d = 20;  //没有使用var声明,其实是创建了全局变量d,而不是函数作用域的d。
    }
    console.log(c); //10
    console.log(d);  //在函数作用域中声明的全局变量d,一样能在全局作用域中读取
  9. 定义形参就相当于在函数作用域中声明了变量。

    JavaScript
    var e = 23;
    function fun6(){
        alert(e);
    }
    fun6(); //输出23
    
    function fun7(e){
        alert(e);
    }
    fun7(); //undefined,定义形参就相当于在函数作用域中声明了变量,没有传实参,故输出undefined。
    
    var a = 123;
    function fun(a){
      alert(a);
      a = 456;  //注意:上面a定义了形参,所以a在函数域中找得到,此时是函数域的a = 456。对全局变量a没有影响
    }
      fun(223);  //223
      alert(a); //还是输出123。

块级作用域(这里只说概念,其他会在 ES6 部分详细说明)

  1. 通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。
  2. 块级作用域在如下情况被创建:
    1. 在一个函数内部
    2. 在一个代码块(由一对花括号包裹)内部

作用域链

  1. 多个上下级关系的作用域形成的链,它的方向是从下向上的(从内到外)
  2. 查找变量时就是沿着作用域链来查找的(查找属性和方法沿着隐式原型链)
  3. 查找一个变量的查找规则
    1. 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入 2
    2. 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入 3
    3. 再次往上一级作用域中查找,直到全局作用域,如果还找不到就抛出做不到的异常

作用域链面试题

JavaScript
  var x = 10;
  function fn() {
    console.log(x);
  }
  function show(f) {
    var x = 20;
    f();
  }
  show(fn);  //10

    var fn = function () {
    console.log(fn)
  }
  fn()  //fuNction(){console.log(fn)}

  var obj = {
    fn2: function () {
    console.log(fn2) //现在自身找,找不到去全局中找
    //console.log(this.fn2)才能找到obj中的fn2
    }
  }
  obj.fn2()  //报错:fn2 is not defined

执行上下文

  1. 代码分类(位置):

    1. 全局代码
    2. 函数(局部)代码
  2. 全局执行上下文

    1. 在执行全局代码前将 window 确定为全局执行上下文
    2. 对全局数据进行预处理
      1. var 定义的全局变量 ==> undefined,添加为 window 的属性
      2. function 声明的全局函数 ==> 赋值(func),添加为 window 的方法
      3. this ==> 赋值(window)
    3. 开始执行全局代码
  3. 函数执行上下文

    1. 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
    2. 对局部数据进行预处理
      1. 形参变量 ==> 赋值(实参) ==> 添加为执行上下文的属性
      2. arguments ==> 赋值(实参列表),添加为执行上下文的属性
      3. var 定义的局部变量 ==> 赋值 undefined(变量提升),添加为执行上下文的属性
      4. function 声明的函数 ==> 赋值(func),添加为执行上下文的方法
      5. this ==> 赋值(当前调用函数的对象)
    3. 开始执行函数体代码
    JavaScript
    //函数执行上下文
    function fn(a1){
        console.log(a1)  //2
        console.log(a2)  //undefined
        a3()  // a3()
        console.log(this)  // window
        console.log(arguments)  // 伪数组[2,3]
    
        var a2 = 3
      function a3(){
          console.log('a3()')
      }
    }
    fn(2,3)

执行上下文栈(与栈对应的是队列,栈是后进先出[FILO]如容器,队列是先进先出如管道[FIFO],会在后续数据结构算法中详细说明)

  1. 在全局代码执行前,JS 引擎就会创建一个栈来存储管理所有的执行上下文对象
  2. 在全局执行上下文(window)确定后,将其添加到栈底中(压栈)
  3. 在函数执行上下文创建后,将其添加到栈中(压栈)
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)
  5. 当所有的代码执行完后,栈中只剩下 window

执行上下文栈面试题

JavaScript
//1.依次输出什么
  //gb:undefined,fb:1,fb:2,fb:3,fe:3,fe:2,fe:1,ge:1
  //整个过程产生了几个执行上下文
  //5个
  console.log('gb: '+ i)
  var i = 1
  foo(1)
  function foo(i) {
    if (i == 4) {
      return
    }
    console.log('fb:' + i)
    foo(i + 1) //递归调用: 在函数内部调用自己
    console.log('fe:' + i)
  }
  console.log('ge: ' + i)

   //测试题1:  先执行变量提升, 再执行函数提升
  function a() {}
  var a
  console.log(typeof a) // 'function'


   //测试题2:
  if (!(b in window)) {
    var b = 1  //变量声明提升
  }
  console.log(b) // undefined

   //测试题3:
  var c = 1
  function c(c) {
    console.log(c)
    var c = 3
  }
   c(2) // 报错,c is not function
  //实际顺序
  var c = 1
  function c(c) {
    console.log(c)
    var c = 3
  }
  c = 1
  c(2)

作用域与执行上下文的区别

区别一

  1. 全局作用域之外,每个函数都会创建自己的作用域(即函数作用域),作用域在函数定义时就已经确定了。而不是在函数调用时
  2. 全局执行上下文环境是在全局作用域确定之后,js 代码马上执行之前才创建
  3. 函数执行上下文环境是在调用函数时,函数体代码执行之前创建

区别二

  1. 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
  2. 执行上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会自动释放

联系

  1. 执行上下文(对象)从属于所在的作用域(主从关系)
  2. 全局上下文环境 ==> 全局作用域
  3. 函数上下文环境 ==> 对应的函数使用域