变量理解
- 掌握var、let、const变量声明使用
- 掌握变量生命周期
块作用域
- 块级作用域指在If语句,switch语句,循环语句等语句块中定义变量,这意味着变量不能在语句块之外被访问。
- 简单来说:由
{}包含的代码块所产生的作用域 (即{}的范围)
变量提升
什么是变量提升?
变量提升意味着变量和函数的声明会移动到全局作用域的最前面
- 注意:变量提升,是把变量提升到全局作用域或函数作用域的头部,不会提升变量的初始化和赋值
- 没有把变量的值提升!! 只是声明变量提升,值不提升!!!!
实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中
先上一个简单的例子:
console.log(a);//undefined
var a = "hey I am now hoisting";
看起来,我们在a被声明前调用a,没有报错,反而是返回一个undefined值
- 原因是:a其实已经在调用前被声明了,只是没有被初始化。JavaScript会把作用域里的所有变量和函数提到函数的顶部声明,相当于:
var a;
console.log(a);//undefined
a = "hey I am now hoisting";
函数声明提升
- 前面我们知道,变量可以提升到顶部
- 而函数声明也是可以提升到顶部的
- 可以在函数声明之前 调用函数
sayHi() // Hi there!
function sayHi() {
console.log('Hi there!')
}
为什么会有变量提升?
提高性能
- 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配内存空间
容错性更好
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行。
函数提升和变量提升优先级
在JS代码执行之前,浏览器的解析器在遇到 var 变量名 和function 整个函数 提升到当前作用域的最前面
- 函数提升优先级高于变量提升
- 不会被同名变量声明覆盖,但是会被变量赋值后覆盖
- 存在同名函数与同名变量时,优先执行函数
// 函数提升优先级高于变量提升
function func(s) {
console.log(s);
var s = 2; // 变量提升,值不提升,提升声明变量var
function s() {} // 函数提升,会覆盖之前声明的变量
console.log(s);
}
func(1);
// 相当于以下代码
function func(s) {
console.log(s);
var s; // 被覆盖
function s() {}
s = 2;
console.log(s);
}
func(1);
----------------------------
console.log(a); //f a()
console.log(a()); //1
var a=1;
function a(){
console.log(1);
}
console.log(a); //1
a=3
console.log(a()) //a not a function
变量的生存周期
- 全局变量的生存周期是永久的,除非我们主动销毁。
- 变量的永久生存,且可以随时调用,但是使用时要适度,正是因为它的生命周期长,所以将占据更多的内存。全局变量会在页面关闭后销毁。
- 函数内部的局部变量,在函数执行完毕后销毁。(不考虑闭包等情况)
function add(n) {
const result = 0;
return result + 1;
}
add(1); //1
add(1); //1
变量搜索顺序
- 先局部变量->后全局变量
- 先解析,后赋值
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链
先局部变量->后全局变量
var a=10;
(function(){
var a=20;
alert(a); //20
})()
先解析,后赋值
var a=10;
(function(){
alert(a); //undefined
var a=20;
})()
var
- var声明的命令可以变量提升
- var 可以重复声明变量,因此可以覆盖原有的值,相当于重新赋值了
- var定义的变量可以修改,如果不初始化会输出undefined,不会报错。
var a = 1; console.log('函数外var定义a:' + a);//可以输出a=1 function change(){ a = 4; console.log('函数内var定义a:' + a);//可以输出a=4 } change(); console.log('函数调用后var定义a为函数内部修改值:' + a);//可以输出a=4
let
-
let,用来声明变量,是块级作用域
-
就是所声明的变量,只在let命令所在的代码块内有效
-
如果不初始化会输出undefined,不会报错
-
函数内部使用let定义后,对函数外部无影响
let非常适合用于for循环内部的块级作用域- 用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响
-
let不允许在相同作用域内,重复声明同一个变量。
-
let没有变量提升和let有暂时性死区的约束
- 用
let声明的变量,不存在变量提升 - 而且要求必须 等
let声明语句执行完之后,变量才能使用,不然会报Uncaught ReferenceError错误。
- 用
console.log(aicoder); // 错误:Uncaught ReferenceError ...
let aicoder = 'aicoder.com';
// 这里就可以安全使用aicoder
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。 总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
const
-
const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。 -
const定义的常量不可以修改,而且必须初始化。否则会报错 -
const声明的常量,也与let一样不可重复声明 -
const声明一个只读的常量。一旦声明,常量的值就不能改变。 -
const跟let一样都会产生块级作用域,内部跟外部定义的变量毫不相干
const a = 2;{const a = 1;}console.log(a); //2
const修改对象的理解
const 定义的基本类型不能被修改,而对于引用类型的属性是可以修改的
- const定于对象的属性是可以修改的,因为是同一个地址指针
- const只能保证指向的内存的指针不被改变,如果指针改变那么就会报错
const obj = { a: 123 }
const obj1 = { a: 456 }
//修改属性,是不会报错,因为指向的地址没有发生变化
obj.a = 444
//const只能保证指向的内存的指针不被改变
//如果赋值给了一个新对象,那么就改变了对象指向地址指针,会报错
obj = obj1
console.log(obj)
本质
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心
const 定义的基本类型不能被修改,而对于引用类型是可以修改的【指向的堆内存的地址的指针】
const b = 1; *//正确*
const b; *//错误,必须初始化*
console.log('函数外const定义b:' + b);/有输出值
//b = 5;
//console.log('函数外修改const定义b:' + b);//无法输出
switch语句中的块级作用域
let number = 1;
switch(number){
case 1:
let name = 'Jony';
break;
case 2:
let name = 'yu';
break;
default:
console.log(name);
}
- 这样会发生错误,提示变量重复
- 通过例子可以知道,switch语句中,整个switch语句构成一个块级作用域,而与case无关
- 每一个case并不会构成一个独立的块级作用域。
可修改为
let number = 1;
switch(number){
case 1:
let name = 'jony';
break;
case 2:
name = 'yu';
break;
}
面试题
1、var变量提升问题
var a = 99; // 全局变量a
f(); // f是函数,虽然定义在调用的后面,但是函数声明会提升到作用域的顶部。
console.log(a); // a=>99, 此时是全局变量的a
function f() {
console.log(a); // 当前的a变量是下面变量a声明提升后,默认值undefined
var a = 10;
console.log(a); // a => 10
}
// 输出结果:
undefined
10
99
2、循环输出问题
for (var i = 0; i <10; i++) {
setTimeout(function() { // 同步注册回调函数到 异步的 宏任务队列。
console.log(i); // 执行此代码时,同步代码for循环已经执行完成
}, 0);
}
// 输出结果
10 共10个
// 这里面的知识点: JS的事件循环机制,setTimeout的机制等
- 因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。
如果把 var改成 let声明:
// i虽然在全局作用域声明,但是在for循环体局部作用域中使用的时候,变量会被固定,不受外界干扰。
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // i 是循环体内局部作用域,不受外界影响。
}, 0);
}
// 输出结果:
0 1 2 3 4 5 6 7 8 9
- 在用var声明的for循环中不存在作用域,所以最后会声明到上一级的作用域中,之后循环完后执行异步代码输出i,此时i开始取值,所以取得值都是循环完后的值。再用let声明的for循环中存在块级作用域,每次声明都会存在不同的作用域中,之后循环完后执行异步代码输出i,此时i开始取值,所以取得值是每个作用域对应的i。