前言
作为一名前端攻城狮,必须理解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、函数声明提升要优先于变量声明提升
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。