在解读本文章之前,你需要理解let、const和var的区别,还有this的指向问题。
一、作用域的概念
什么是作用域: 简单来说就是我在我家玩,你在你家玩,当你没来我家的时候我是没办法和你认识的,同样我也没办法向你借钱,我都不认识你咋跟你借钱,对吧?你的钱就只能你用,或者你家人用,而我的钱也同样如此。可能用自己的朋友圈举例子会更为恰当,只有你的圈子里才知道你叫啥名字,同样也只有我的圈子才知道我的名字,除非我们一起玩了,你进入了我的圈子,我们才能相互认识。
- 在 Web 浏览器中,全局作用域被认为是
window
对象,因此所有全局变量和函数都是作为window
对象的属性和方法创建的。 - 在 Node环境中,全局作用域是
global
对象。
要是没理解,别担心,后面讲到具体例子还会讲一遍。
二、作用域的分类
- 全局作用域
- 函数作用域
- 块作用域
- 词法作用域
- 动态作用域
- 作用域链
1.全局作用域:
静态作用域: 静态作用域是指声明时就已经确定作用域
var a = 1;
function fn() {
var b = 2;
console.log(a + b);
}
fn(); // 3
2. 动态作用域:
动态作用域是在运行时确定的,词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,静态作用域是指声明时就已经确定作用域,动态作用域是指哪里调用的这个函数才确定这个作用域。
var name = "ww";
function say() {
//这里仅仅只是定义了一个函数,还未调用,也就是这里的代码还未运行,
//所以还无法确定this指向的作用域
console.log('我的家乡:' + this.name)
}
var china = {
name: '中国',
say,
beijing :{
name: '北京',
say
}
}
//该代码表示1秒后运行这行代码,当随机数是大于0.5时运行china.say(),
//小于等于0.5就运行china.beijing.say();
setInterval(() => Math.random() > 0.5 ? china.say() : china.beijing.say() , 1000)
//china.say()表示是china调用的say方法,那么此时this指向
//china这个对象,然后china中存在name这个变量,
//那么china.name就是"中国",那么say中的this.name就是china.name,也就是"我的家乡:中国";
//而china.beijing.say()则是say是定义在china中的变量beijing这个对象里面,
//那么它的this指向现在变成了beijing这个对象,那么say中的this.name
//就是beijing.name,也就是"我的家乡:北京";
console.log('我的名字:' + this.name);//这里打印'我的名字:ww',这里this就是window,window就是指向全局
3. 函数作用域:
指在函数内声明的所有变量在函数体内始终是可见的,可以在整个函数的范围内使用及复用。
var a = 'a';//老祖宗
function f1() {
var b = 'b';//爷爷
function f2() {
var c = 'c';//爸爸
function f3() {
if(true) {
var d = 'd';//儿子,注意这里是var声明的
}
console.log(a, b, c, d);//孙子继承
}
f3()
}
f2()
}
f1()
//最终会打印"a b c d",因为他是在最里面的函数里面,所以他能访问到所有的变量,
//就跟传家宝一样,爷爷传给爸爸,爸爸传给儿子,儿子传给孙子,
//最后孙子辈能得到爷爷爸爸儿子的所有东西。
4. 作用域链
相信聪明的你已经从上面的例子中大概了解了什么是作用域链了, 作用域链就是从自己一级一级往上查找,直到找到window还没有就结束查找。
再举个例子
var a = "3";
function f1() {
var a = '4';
function f2() {
function f3() {
console.log(a);//实际运行时他先会在f3这个函数内部找,看下有没有定义变量a,
//然后再往上查找到f2函数内部,也没有,继续往上查找到f1,发现存在变量a为4,
//那么返回“4”,结束查找。要是f1里面也没有就继续向上查找,直到查找到window里
//也没有,那么就会报错:a is not defined;说是没有定义变量a。
}
f3()
}
f2()
}
f1()
通过这个例子我们知道,作用域链就是从自己开始一级一级往上查找,直到找到window还没有就结束查找。 举个例子:你作为一个士兵,勇猛杀敌,从小兵到伍长再到校尉再到将军再到兵马大元帅,再到皇帝。直到封无可封了。
5. 块级作用域
块级作用域:ES6推出了let、const实现块级作用域
var a = 'a'
function f1() {
var b = 'b'
function f2() {
var c = 'c'
function f3() {
if(true) {
let d = 'd'; // var 改为 let
console.log("ww", d);//这里可正常访问
}
console.log(a, b, c, d);//这里报错
}
f3()
}
f2()
}
f1()
//这段代码和上面的例子中唯一的区别就是定义变量d时,这里使用了let定义,
//当我们使用let定义变量时,我们就要注意这个大括号{}了,
//只要使用了let去定义变量,那么这个变量就只能在{}里才能访问到了,
//也就是{}就是它的块级作用域,那你在这个{}外的任何地方访问它都会报错:d is not defined
var存在变量提升,let不存在变量提升
这句话其实对也不对,实际上,let 和 const 也会有变量提升:
- 他们会在创建执行上下文时,记录到词法环境中,但是并不会将 undefined 赋给他们(也就是说,他会提升到块级作用域的顶部,但是是 uninitialized 状态)
- var 可以重复声明的原因是,变量提升时会复用执行上下文中变量环境已有的标识符,但是 let 和 const 在提升到块级作用域顶部时, 不会复用词法环境已有的标识符,所以会产生冲突,甚至还会和 var 产生冲突(因为同一个作用域内部,是不允许重复标识符的出现的)
- 在块级作用域顶部和他们声明语句所处的地方之间,如果这个 let 或 const 声明的变量被访问,就会引发一个 ReferenceError,这个就是暂时性死区
console.log(a);
let a = 2;
这个报错意思是不能接受在初始化变量前去访问变量a;
var可以重复定义多次,let重复定义会报错
let a = 1;
let a = 2;
console.log(a)
这个报错意思是变量a已经声明过了;
var定义的变量会挂载在window对象上,它会创建一个新的全局变量作为全局对象的属性
var value1 = "张三";
let value2 = "李四";
const value3 = "王五";
console.log(window.value1) // 张三
console.log(window.value2) // undefined
console.log(window.value3) // undefined
参考文章: