1

前言

作为一名前端攻城狮,必须理解JavaScript的执行上下文顺序和事件机制,以便于写出更加健壮的代码。这就不得不掌握作用域和声明提升这两个知识点。

声明提升包括变量声明提升和函数声明提升

变量声明提升

 ES6以前声明定义一个变量常用var关键字,而在ES6以后声明定义一个变量可以采用let关键字,声明定义一个常量可以采用const关键字。按照别的语言特性,在a声明赋值以前去输出a,应当报错引用错误。而在js的语法中,却输出了undefined。

 console.log(a);    // undefined
 var a = 1;

这是什么原因呢?其实就是变量声明提升的概念。这串代码在执行以前,会先扫描一遍var关键字定义的变量,并将其置为undefined,之后才是对这些变量赋值。也就是说,这串代码实际上是这么执行的。

var a ;
console.log(a);
a = 1;

讲到var关键字的缺陷,又让我想到了另一个概念,变量重复声明。使用var关键字重复声明定义一个变量时后面的赋值会覆盖前面的赋值,但没有任何语法错误提示。

var a = 1;
var a = 2; 
console.log(a);    // 2

关于var关键字的设计缺陷,与作用域息息相关的是var在块级作用域的表现。按照别的语法特性,在块级作用域中声明定义的变量,无法在全局环境下被访问。但在JavaScript中,var关键字可以穿透块级作用域。

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

ES6提出了let关键字,修复了这几个缺陷。

  console.log(b);    //  Uncaught ReferenceError: Cannot access 'b' before initialization
  let b = 2;
  {
    let c = 2;
  }
  console.log(c);    // Uncaught ReferenceError: c is not defined
  let d = 3;
  let d = 4;    // Uncaught SyntaxError: Identifier 'd' has already been declared
 

函数声明提升

声明函数有两种方式,一种是显式的函数式声明,另外一种是使用函数表达式的方式来声明。
两种函数声明方式的区别:函数式声明在前后都可以被调用,而使用函数表达式声明的时候不能够在前面进行调用

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

同样的把实际代码的执行顺序翻译出来

 var foo2;
 function foo(){
   console.log("function");
 }
 console.log(foo());    // function
 console.log(foo2());   // 此时foo2是undefined,所以报错foo2 is not a function
 foo2 = function(){
    console.log("var");
 }

再来看看当两种方式同名时的情况,即变量声明提升和函数声明提升的综合体。

 console.log(foo);     //  function foo(){console.log("function");}
 console.log(foo());   //  function
 function foo(){
   console.log('function');
 }   
 var foo = function(){
   console.log('var');
 }
 console.log(foo());     // var 

在第一行代码执行时,按照常理foo函数还没被创建,此时应当访问不到。但由于JS函数声明提升要优先于变量声明提升,或者说在代码进行扫描的过程中,变量声明提升先被执行,而后执行函数声明提升,覆盖了前者。导致实际上执行代码顺序如下:

  var foo ;   // 此时foo为undefined
  function foo(){
    console.log('function');
  }
  console.log(foo);      //  function foo(){console.log("function");}
  console.log(foo());    //  function
  foo = function(){
    console.log("var");
  }
  console.log(foo());      // var
作用域包括全局作用域、函数作用域、val作用域、块级作用域(ES6生效)。eval作用域由于安全性问题不建议使用。

全局作用域

全局作用域应该是最好理解的,值得一提的是当使用var关键字在全局作用域定义一个变量时,该变量会被自动挂到window。

 var a = 1;
 console.log(window.a);   // 1

而在ES6使用let关键字定义一个变量时,不会自动将其挂到window对象中

 let a = 1;
 console.log(window.a);   // undefined

函数作用域

函数是JS语法的一等公民,在函数作用域中定义的变量无法在外界进行访问,闭包可以解决这个问题,但这里我们先不考虑闭包的情况。

 function foo(){
    var a = 1;
   }
 console.log(a);   // Uncaught ReferenceError:a is not defined

作用域链

当访问一个变量时,js引擎会在当前作用域查找该变量,若没找到则去父作用域找,直到找不到这个变量时则抛出引用错误,作用域的顶端是全局对象。这就是作用域链的概念。

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

实际的代码执行顺序:
1、全局作用域的变量声明提升到顶部
2、函数foo声明提升
3、函数作用域内部也会发生变量声明提升

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

最后再做一道JavaScript权威指南的经典题

var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
       return scope;
  }
  return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
      return scope;
  }
  return f;
}
checkscope()();

由于作用域在函数创建阶段已经被确定,且由于在函数checkscoped调用完仍旧存在函数f对于scope的引用而产生闭包,因此scope不会在被销毁掉,因此两段代码都是f向上查找最近的变量scope,即local scope

小结

1、使用var关键字定义一个变量时会发生变量提升
2、var关键字会穿透块级作用域
3、函数显式声明在声明前后都可以被调用,函数表达式声明只能够在声明后调用
4、函数声明提升要优先于变量声明提升


wuquan133
26 声望1 粉丝

正在成长中的小前端