浅谈JavaScript作用域
作用域的问题是任何一门语言必须弄清的问题之一,作为一个初学者,我希望通过这篇博客对我所学的知识做一个总结,有不对或者不清楚的地方还请不吝赐教
词法作用域和动态作用域
-
词法作用域
也可以理解为静态作用域,也就是当你打出代码的时候就决定了作用域;无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
作用域链和遮蔽效应
var a = 1,b = 2,c = 3;
function f1(){
var b = 20, c = 30;
function f2(){
var c = 300;
console.log(a,b,c);
}
f2();
}
f1();//1,20,300
最外面的是全局作用域,然后是f1的函数作用域,最里面是f2的函数作用域,他们是嵌套的关系;对于最里层的f2,它的作用域链就是 f2内–>f1内–>全局(Window);
通过这种技术可以使得同名变量在不同作用域中值不同。
-
动态作用域
javascript使用的是词法作用域,它最重要的特征是它的定义过程发生在代码的书写阶段那为什么要介绍动态作用域呢?实际上动态作用域是javascript另一个重要机制this的表亲。作用域混乱多数是因为词法作用域和this机制相混淆,傻傻分不清楚
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();
【1】如果处于词法作用域,也就是现在的javascript环境。变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为2。所以控制台输出2
【2】如果处于动态作用域,同样地,变量a首先在foo()中查找,没有找到。这里会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,找到并赋值为3。所以控制台输出3
两种作用域的区别,简而言之,词法作用域是在定义时确定的,而动态作用域是在运行时确定的
声明提升
- 变量提升
a = 2;
console.log(a);//2
var a ;
console.log(b);//undefine
var b = 2;
var a = 2;
这个代码片段实际上包括两个操作:var a 和 a = 2
第一个定义声明是在编译阶段由编译器进行的。第二个赋值操作会被留在原地等待引擎在执行阶段执行
每个作用域都会进行提升操作
- 函数提升
foo();
function foo(){
console.log(1);//1
}
foo1();
var foo1 = function(){
console.log(1);//TypeError: foo1 is not a function
}
函数声明会提升,但函数表达式却不会提升
上述代码提升后会变为
function foo(){
console.log(1);//1
}
foo();
var foo1;
foo1();
foo1 = function(){
console.log(1);//TypeError: foo1 is not a function
}
这样就容易理解了,函数声明提升了,而字面量形式的函数表达式会使用变量提升,当执行foo1();时,foo1 是一个未赋值的变量,默认赋值为undefine; 自然不能拿当做函数来执行;
- 函数覆盖
函数声明和变量声明都会被提升。但是,函数声明会覆盖变量声明;但是,如果变量存在赋值操作,则最终的值为变量的值
变量的重复声明是无用的,但函数的重复声明会覆盖前面的声明(无论是变量还是函数声明)
var a;
function a(){};
console.log(a);//'function a(){}'
a = 1;
console.log(a);//1
【1】变量的重复声明无用
var a = 1;
var a;
console.log(a);//1
【2】由于函数声明提升优先于变量声明提升,所以变量的声明无作用
var a;
function a(){
console.log(1);
}
a();//1
【3】后面的函数声明会覆盖前面的函数声明
a();//2
function a(){
console.log(1);
}
function a(){
console.log(2);
}
所以,应该避免在同一作用域中重复声明
块级作用域
随着ES6的推广,块作用域也将用得越来越广泛。
- 变量污染
var i = 99;
for (var i= 0; i<10; i++) {
console.log(i);
}
console.log(i);//10
本来只想在循环内使用的变量污染了全局环境;
ES6改变了现状,引入了新的let关键字,提供了除var以外的另一种变量声明方式。let关键字可以将变量绑定到所在的任意作用域中(通常是{…}内部),实现块作用域;
var i = 99;
for (let i= 0; i<10; i++) {
console.log(i);
}
console.log(i);//99
var a = [];
for(let i = 0; i < 5; i++){
a[i] = function(){
return i;
}
}
console.log(a[0]());//0
以及setTimeout同样可以通过在循环中使用let来防止最后输出相同;
let 不可以变量提升,不可以多次声明
除了let以外,ES6还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误;
const声明的常量,也与let一样不可重复声明
- try 中的变量也都为块级变量