关于「闭包中使用变量的问题」部分,是有两个疑问的,希望有大佬指点【下面的 小问题1、小问题2】。
ECMAScript是JavaScript的标准,js是按照ES的标准进行的实现(从出现时间上是先有的js);ES的主流实现只有js,大多数情况下,基本上js就等同于ES。
ECMAS标准的第6版就是ES6,正式名称为 ES2015,2015 年 6 月正式发布(以后标准都是以年作为名称,比如ES2016、ES2017...,不过只有ES6版本更新最大【因此被称为“JavaScript 语言的下一代标准”】,后续基本都是新特性的改进和补充)
变量和块级作用域
ES6中对变量的声明提供了let
和const
两个新的关键字,解决了var
变量存在的很多问题,并提供了块级作用域的支持,这使得很多场景下变量的行为更加合理。
let、const变量声明
ES6提供了新的变量声明关键字:
let用于声明一个变量。const用于声明一个常量。
let a=10;
const g=9.8;
var的一些问题
1. 重复声明
es6之前的var关键字变量声明,可以重复。
var a=12;
var a=5;
加上变量提升的问题,这将导致变量的混乱,以及无法预知的问题和数据错误。
2. 无法控制修改(无法定义常量)
比如无法定义常量。只能通过约定(大写字母)的方式表示一个常量。但实际使用中,如果不小心将会修改变量(比如判断时写了一个等号=,语法没问题,但逻辑肯定不正确了)
var MY_CONST = 10;
if(MY_CONST=10){}
3. 没有块级作用域
ES5只有全局作用域和函数作用域,会带来很多不合理的情况或场景。
- 一种是:一个块内的内层变量覆盖外层变量;
var tmp = 10;
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
用于变量提升,内部的tmp
变量覆盖了外层的tmp
,输出undefined
。
- 另一种是:块内变量泄露为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
var k='my';
console.log(s[i]);
}
console.log(i); // 5
console.log(k); // my
for循环内用于控制循环的变量i
、内部的变量k
,在循环结束后仍然能够使用,泄露成了全局变量。
不同块之间的变量将会相互影响,重复声明也将导致变量被替换,发生错乱问题。
5. 变量提升问题
var
变量可以在声明之前使用,值为undefined
,这种现象被称为“变量提升”。
6. 闭包中使用变量的问题(实际是由于没有块级作用域导致的)
闭包是由函数和与其相关的参照环境组合而成的实体。在js中,最通常的表现是函数内部的子函数读取或引用父函数作用域内的变量,并返回了子函数。
闭包中使用变量的问题,严格来讲是没有块级作用域的变量,在延迟访问使用时,var
变量作为外层变量(全局变量或函数变量),值发生变化,导致当在函数内使用时不正确的问题。
小问题1:延迟访问,不知道在闭包之外的情况下会不会发生???
- 循环中函数使用变量的问题
循环中的函数使用变量,本质上也是一种“闭包”使用变量,不过由于使用的是全局变量,这些变量本来就由全局环境进行维护。【小问题2:由于函数内引用了变量,应该其也是维护了一份闭包环境。】
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
正常要实现的逻辑是,当执行第几个函数时,输出的是几。但是,可以看到,执行a[6]();
时输出的是10
。
由于i作为全局变量,每一次循环,变量i
的值都会发生改变,循环内的函数中console.log(i)
使用的i
就是全局的i
,即所有数组成员里面的i
都是同一个i
。循环结束时,i
的值变为10,。因此,执行任何一个函数输出都是10。
- 通过循环给dom添加事件方法时使用变量的问题
比如获取三个按钮,并循环为其添加click事件,弹出循环时的变量。
<button>A</button>
<button>B</button>
<button>C</button>
<script>
window.onload=function(){
let btns = document.getElementsByTagName('button');
for (var i = 0; i < btns.length; i++) {
btns[i].addEventListener('click',function(){
alert(i);
})
}
}
</script>
同样的,想要实现点击哪个按钮,输出对应的数字。实际上,每个按钮点击都会输出3
。
i作为函数变量,后面添加的所有的点击事件函数都引用的这一个i
,在循环结束时,已经变为了3,执行点击弹出时,所有的按钮都弹出最终的结果3
。
这是最典型的闭包问题的情景。循环先执行结束,后面执行函数时的变量问题。此外,还可以通过函数执行返回一个闭包函数后,修改其引用的变量后,再执行闭包函数,值也可能发生意料之外的改变。
- 解决办法
使用var时最简单的解决办法是,通过一个立即执行函数返回或者添加一个引用了父函数变量的闭包环境的函数。
for (var i = 0; i < btns.length; i++) {
(function(i){
btns[i].addEventListener('click', function () {
alert(i);
});
})(i);
}
// 或
for (var i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', (function(i){
return function() {
alert(i);
}
})(i));
}
其本质是通过立即执行函数的参数,值传递的方式被其内的闭包函数引用,维持了正确变量值。
let、const变量的改进
重复声明,将会报语法错误。
Uncaught SyntaxError: Identifier 'xxx' has already been declared
const常量禁止修改
常量重新赋值将会报类型错误。
Uncaught TypeError: Assignment to constant variable.
let/const 会在代码块内形成独立的作用域
let/const 仅在块级作用域内有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
这将带来更合理、更正确的处理场景。
let tmp = 10;
function f() {
console.log(tmp);
if (true) {
let tmp = 'hello world';
}
}
f(); // 10
function f1() {
if (true) {
let tmp = 'hello world';
}
console.log(tmp);
}
f1(); // 10
function f2() {
let tmp = 10;
if (true) {
let tmp = 'hello world';
}
console.log(tmp);
}
f2(); // 10
在函数内,if
代码块内的块级作用域变量不影响外部变量的使用。
ES6中允许块级作用域的任意嵌套;内层作用域可以定义外层作用域的同名变量(不推进,通常不应该在内层作用域声明同名的变量)
由于只在块级作用域内有效,块级作用域中的变量也不会泄露为全局变量,影响全局环境。
不存在变量提升
var命令会发生“变量提升”现象,变量可以在声明之前使用,这一点非常奇怪,不符合一般的逻辑。
let/const命令声明的变量一定要在声明后使用。否则报引用错误。
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
闭包中没有使用变量的问题
for
循环中使用let变量。
如下,因为有着独立的块级作用域,其内的函数引用变量,维护者独立i
值,执行函数时输出正确。
let a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
对于循环时添加事件,输出变量,也没有使用错误。
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function () {
alert(i);
});
}
暂时性死区
只要块级作用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域或者"应用"在块级作用域内,不再受外部的影响。
var tmp = 123;
if(true){
tmp = 'abc'; // ReferenceError: Cannot access 'tmp' before initialization
let tmp;
}
块级作用域内的let,影响了整个块级区域,即使有着全局变量,在let声明之前,也无法使用。
在代码块内,let命令声明变量之前,该变量都是不可用的。在语法上,let声明变量及之前的部分,称为“暂时性死区”(temporal dead zone
,简称 TDZ
)。
const的不可变
const实际上保证的是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值保存在变量指向的内存地址,因此等同于常量。
对于复合类型的数据(如对象、数组),由于是引用类型数据,变量指向的内存地址保存的只是一个指向实际数据的指针(内存地址),这个指针是固定不变的,但是指针指向的数据结构是否可以变化,则不能控制。
const foo = {};
console.log(foo); // {}
foo.prop = 123;
console.log(foo); // {prop: 123}
foo = {}; // 不能改变赋值