let的作用是声明变量,和var差不多。
let是ES6提出的,在了解let之前,最好先熟悉var的原理。
JavaScript有一个机制叫“预解析”,也可以叫“提升(Hoisting)机制”。很多刚接触JavaScript的人都会被这个机制弄混。比如:
// var 的情况 console.log(a); // 输出undefined var a = 2;
在预编译阶段,JavaScript引擎会将上面的a函数修改成下面的写法:
var a; //声明且初始化为undefined console.log(a); a=2;
我们把上面的 var 变成 let ;
// let 的情况 console.log(a); // 报错ReferenceError let a = 2; //相当于在第一行先声明bar但没有初始化,直到赋值时才初始化
由此我们得出:
变量提升现象:浏览器在运行代码之前会进行预解析,首先解析函数声明,定义变量,解析完之后再对函数、变量进行运行、赋值等。
-不论var声明的变量处于当前作用域的第几行,都会提升到作用域的头部。
-var 声明的变量会被提升到作用域的顶部并初始化为undefined,而let声明的变量在作用域的顶部未被初始化
在ES6中对块级作用域做了进一步强化,从而使变量在生命周期内能被更好的控制。
块级声明用于声明在指定块的作用域之外无法访问的变量。
“块级作用域”也可以称为“词法作用域”。
- 块级作用域存在于
- 函数内部
块中(字符 { 和 } 之间的区域)
比如 if 和 for 在ES6中也被定义成一个块级。
let声明的用法与var相同,用let代替var来声明变量,就可以把变量的作用域限制在当前代码块中。
而且let声明不会被提升(在预解析的过程中,不会把声明变量放在所有代码的之前),因此开发者通常会将let声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。
var a = 123; if (true) { a = 456; // ReferenceError let a; } console.log(a); //输出值为123,全局 a 与局部 a 不影响
上面这小段代码,先声明全局变量 a = 123; 按照我们以往的思维,如果if 判断语句中没有 let a;则最后会输出 456;但是if 判断语句作为块作用域,内部在未声明变量的时候直接给 a 赋值为 456;因此会报错。
由此可见,用let来声明变量比var更严紧。
let的另一个特性是禁止在同一个作用域下重复声明。
var a = 10; let a = 20; // 抛出语法错误 // Uncaught SyntaxError: Identifier 'a' has already been declared // 很直接的告诉开发者变量a已经被定义过了。
不管之前用var还是let声明,只要后面再重复声明同一个变量,都会报错。
let a = 10; var a = 20; // 也会报错
上面是的报错是因为在同一个作用域下,用let重复声明。
熟悉JavaScript的开发者都知道,var是可以重复声明的,而后面声明的操作会覆盖前面的声明。
如果不在同一个作用域下,是可以用let来重复声明相同名的变量。
let a = 30; if (true) { let a = 40; console.log(a); // 输出40 } console.log(a); // 输出30
同时 let 还有一个功能是防止变量泄露,
用var声明
for (var i=0; i<10; i++) {} console.log(i); // 输出 10
用let声明
for (let i=0; i<10; i++) {} console.log(i); // 抛出错误:Uncaught ReferenceError: i is not defined
最后我们总结出:let
- 在同一个作用域下,不可以被重复声明
- 可以重新赋值
- 可以防止变量泄露
接下来我们看一个 var 和 let 的实战练习:
我们先看一个正常的for循环,普通函数里面有一个for循环,for循环结束后最终返回结果数组
function foo(){ var arr = []; for(var i=0;i<5;i++){ arr[i] = i; } return arr; } console.log(foo()) //输出结果为 [0,1,2,3,4]
有时我们需要在for循环里面添加一个匿名函数来实现更多功能,看下面代码
//循环里面包含闭包函数
function foo(){
var arr = [];
for(var i=0;i<5;i++){
arr[i] = function(){
return i;
}
}
return arr;
}
console.log(foo()); //执行5次匿名函数本身 --> [ [Function], [Function], [Function], [Function], [Function] ]
console.log(foo()[1]); //执行第2个匿名函数本身 --> [Function]
console.log(foo().length); //最终返回的是一个数组,数组的长度为5 --> 5
console.log(foo()[0]()); //数组中的第一个数返回的是5 --> 5
上面这段代码就形成了一个闭包:
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见的方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。
在for循环里面的匿名函数执行 return i 语句的时候,由于匿名函数里面没有i这个变量,所以这个i他要从父级函数中寻找i,而父级函数中的i在for循环中,当找到这个i的时候,是for循环完毕的i,也就是5,所以这个 foo 得到的是一个数组[5,5,5,5,5]。
那我们怎么才能输出 [1,2,3,4,5],在上述中我们说了在闭包内部函数调用 i 时没有找到转而向上层父级去找,那我们在调用内部函数时将值传给内部函数不就可以不需要去父级找了,在 JavaScript 函数中有有匿名函数的自我执行,即 在函数体外面加一对圆括号,形成一个表达式,在圆括号后面再加一个圆括号,里面可传入参数。
(function(){ console.log(123); })();
//输出 123
var a = 123;
(function(b){
console.log(b);
})(a);
//输出 123
根据上面的执行结果我们可以将目标函数改造如下:
function foo(){ var arr = []; for(var i=0;i<5;i++){ arr[i] = (function(num){ return num; })(i); } return arr; } console.log(foo()); // [ 0, 1, 2, 3, 4 ] console.log(foo()[1]); // 1 console.log(foo().length); // 5 console.log(foo()[0]); // 0
这样在每次调用 foo 的时候内部的匿名函数都会自我执行,并且将 i 传入匿名函数进行返回。
我们现在来看下面的代码:
function foo(){ var arr = []; for(var i=0;i<5;i++){ arr[i] = function(){ return i; } } return arr; } console.log(foo()[0]()); // 5
function foo(){
var arr = [];
for(let i=0;i<5;i++){
arr[i] = function(){
return i;
}
}
return arr;
}
console.log(foo()[0]()); // 0
在上面的代码中,我们分别用 var 和 let 来声明 i 的值,获得的结果却是不一样的,当为 var 时我们在上面的时候已经解释过了,但是为什么当 var 换为 let 之后会变呢,只是由于使用 let 声明块级变量,这样每次循环时就会在自己的作用域内找 i 的变量,而不是去全局找 i 的变量。
我们再来看一道面试时会经常问到的题目:
for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); },1000); } // 5,5,5,5,5 for(let i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); },1000); } // 0,1,2,3,4
在setTimeout的时候,匿名函数function(){console.log(i);}会被声明创建,当匿名函数执行的时候,会查找当前运行环境的 i 的值。
var声明的 i ,运行环境的 i 的值为5,但是let声明的 i,运行环境中 i 的值是每一个循环创建匿名函数时候的 i。
所以得到了0-4的值。
let替换var,可以很好的解决闭包的问题。