什么是作用域?
先来看2个例子:
栗子1:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5](); // 10
栗子2:
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5](); // 5
为什么栗子1中结果是10而不是5呢?反而栗子2中是5呢?
首先,在栗子1中,我们在for循环里使用的是var
声明里一个i
。
在for循环中使用var i = 0
;相当于声明了一个全局变量,每次改变都是改变同一个全局变量i
,后面声明的会覆盖前面声明的全局变量;
其次,循环中,数组 a 中的所有值都指向function(){}
,这个方法被不断的重写,即i=0
时,指向log(0)
;i=1
时,i=0和i=1
都指向了log(1)
;执行到a[9]时,a[0]~a[9] 都指向了log(9)
;
当循环结束后,i
又++,i
自然等于10,所以a[5]()
输出为10。
再来看一个栗子3:
for(var i = 0; i<5;i++){
setTimeout(()=>{
console.log(i)
},0)
}
// 5 5 5 5 5
上面的代码因为setTimeout
是一个异步,所以它拿到的是循环结束后的i
的值,因为上面我们说的var
是全局变量会被覆盖掉所以最后的i
值是5,而且一共循环了5次(0-4),所以会打印触五个5。
以下栗子4也说明了这个问题:
{
var i = 0;
setTimeout(() => {
console.log(i); //输出2
}, 1000);
}
{
var i = 1;
setTimeout(() => {
console.log(i); //输出2
}, 1000);
}
{
var i = 2;
setTimeout(() => {
console.log(i); //输出2
}, 1000);
}
//相当于每次循环给i重复声明一次全局变量,后面覆盖前面的,
//循环结束,打印时访问的变量在局部找不到,就去向上级查找,找到全局变量,都为2
在栗子2中,我们在for循环里使用的是let
声明里一个i
。
而let
具有块级作用域,相当于声明了一个局部变量。
当前的i
只在本轮循环有效,每一次循环的i
其实都是一个新的局部变量;
循环结束,打印时访问每次循环的局部变量,所以打印0,1,2,3,4,5,6,7,8,9
,所以a[5]()
输出为5;
把上面栗子3里的var
改完let
,我们再看看效果,栗子5
for(let i = 0; i<5;i++){
setTimeout(()=>{
console.log(i)
},0)
}
// 0
// 1
// 2
// 3
// 4
一、作用域
回到初始问题,什么是作用域?
作用域是可访问变量的集合。
在 JavaScript 中,对象和函数同样也是变量。
在 JavaScript 中,作用域为可访问变量,对象,函数的集合。
作用域永远都是任何一门编程语言中的重中之重,因为它控制着变量与参数的可见性与生命周期。
在JS中一共有两种作用域:全局作用域和函数作用域。
在ES6中新增的块级作用域。
全局作用域
- 直接编写在
script
标签中的JS代码,都在全局作用域 - 全局作用域在页面打开时创建,在页面关闭时销毁
- 在全局作用域中有一个全局对象
window
(它代表的是一个浏览器的窗口),我们可以直接使用 - 在全局作用域中:
- 创建的变量都会作为
window
对象的属性保存 - 创建的函数都会作为
window
对象的方法保存
- 创建的变量都会作为
- 全局作用域中的变量都是全局变量
- 在页面的任意的部分都可以访问的到
变量的声明提前
- 使用
var
关键字声明的变量,会在所有的代码执行之前被提前声明(但是不会赋值),但是如果声明变量时不使用var
关键字,则变量不会被声明提前
函数的声明提前
- 使用函数声明形式创建的函数
var fn = function(){ }
,它会在所有的代码执行之前就被创建
函数作用域
也称为局部作用域,变量在函数内声明。
- 调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
- 每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的
- 在函数作用域中可以访问到全局作用域的变量
- 在全局作用域中无法访问到函数作用域的变量
- 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用
- 如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错
ReferenceError: xxx is not defined
- 如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错
在函数作用域也有声明提前的特性,使用var
关键字声明的变量,会在函数中所有的代码执行之前被提前声明
如果变量在函数内没有声明(没有使用 var
关键字),该变量为全局变量。
//函数作用域 : 在函数里面声明的变量,只能在函数内部被访问
//函数作用域:函数里面声明的变量,无论是var声明,还是let声明,都是局部变量
function fn() {
var a = 10;//局部变量
let b = 20;//局部变量
console.log(a,b);//10,20
};
fn();
console.log(a);//报错 a is not defined
console.log(b);//报错 b is not defined
块级作用域
ES5
中没有块级作用域,因此在全局作用域下的 if
或 for
或 {}
中声明的变量都是全局变量,如果 if
或 for
或 {}
在函数作用域中,则在其内部声明的变量可以在函数作用域中使用;
块级作用域是新增命令let
和const
来体现。
同时,块级作用域并不影响var
声明的变量,var
声明的变量的性质和原来一样,还是具有变量提升的特性。
注:由于let
和const
属于ES6
,所以都必须使用严格模式,否则会报错。
任何一对花括号 {}
中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
//在大括号里面 且 使用let声明的变量
if(true){
var a = 10;//全局变量
let b = 20;//块级变量(块级作用域 :只在大括号内部起作用)
console.log(a,b);//10,20
};
console.log(a);//10
// console.log(b);//报错 b is not defined
为什么需要块级作用域?
ES5
只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能覆盖外层变量:
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = "hello";
}
}
fn(); // undefined
上面代码中,函数fn()
执行后,输出结果为 undefined
,原因在于变量提升,导致内层的 tmp
变量覆盖了外层的 tmp
变量。(当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用)
第二种场景,用来技术的循环变量泄露为全局变量:
var s = "hello";
for(var i=0;i<s.length;i++){
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
ES6的块级作用域
function f1(){
let n = 5;
if(true){
let n = 10;
}
console.log(n); // 5
}
上面的函数有2个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用 var
定义变量n,最后输出的值就是10。
1.外层作用域无法读取内层作用域的变量:
{
{let insane = "hello"}
console.log(insance); // 报错 ReferenceError: insance is not defined
}
2.内层作用域可以定义外层作用域的同名变量:
{
let a = "hello";
{let a = "hello"}
}
3.块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)
不再必要了
// IIFE写法
(function(){
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
ES6
以前变量的作用域是函数范围,有时在函数内局部需要一些临时变量,因为没有块级作用域,所以就会将局部代码封装到IIEF中,这样达到了想要的效果又不引入多余的临时变量。而块作用域引入后,IIEF当然就不必要了!
function f(){
...
swap(var_a,var_b);
(function swap(a,b){
var tmp;
tmp = a;
a = b;
b=tmp;
})(var_a,var_b);
}
如上面的代码, tmp
被封装在IIFE中,就不会污染上层函数;而有块级作用域,就不用封装成IIEF,直接放到一个块级中就好。
function f(){
let a,b;
...
{
let tmp;
tmp = a;
a = b;
b=tmp;
}
}
更简单的说法是,立即执行匿名函数的目的是建立一个块级作用域,那么现在已经有了真正的块级作用域,所以立即执行匿名函数就不需要了。
二、let、const和var的区别
1. let/const不允许重复声明变量,var可以
注:var已经声明的变量名,let/const也不能重复声明
var a = 1;
var a = 2;
console.log(a); //输出结果为 2
let b = 2;
let b = 3;
console.log(b); //报错但是,如果let声明了一个变量,变量的值可以改变。
let b = 2;
b = 3;//这里是赋值,不是声明
console.log(b); //3
2. let/const没有变量提升
也就是不会在预解析的时候进行解析。
var
: 具有变量提升。 (在声明前可以访问变量,获取的是undefined
)let、const
: 暂时性死区。 (在声明前不可以访问变量,会报错ReferenceError
)
暂时性死区:与通过
var
声明的有初始化值undefined
的变量不同,通过let
声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致ReferenceError
。该变量处在一个自块顶部到初始化处理的“暂存死区”中。
console.log(a); //undefined
var a = 1;
console.log(b);
let b = 2; //报错 ReferenceError: Cannot access 'b' before initialization
console.log(c);
const c = 8; //报错 ReferenceError: Cannot access 'c' before initialization
3. let/const 具有块级作用域
{
let a=1;
console.log(a) //1
}
console.log(a); //ReferenceError: a is not defined
{
let a=1;
console.log(a) //1
}
console.log(a); //1
ES6中允许块作用域任意嵌套,外层无法访问内层的变量。但是内层可以访问外层,也就是子级访问父级;
4.常量const
注意:一旦声明,常量的值不能改变的!
1.const常量一定要赋值,否则报错
const a;
console.log(a); //SyntaxError: Missing initializer in const declaration
2. const常量的值不能改变,改变会报错
const a = 10;
a = 99;
console.log(a); //TypeError: Assignment to constant variable.
//使用 const 定义的对象或者数组,其实是可变的。
// 创建常量对象
const car = {type:"Fiat", model:"500", color:"white"};
// 修改属性
car.color = "red";
// 添加属性
car.owner = "Johnson";
//但是不能对常量对象重新赋值:
const car = {type:"Fiat", model:"500", color:"white"};
car = {type:"Volvo", model:"EX60", color:"red"}; // 错误 TypeError: Assignment to constant variable.
//数组同样道理
// 创建常量数组
const cars = ["Saab", "Volvo", "BMW"];
// 修改元素
cars[0] = "Toyota";
// 添加元素
cars.push("Audi");
//但是不能对常量数组重新赋值:
const cars = ["Saab", "Volvo", "BMW"];
cars = ["Toyota", "Volvo", "Audi"]; // 错误 TypeError: Assignment to constant variable.
3. 常量不能重复声明,否则报错
const a = 10;
const a = 99;
console.log(a); //SyntaxError: Identifier 'a' has already been declared
四、创建、初始化和赋值
1.我们来看看 var 声明的「创建、初始化和赋值」过程
假设有如下代码:
function fn(){
var x = 1 ;
var y = 2 ;
}
fn()
在执行 fn
时,会有以下过程(不完全):
- 进入
fn
,为fn
创建一个环境。 - 找到
fn
中所有用var
声明的变量,在这个环境中「创建」这些变量(即 x 和 y)。 - 将这些变量「初始化」为
undefined。
- 开始执行代码
- x = 1 将 x 变量「赋值」为 1
- y = 2 将 y 变量「赋值」为 2
也就是说 var
声明会在代码执行之前就将「创建变量,并将其初始化为 undefined」。
这就解释了为什么在 var x = 1
之前 console.log(x)
会得到 undefined。
2.接下来来看 function 声明的「创建、初始化和赋值」过程
假设代码如下:
fn2()
function fn2(){
console.log(2)
}
JS 引擎会有以下过程:
- 找到所有用
function
声明的变量,在环境中「创建」这些变量。 - 将这些变量「初始化」并「赋值」为
function(){ console.log(2) }
。 - 开始执行代码
fn2()
也就是说 function
声明会在代码执行之前就「创建、初始化并赋值」。
3.接下来看 let 声明的「创建、初始化和赋值」过程
假设代码如下:
{
let x = 1;
x = 2;
}
我们只看 {}
里面的过程:
- 找到所有用
let
声明的变量,在环境中「创建」这些变量 - 开始执行代码(注意现在还没有初始化)
- 执行 x = 1,将 x 「初始化」为 1(这并不是一次赋值,如果代码是
let x
,就将x
初始化为undefined
) - 执行 x = 2,对 x 进行「赋值」
这就解释了为什么在 let x
之前使用 x 会报错:
let x = 'global';
{
console.log(x) // ReferenceError: Cannot access 'x' before initialization
let x = 1
}
原因有两个
console.log(x)
中的 x 指的是下面的 x,而不是全局的 x- 执行 log 时 x 还没「初始化」,所以不能使用(也就是所谓的暂时死区)
看到这里,你应该明白了 let 到底有没有提升:
let
的「创建」过程被提升了,但是初始化没有提升。var
的「创建」和「初始化」都被提升了。function
的「创建」「初始化」和「赋值」都被提升了。
最后看 const
,其实 const
和 let
只有一个区别,那就是 const
只有「创建」和「初始化」,没有「赋值」过程。
五、关于let/const声明的变量在window里无法获取到的问题
为什么会出现这种问题,就需要知道ES6与ES5变量声明方面的区别了:
- ES5 声明变量只有两种方式:var和function。
- ES6 有let、const、import、class再加上 ES5 的var、function共有六种声明变量的方式。
- 还需要了解顶层对象:浏览器环境中顶层对象是window,Node中是global对象。
- ES5中,顶层对象的属性等价于全局变量。(敲黑板了啊)
- ES6中,有所改变:var、function声明的全局变量,依然是顶层对象的属性;let、const、class声明的全局变量不属于顶层对象的属性,也就是说 ES6 开始,全局变量和顶层对象的属性开始分离、脱钩。
所以ES6非严格模式下,与var声明的全局变量都会成为window的属性:
a = 1;
console.log(window.a); // 1
var b = 2;
console.log(window.b); // 2
而使用let/const声明的全局变量,不会成为window的属性:
let c = 3;
console.info(window.c); // undefined