跳到主要内容

变量理解

  • 掌握var、let、const变量声明使用
  • 掌握变量生命周期

块作用域​

  • 块级作用域指在If语句,switch语句,循环语句等语句块中定义变量,这意味着变量不能在语句块之外被访问。
  • 简单来说:由 {}包含的代码块所产生的作用域 (即 {} 的范围)

变量提升​

什么是变量提升?

变量提升意味着变量和函数的声明会移动到全局作用域的最前面

  • 注意:变量提升,是把变量提升到全局作用域或函数作用域的头部,不会提升变量的初始化和赋值
    • 没有把变量的值提升!! 只是声明变量提升,值不提升!!!!

实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中

先上一个简单的例子:

js
console.log(a);//undefined
var a = "hey I am now hoisting";

看起来,我们在a被声明前调用a,没有报错,反而是返回一个undefined值

  • 原因是:a其实已经在调用前被声明了,只是没有被初始化。JavaScript会把作用域里的所有变量和函数提到函数的顶部声明,相当于:
js
var a;
console.log(a);//undefined

a = "hey I am now hoisting";

函数声明提升​

  • 前面我们知道,变量可以提升到顶部
  • 而函数声明也是可以提升到顶部的
  • 可以在函数声明之前 调用函数
js
sayHi() // Hi there!

function sayHi() {
console.log('Hi there!')
}

为什么会有变量提升?​

提高性能​

  • 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能
  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配内存空间

容错性更好​

  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行。

函数提升和变量提升优先级​

在JS代码执行之前,浏览器的解析器在遇到 var 变量名 和function 整个函数 提升到当前作用域的最前面

  • 函数提升优先级高于变量提升
  • 不会被同名变量声明覆盖,但是会被变量赋值后覆盖
  • 存在同名函数与同名变量时,优先执行函数
js
// 函数提升优先级高于变量提升
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

变量的生存周期​

  • 全局变量的生存周期是永久的,除非我们主动销毁。
    • 变量的永久生存,且可以随时调用,但是使用时要适度,正是因为它的生命周期长,所以将占据更多的内存。全局变量会在页面关闭后销毁。
  • 函数内部的局部变量,在函数执行完毕后销毁。(不考虑闭包等情况)
js
function add(n) {
const result = 0;
return result + 1;
}

add(1); //1
add(1); //1

变量搜索顺序​

  • 先局部变量->后全局变量
  • 先解析,后赋值

当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链

先局部变量->后全局变量​

js
var a=10;
(function(){
var a=20;
alert(a); //20
})()

先解析,后赋值​

js
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错误。
js
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一样都会产生块级作用域,内部跟外部定义的变量毫不相干

    js
    const a = 2;
    {
    const a = 1;
    }
    console.log(a); //2

const修改对象的理解​

const 定义的基本类型不能被修改,而对于引用类型的属性是可以修改的

  • const定于对象的属性是可以修改的,因为是同一个地址指针
  • const只能保证指向的内存的指针不被改变,如果指针改变那么就会报错
js
const obj = { a: 123 }
const obj1 = { a: 456 }
//修改属性,是不会报错,因为指向的地址没有发生变化
obj.a = 444

//const只能保证指向的内存的指针不被改变
//如果赋值给了一个新对象,那么就改变了对象指向地址指针,会报错
obj = obj1
console.log(obj)

本质​

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心

const 定义的基本类型不能被修改,而对于引用类型是可以修改的【指向的堆内存的地址的指针】

js
const b = 1; *//正确*
const b; *//错误,必须初始化*
console.log('函数外const定义b:' + b);/有输出值
//b = 5;
//console.log('函数外修改const定义b:' + b);//无法输出

switch语句中的块级作用域​

js
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并不会构成一个独立的块级作用域。

可修改为

js
let number = 1;
switch(number){
case 1:
let name = 'jony';
break;
case 2:
name = 'yu';
break;
}

面试题​

1、var变量提升问题​

js
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、循环输出问题​

js
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声明:

js
// 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。