JS中的作用域与变量提升 | 青训营笔记

70 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的的第10天

JS中的作用域

ES6块级作用域:

每个{}大括号内就算一个块级作用域 ❎

每个函数内,或者用 let / const 定义的变量 / 常量 存在一个块级作用域 ✅

作用域链

内部函数访问外部函数的变量,采取的是链式查找

根据作用域链从函数内往外找,来决定 变量/方法 取哪一个值(哪怕是undefined也算找到)

function fun() {
    window.a = 10
    console.log(a) // 打印undefined
    var a = 20     // 注意这里有变量提升,打印undefined只不过是因为“作用域链”
}
fun()


function fun() {
    a = 10  // 如果是不加var,是先执行window.a = 10呢?还是变量提升var a呢?
    console.log(a) // 答案是先变量提升var a,那么a = 10 就不能解释成window.a = 10 了
    var a = 20
    }
fun() // 所以打印10 

连续声明变量

连续声明变量时,没有用 var / let / const 声明的,相当于在全局声明(window.xxx)

function test(){
    var a = 1, b = 1;
    return b;
}
function test1(){
    var a1 = b1 = 1; // 相当于先 window.b1 = 1	然后 var a1 = b1
}
test();
// console.log(a); // undefined a是局部的
// console.log(b); // undefined b是局部的

test1();
// console.log(a1);// undefined a1是局部的
console.log(b1); // 1 说明b1是全局的,相当于 window.b1 = 1

var 的变量声明提升

JS引擎运行.js分为2步:预解析、代码执行

  1. 预解析:js引擎会把.js 里面的所有 var 和 function 提到当前作用域的最前面▲
var name = 'a';
(function () {
    if (typeof name == 'undefined') {
        var name = 'b'
        console.log('111' + name)
    } else {
        console.log('222' + name)
    }
})()
// 结果是 111b
因为if else 虽然有{},但它不是函数,所以没有块级作用域,它处在 立即执行函数的块级作用域
所以 立即执行函数的作用域 只要发现里面有 var 哪怕判断条件不对,都会先把 var 提升到当前作用域上面声明

上面的例子相当于
var name = 'a';
(function () {
    var name	// 变量声明提升
    if (typeof name == 'undefined') {
        name = 'b'
        console.log('111' + name)
    } else {
        console.log('222' + name)
    }
})()
  1. 代码执行:按照书写顺序从上往下执行
function fun() {
    a = 10
}
fun()
console.log(a)
// 10
----------------------------------------------------------------------------
即使函数声明提升了,但是代码执行顺序从上往下,都还没到执行函数内部的时候呢
console.log(a)
function fun() {
    a = 10
}
fun()
// 报错,a未定义

预解析的执行顺序

预解析分为:变量预解析(变量提升)和 函数预解析(函数提升)

  1. 变量(包括函数表达式)提升:只提升变量声明,不提升变量赋值

  2. 函数提升:只提升函数(function)声明,不调用函数

  3. js代码执行顺序:传入的参数 > 函数提升 > 变量提升(只声明不赋值) > window. (你连声明都没声明就直接赋值,我才挂到 window. 上)

    传参赋值 会优先于 变量提升 执行▲ 变量提升中,函数声明 会优先于 变量声明 执行

▲ 函数声明和赋值相当于是一起执行的!!!
function fun() {
    console.log(a)
    var a
    function a() {
        return 666
    }
}
fun()
// 打印 ƒ a() {return 666}
所以上面相当于
function fun() {
    function a() {
        return 666
    }
    var a
    console.log(a)
}
fun()
----------------------------------------------------------------------------
▲ 传参赋值 先于函数变量提升执行;又因为函数声明和赋值相当于是一起执行的,所以被函数覆盖
function fun(a) {
    function a() {
        return 666
    }
    console.log(a)
}
fun(100)
// 打印 ƒ a() {return 666}
----------------------------------------------------------------------------
▲ 函数提升 > 变量提升
function fun() {
    var a = 10
    function a() {}
    console.log(a) // 结果是 10
}
fun()

上面例子相当于
function fun() {
    function a() {}
    var a 	// 注意,此时a 是 ƒ a() {},并不是undefined
    a = 10	// 后面才赋值成了10
    console.log(a)
}
fun()
----------------------------------------------------------------------------
▲ 变量提升 > window. 
function fun() {
    a = 10
    console.log(a) // 打印 10
    var a = 20
    }
fun()

上面例子相当于
function fun() {
    var a	// 优先于 window. 
    a = 10
    console.log(a) // 打印 10
	a = 20
    }
fun()
----------------------------------------------------------------------------
▲ 综合起来:传入的参数 > 函数提升 > 变量提升(只声明不赋值) >  window. 
function fun(a) {
    console.log(a)
    var a = 10
    function a() {
        return 666
    }
}
fun(100)
// 打印 ƒ a() {return 666}

所以上面相当于
function fun(a) {
    var a = 100
    function a() {
        return 666
    }
    var a
    console.log(a)
    a = 10
}
fun(100)
参数 > 变量
function fun(a) {
    var a
    console.log(a)
    a = 10
}
fun(100)
// 打印 100
----------------------------------------------------------------------------
函数 > 变量
function fun() {
    function a() {
        return 666
    }
    var a
    console.log(a)
    a = 10
}
fun()
// 打印 ƒ a() {return 666}
  1. 声明变量并不是调到最前面,而是紧跟在同级的,原本就有的声明之后
var a = 18;	// 拆分成 var a; 和 a = 18;
fun();
function fun() {
    var b = 9;
    console.log(a);
    console.log(b);
    var a = '123';
}

// 相当于以下代码
var a;
function fun() {	// 函数并不是调到最前面,而是紧跟在原本就有的声明之后
          	var b;
    		var a; // a并不是调到最前面,而是紧跟在原本就有的声明之后
    		b = 9;
          	console.log(a);
          	console.log(b);
          	a = '123';
      }
a = 18 ;
fun();
// 最后打印 undefined 和 9

例题:

f1();
console.log(c);
console.log(b);
console.log(a); 
function f1() {
        var a = b = c = 9;	//相当于a有声明,局部变量;b和c没声明,是全局变量
        console.log(a);
        console.log(b);
        console.log(c);
      }
//输出5个9,最后报错

暂存死区

与通过 var 声明的有初始化值 undefined 的变量不同,通过 let 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。

看过很多文章后用大佬的话做下总结

  1. let有无变量提升取决于你如何定义变量提升。 若[变量提升」是指变量可在声明语句之前被调用,则let没有变量提升;若[变量提升」是指变量在声明语句之前就被执行上下文记住,则let有变量提升。【let 声明的变量,虽然不能在声明前知道它的值,但是能让上下文知道存在这个变量

  2. JS代码是即时编译与执行的,一个函数作用域会拥有一个执行上下文,执行上下文是一块存储空间。执行上下文内又有名为「变量环境」和「词法环境」2个环境。

  3. var和function声明的变量,在代码编译完成后,执行之前,其变量名和值就被存储在变量环境中了,所以在代码执行阶段的任何时刻,都可以调用它们,自然也能在声明语句之前调用了。

  4. const和let声明的变量,在代码编译完成后、执行之前,其变量名被存储在**「词法环境」中,代码执行过程中会从依据【词法环境→变量环境→闭包/上一个作用域】的顺序来查找变量**,而词法环境所存储的值被要求只有在声明语句之后才能调用。所以会存在暂时性死区,但变量又确确实实被执行上下文提前记住了,所以可以把暂时性死区理解为「变量暂时不能使用的阶段」。所以得出结论①

var a = 456;
(function() {
    console.log(a)
    let a=123
})()

如果let没有变量提升的话输出的应该是456
但是如果你执行代码的话会报错 Cannot access 'a' before initialization 初始化前无法访问“a”

想不到吧!从这里其实可以看出let存不存在变量提升有争议了,值在变量显式赋值之前不能对变量进行读写,否则就会报错,这也就是所谓的letconst的暂时性死区。