什么是作用域
作用域是编程领域经常提及的一个概念,那么到底什么是作用域呢,我想用一句代码来解释
let x = 1
这个语句其实定义了,变量
,值
,变量与值的绑定
,作用域
变量:这里x就是一个变量,是用来指代一个值的符号。
值:就是具体的数据,可以是数字,字符串,对象等。这里1就是一个值。
变量绑定:就是变量和值之间建立对应关系,x = 1就是将变量x和1联系起来了。
作用域:
作用域就是变量绑定的有效范围
。就是说在这个作用域中,这个变量绑定是有效的,出了这个作用域变量绑定就无效了。
变量申明提前
在es6问世之前我们申明变量都是用的var,使用var申明的变量在作用域之内提前,例如
var a = 1
function fn(){
console.log(a)
var a = 2;
}
fn()
其实大家可以猜猜输出的是什么,输出的是undifined
,因为console.log(a)的作用域是fn,所以排除var a = 1的干扰项,在console.log(a)运行的时候 var a
提升到了函数体的最上面但是并没有完成赋值,等到console.log(a)运行完时才去完成的赋值,所以输出的是undefined
,相当于下面的代码
var a = 1
function fn(){
var a;
console.log(a);
a = 2;
}
fn()
函数申明提前
function fn() {
a();
function a() {
console.log(1);
}
}
fn();
上面的代码会输出1,为什么呢,使用申明式定义函数会将整个函数体提升,所以这种方式等价于下面的方式
function fn() {
function a() {
console.log(1);
}
a();
}
fn();
思考
function fn() {
a();
var a = function() {
console.log(1);
}
}
fn();
大家猜猜这段代码会输出什么呢?对的,猜得没错,会报错的,具体原因在上个部分已经说了
变量申明和函数申明提前的优先级
这个很简单,我们做一个实验就知道了
var x = 1;
function x() {}
console.log(typeof x); // number
最终输出的是number,说明了函数申明提前的优先级更高一些
块级作用域
块级作用域就是{}
内定义的变量只能在{}
内才可以访问到,不同于var
定义的变量是不会再提升到函数体的顶部了,这样才符合我们编程的习惯,es6为了这一编程习惯引入了let
和const
来定义块级作用域的变量,const 定义常量,一经定义不可修改
function fn(){
if(true){
let a = 1
}
console.log(a);
}
没错,他会报错,因为我们的a变量只能在{}里面能访问
不允许重复申明
var a = 1;
let a = 2;
没错,他会报错,因为在同一个块中块级作用域只能申明一次
循环语句中的应用
for(var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
})
我们期望的是输入1,2,3但是并没有,输出的是三次3,为什么呢?这个和javascript事件循环机制有关系,javascript在执行的时候遇到宏任务
和微任务
时会将其加入到事件队列里面去,等到主执行栈里面的代码也就是i++执行完之后再把事件队列里面的微任务
拿出来执行,微任务执行完之后再去拿宏任务
出来执行。所以到执行console.log()的时候i已经累加到3了,所以输出的三次都是3。ps:setTimeout是宏任务
那怎么才能输入我们的预期呢,两种借据方案
非es6版本
for(var i = 0; i < 3; i++) {
(function(i) {
setTimeout(() => {
console.log(i)
})
})(i)
}
es6版本
for(let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
})
}
不影响全局对象
在最外层(全局作用域)使用var申明变量,该变量会成为全局对象的属性,如果全局对象刚好有同名属性,就会被覆盖。
var sessionStorage = 'sessionStorage';
console.log(window.sessionStorage); // sessionStorage被覆盖了,输出'json'
复制代码而使用let申明变量则没有这个问题:
let sessionStorage = 'sessionStorage';
console.log(window.sessionStorage); // sessionStorage没有被覆盖,还是之前那个函数
复制代码上面这么多点其实都是let和const对以前的var进行的改进,如果我们的开发环境支持ES6,我们就应该使用let和const,而不是var。
作用域链
前面那个例子的作用域链上其实有三个对象:
f1作用域 -> f作用域 -> 全局作用域
复制代码大部分情况都是这样的,作用域链有多长主要看它当前嵌套的层数,但是有些语句可以在作用域链的前端临时增加一个变量对象,这个变量对象在代码执行完后移除,这就是作用域延长了。能够导致作用域延长的语句有两种:try...catch
的catch
块和with
语句。
try...catch
这其实是我们一直在用的一个特殊情况:
let x = 1;
try {
x = x + y;
} catch(e) {
console.log(e);
}
复制代码上述代码try里面我们用到了一个没有申明的变量y,所以会报错,然后走到catch,catch会往作用域链最前面添加一个变量e,这是当前的错误对象,我们可以通过这个变量来访问到错误对象,这其实就相当于作用域链延长了。这个变量e会在catch块执行完后被销毁。