ES6块级作用域---超详细!
1. 概述
1.1 变量的作用域
- ES6之前,变量的作用域只有两种:
- 局部作用域:只能在当前函数内部使用
- 全局作用域:可以在任意函数内使用——是全局对象的成员
- ES6中,增加了一种变量作用域:
- 块级作用域:只能在当前块内使用
1.2 为什么要用块级作用域
- 变量提升,内层变量覆盖外层变量
var i = 10;
function func() {
console.log(i);
if (true) {
var i = 5;
}
}
func(); // undefined
为什么是undefined?
var i = 10;
function func() {
var i; //变量提升
console.log(i); // undefined
if (true) {
var i = 5;
}
}
func();
- 用来计数的循环变量污染了全局。下例变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
for(var i=0;i<10;i++){
console.log(i);
}
console.log(i); //10
1.3 ES6块级作用域的使用
- let为JS新增了块级作用域
{
let i=10;
}
console.log(i); //i is not defined
- 块级作用域可以任意嵌套,外层作用域无法读取内层作用域的变量
{
{
{
{
{
let i=10
}
console.log(i); // 报错
}
}}};
- 内层作用域可以定义外层作用域的同名变量
{
{
{
{
{
let i=10;
console.log(i); //10
}
let i=1;
console.log(i); //1
}
}}};
- ES6允许在块级作用域中声明函数
function f() {
console.log('I am outside!');
}
(function () {
if (false) {
function f() {
console.log('I am inside!');
}
}
f();
}());
在ES5中,会得到 I am inside!,因为存在函数提升(整个函数都会被提升)会将整个函数提升到匿名函数作用域顶部,外部的f()函数被覆盖,所以f()的结果是I am inside!
在ES6中,我们分两种情况看:
- 严格模式下,得到的结果是I am outside! ,因为内部的f()函数在if的块级作用域内,无法影响外部全局的f()函数
- 非严格模式下,会报错: Uncaught TypeError: f is not a function。
2. 一道高频笔试题解析
我们先看看下面这段代码:
<body>
<button>click me</button>
<button>click me</button>
<button>click me</button>
<script>
var btns=document.getElementsByTagName("button");
for(var i=0;i<btns.length;i++){
btns[i].onclick=function(){
alert(`我是第${
i}个按钮`)
}
}
</script>
</body>
上述代码想实现:一个网页上有三个完全相同按钮,我们希望点第一个按钮时,弹出“我是第0个按钮”,点第二按钮弹出“我是第1个按钮”,点第三个按钮弹出“我是第2个按钮”,但是运行结果却不是我们预期的——无论我们点击哪个按钮,结果都是“我是第3个按钮”,为什么呢?
首先,我们要知道:
- 循环结束时,i的值为3;
- οnclick=function(){alert(
我是第${i}个按钮
)} 是绑定事件处理函数,只定义函数并不执行,只有当用户点击了一个按钮后才会执行。
我们现在点击一个按钮,触发事件开始执行onclick函数。在onclick函数的作用域链对象中,按照从局部到全局的顺序引用对象。局部对象指向了onclick函数临时创建的函数作用域对象(活动对象),活动对象中保存着onclick函数可用的局部变量。现在我们思考,onclick函数中是否有局部变量?
i是局部变量吗???绝对不是!!!,i只是一个变量而已,所以我们在活动对象中没有找到任何局部变量。
按照js引擎的执行顺序,局部找不到,就在全局window中寻找。控制循环的变量i执行完泄露到全局中,我们在全局中找到了i=3,于是就输出了“我是第3个按钮”!
怎么解决这个问题呢?看下面代码
<body>
<button>click me</button>
<button>click me</button>
<button>click me</button>
<script>
var btns=document.getElementsByTagName("button");
for(let i=0;i<btns.length;i++){
btns[i].onclick=function(){
alert(`我是第${
i}个按钮`)
}
}
</script>
为什么用let就可以了呢?让我们看看let的底层代码
<body>
<button>click me</button>
<button>click me</button>
<button>click me</button>
<script>
var btns=document.getElementsByTagName("button");
for(var i=0;i<btns.length;i++){
(function(i){
btns[i].onclick=function(){
alert(`我是第${
i}个按钮`)
}
})(i)
}
</script>
有什么不同呢?
- for循环内部使用了一个匿名函数自调
- 并且将当前的变量值i通过传参的方式传进去
当我们点击第一个按钮时,开始执行匿名函数,同时创建了匿名函数的活动对象,里面保存着匿名函数可用的局部变量。这时i作为匿名函数的参数,一定是保存在活动对象中的,此时,i=0。
接下来我们执行内部的onclick函数。因为onclick函数在匿名函数内部,所以它的作用域链分为三层,第一层是为自己预留的,第二层是外层匿名函数的,第三层全局window的。js引擎同样按照从局部到全局的顺序寻找i。果然在第二层外层匿名函数中找到了i=0,所以结果显示“我是第0个按钮”,第二个第三个按钮亦是如此。
以上也解释了为什么let和for一起使用能形成闭包的效果。
本文主要讲述了ES6的作用域,创作过程中查找了有关函数提升,以及作用域链的知识,后期也会具体的做一个总结。