在此作用域中尚未声明_一文详解ES6系列——块级作用域

论坛 期权论坛 编程之家     
选择匿名的用户   2021-5-30 09:12   11   0

fdb23c37aeb310bdfa93841af53a07b8.png

var声明与变量提升机制

使用ES5语法时,在函数作用域或全局作用域中通过var关键字声明变量。

function es5DecareVar(condition){
    if(condition) {
        var val = "Y";
    }
    return  val;
}

上述函数,分别输入truefalse

es5DecareVar(true);    // Y
es5DecareVar(false);   // undefined

出现该现象的原因则是变量提升机制(Hoisting),由于在预编译阶段,JavaScript引擎会将es5DecareVar进行修改:

function es5DecareVar(condition){
    var val;
    if(condition) {
        val = "Y";
    }
    return  val;
}

将函数中的变量声明均提升至函数顶部,而初始化操作依旧在原处执行,造成了if条件外的变量尚未初始化,所以其值为undifinded

ES6 中的块级作用域强化了对变量生命周期的控制。

块级声明

用于声明在制定块的作用域之外无法访问的变量,也被称为词法作用域,存在于 - 函数内部 - 块中(字符{}之间)

let声明

let声明的用法与var相同,使用let代替var声明变量,可将变量的作用域限制在当前代码块中,且不会被提升。

function es6DecareVar(condition){
    if(condition) {
        let val = "Y";
    }
    return  val;
}

同样的函数,分别输入truefalse

es6DecareVar(true);    // Y
es6DecareVar(false);   // undefined

结果却和使用var一样,但其中的原因却不相同。

使用var时产生的undifinded是由于变量提升机制导致的;

而使用let时,却是由于临时死区(Temporal Dead Zone)造成的。

在解释临时死区前,先了解下ES6中另一个变量声明const

const声明

const声明的是常量,其值一旦被设定后不可更改,因此每个通过const声明的常量必须进行初始化。

const val = "Y";    // 有效声明的常量

const invalidVal;  // 无效声明的常量

val = "N";          // 常量无法修改

可见,常量的声明必须初始化,且无法更改,但使用const完成对象的声明却有些许不同。

const object = {
    id: 1,
    name:  "A"
}

// 可以修改对象属性的值
object.name = "B";

// 抛出语法错误
object = {
    id: 2,
    name:  "C"
}

对象常量的属性值可变更,但却无法改变对象本身的绑定关系。即const声明不允许修改绑定,但允许修改绑定的值。

具体原因引用阮一峰老师在《ECMAScript 6 入门》所述。

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

临时死区

letconst声明的变量并不会被提升到作用域的顶部,如果在声明之前访问这些变量,即使是相对安全的typeof操作符也会触发引用错误。

console.log(typeof value);  // 抛出语法错误
let value = "Y";

原因是当JavaScript引擎在扫描代码发现变量声明时,会根据不同的关键字采取不同的操作。

  • 遇到var声明时,会将变量提升至作用域顶部
  • 遇到letconst声明时,将声明放到临时死区中

只有执行过变量声明语句后,变量才会从临时死区中移出,可被正常访问,否则会触发运行时错误。

块作用域在循环中的使用

var 声明由于变量在循环之外仍能访问,会造成预料之外的结果。

var list = [];

for(var i=0; i < 10; i++){
    list.push(()=>{
        console.log(i);
    })
}

list.forEach((func)=>{
    func();     // 输出10次 数字10
})

为了解决这个问题,通常的方式是在循环中使用立即调用函数表达式(IIFE),强制生成计数器变量的副本。

var list = [];

for(var i=0; i < 10; i++){
    list.push(((val)=>{
        return ()={
            console.log(val);
        }
    }(i)));
}

list.forEach((func)=>{
    func();     // 输出0-9
})

上面的方法在循环内部,IIFE表达式为接手的每一个变量i都创建了一个副本并存储为变量val,从而每个函数被执行后会输出0-9。

而使用ES6的letconst提供的块级绑定可轻松解决。

let在循环中的使用

上述问题使用let关键字来改写:

var list = [];

for(let i=0; i < 10; i++){
    list.push(()=>{
        console.log(i);
    })
}

list.forEach((func)=>{
    func();     // 输出0-9
})

每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于各自的i的副本。

使用for-infor-of也会得到同样的效果。

const在循环中的使用

如果使用const关键字来改写:

var list = [];

// 完成一次迭代后抛出语法错误
for(const i=0; i < 10; i++){
    list.push(()=>{
        console.log(i);
    })
}

原因在于变量i被声明为常量,完成一次迭代后,循环语句试图修改常量i导致语法错误。

而使用for-infor-of则由于每次迭代不会修改已有的绑定关系,而是会创建一个新的 绑定。

var list = [],
    object = {
        a: 1,
        b: 2,
        c: 3
    }

for(const key in object){
    list.push(()=>{
        console.log(key);
    })
}

list.forEach((func)=>{
    func();     // 输出1-3
})

块级绑定的最佳实践

constlet的唯一区别在于,const可以让数值、字符串和布尔变量不可变,且定义的对象始终指向同一个对象。

由于大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头,故建议的使用方式为:

  • 默认使用const
  • 确实需要改变变量的值时使用let
  • 不使用var

如果觉得文章能帮助你理解一些前端知识点,请点赞、关注本专栏,也可关注微信公众号。

f2cbb79db8290bd849362bdbb2507860.png
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP