作用域的新大陆
全局作用域是人尽皆知的,前几章我们也着重介绍了函数作用域。
最后一个兄弟是 块级作用域。
那么,什么是块级作用域呢?我们什么场景下会用到它呢?块级作用域有什么好处呢?...
让我们慢慢往下揭晓答案。
什么是块级作用域
原文中的描述是这样的:
变量的声明应该距离使用的地方越近越好,并最大限度地本地化。
块作用域是一个用来对之前的最小授权原则进行拓展的工具,将代码从在函数中隐藏信息拓展为在块中隐藏信息。
最直观地理解“块”,就是{}。
对,就是那个花括号。
比如:
for(var i=0; i<10; i++) {...}
在这个循环体里面,{} 就能形成个块。但注意啊,此时还不能真正地发挥一个块级作用域该有的功能。
因为我们定义循环体中的 i 时,用的是 var 这个关键字。
使用 var 声明的变量都属于外层作用域。
在上面这个 case 中,i 属于全局作用域。不信你看:
我把console.log(i)
放到循环体外打印,还是能获取到 i 这个变量,说明 i 这个变量并没有被限制在 {} 的作用域里。
那么我们肯定希望,让这个 {} 成为“切切实实”的一个块级作用域,对吧。
此时我们只需要把 var 改成 let 即可。
看!这样在外面就访问不了变量 i 了。
这里还能引申出一个很重要的问题。var 和 let 的区别。
出现上面现象的原因是 var 定义一个变量的时候,会存在提升,但 let 不会。
啥是提升呢?
提升指的是,声明会被视为存在于其所出现的作用域的整个范围内。
var 声明的变量允许先使用再声明。
简单说来就是,var 定义的 i 会先提升到外层去,归属于外层作用域(这里体现在全局上)。
但 let 定义的 i 不会存在提升,必须先声明后使用,归属于当前作用域。
在声明这个步骤的时候,就已经绑定到当前作用域不变了。
请往下看,注意理解绑定的含义。
怎么“制造”块级作用域
但为什么 let 就能够形成一个有效的块级作用域呢?
ES6引入了新的关键字 let。
let 关键字能够将变量绑定到所在的任何作用域中。
也就是说,用 let 声明的变量隐式地创建了它所在的块级作用域。
if (foo) {
{ // <-- 显式的块
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
如果去掉那个显示的 {} 块,就是相当于把 let 定义的 bar 隐式地附加在已经存在的 if 块级作用域中了。
说白了,就是用了 let 去定义后,能把变量绑定到当前的块级作用域中。
外部也就无法访问内部的变量了。
const 关键字也一样能够创建块级作用域变量。
它和 let 的唯一区别就是 const 定义的常量不能被修改。
块级作用域的用处和意义
还是拿上面的例子看问题。
for (var i = 0; i < 10; i++) {
console.log(i);
}
这可能造成一个闭包,能发现不?
闭包是一个函数和其周围的状态(词法环境)的引用捆绑在一起。
闭包通常在嵌套函数中产生,当一个内层函数访问其外层函数的变量时,就会形成闭包。
好,我们改装一下啊:
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button ' + i + ' clicked');
});
}
当点击事件触发时,循环已经完成。
i 的值是最终的值,即buttons.length
。
那么我们在里面打印 i,是不是属于内层去访问外层作用域的变量了。
那是不是就形成了一个闭包。
而我们每次点击 button,都会让每一个事件处理程序共享同一个 i,导致打印出来的i都是同一个,这就是闭包带来的问题。
理解了这个之后,我们再来看,用 let 改造有什么好处。
根据上面的结果表明,改为 let 声明 i后,能够输出我们的预期结果,对吧。
因为当我们引入了 let 之后,每次循环迭代的时候 let 定义的变量都会创建一个新的绑定,而不是共享同一个绑定。
如此一来,每个点击事件处理函数都能捕获到自己的 i 值,就能够解决闭包带来的问题了。
小结
今天主要介绍了块级作用域的概念、用法以及优势,结合了前面的知识点,加深理解作用域的概念。
tomorrow 也继续加油!干巴爹~