你真的了解JavaScript的作用域与闭包吗
一、作用域
1.作用域总体来说就是根据名称查找变量的一套规则。JS查找变量的方式有两种:LHS和RHS。
LHS(left hand side)大致可以理解为给某个变量赋值,在赋值符(=)的左边;RHS(right hand side)是指找到某个变量的源值,如在赋值符(=)的右边。
举个例子说明,对于 a = 10; a = b; 这两个语句。
var a 这里就是LHS查询,查找当前作用域是否有a这个变量来给a进行赋值,如果有则进行赋值;如果没有,在非严格模式下JS引擎会自动创建一个变量a来进行赋值;在严格模式下JS引擎会抛出ReferenceError错误。
a = b;这个语句,对于b这个变量是进行RHS查询,查找b这个变量的值。如果成功查询,则进行赋值操作;如果查询失败,则抛出ReferenceError错误。如果查询成功,但是对这个变量的操作有问题,就会抛出TypeError错误。如b只是一个普通的变量,却当成函数使用(b())。
2.JS是基于词法作用域的。指在你写代码时将变量和函数写在哪里决定的,而不是调用顺序决定的。
举个例子
var num = 1; function a() { console.log(num); // 1 } function b() { var num = 2; a(); } b();
当按照顺序执行b()时,在b函数作用域中定义了一个值为2的num变量,然后在b函数中调用a函数,来到a函数的作用域进行调用(而不是在b函数作用域),找到全局变量num = 1。
3.不同作用域间是互相不会影响的,重复定义变量是不会发生错误的。
举个例子
var i = 1; var j = 2; function a(){ var i = 3; var j = 4; console.log(i,j); // 3,4 } function b(){ var i = 5; var j = 6; console.log(i,j); // 5,6 } console.log(i,j);// 1,2 a(); b();
4.for循环中,var声明的变量在全局作用域能够访问,并且值是退出循环的那个值;let声明的变量会产生块作用域,它将值重新绑定到循环迭代中。
for(var i = 0;i<5;i++){ console.log(i); // 0 1 2 3 4 } console.log(i); // 5
for(let i = 0;i<5;i++){ console.log(i); // 0 1 2 3 4 } console.log(i); // ReferenceError
5.每个作用域都会发生提升操作;函数声明会发生提升,但是函数表达式不会被提升;
b() // TypeErrorn() // ReferenceError,因为不会变量提升// 函数声明function a(){ console.log(num); // undefined var num = 1;}// 函数表达式var b = function n(){ console.log('b');}
6.函数声明首先被提升,变量声明在后;
函数首先被提升可以认为是函数的优先级高于变量,重复声明的变量会被函数替代。但是其他函数声明可以覆盖之前的函数声明。
b() // TypeError n() // ReferenceError,因为不会变量提升 // 函数声明 function a(){ console.log(num); // undefined var num = 1; } // 函数表达式 var b = function n(){ console.log('b'); }
二、闭包
1.闭包是基于词法作用域书写代码时的必然结果。
不论函数在哪里执行,函数都会在代码书写的位置(即函数本身在哪)来进行作用域链的查找。当函数进行作用域查找时,就形成了闭包。
也可以说是当函数被当作值进行传递的时候(如参数传递、返回值等),就会形成闭包。
举个例子
function foo() { var a = 2; function bar() { console.log(a); } return bar; } var a = 3; var baz = foo(); baz(); // 2
当foo函数执行时,会返回bar函数,当bar函数在全局作用域执行时,会沿着bar函数定义的地方开始进行RHS查找a变量的值。这时就产生了闭包。闭包的产生准确来说应该是函数在函数本身所处作用域之外执行,对于IIFE(立即执行函数)来说虽然创建了闭包,但是却没有真正使用闭包,因为立即执行函数的执行作用域是函数定义的作用域,虽然是闭包,但是没有使用闭包的功能。
闭包的功能:函数在所处作用域之外执行时,会沿着函数定义时的作用域链进行查找。
var a = 1; // 立即执行函数 ( function(){ console.log(a); } )();
2.循环与闭包
观察下面的例子
for(var i = 0;i<3;i++){ setTimeout(()=>{ console.log(i); },i*500) }
输出结果为 3 3 3,并不是想象中的 0 1 2。因为在for循环结束时,setTimeout开始执行,在执行的时候进行变量 i 的RHS查询,但是变量 i 只能在全局作用域中查到,这是变量 i 的值是退出循环的值 i = 3;
可能与你的想象结果有些许不同,如果要输出 0 1 2 该怎么做?
(1).使用IIFE形成闭包是否可行?
for (var i = 0; i < 3; i++) { (function () { setTimeout(() => { console.log(i); }, i*500) })() }
结果依然是 3 3 3。因为虽然使用IIFE形成了闭包,但是在闭包中并没有 i 的值,还会沿着作用域链查找到全局 i = 3;
(2).在IIFE使用一个变量临时保存 i 值或传参
for (var i = 0; i < 3; i++) { (function () { var j = i; setTimeout(() => { console.log(j); }, i*500) })() } // 或 for (var i = 0; i < 3; i++) { (function (i) { setTimeout(() => { console.log(i); }, i*500) })(i) }
结果为 0 1 2,第一个for循环闭包中有变量 j ,第二个for循环有形参 i ,都不会到达全局作用域。
(3).使用 let 生成块作用域
for(let i = 0;i<3;i++){ setTimeout(()=>{ console.log(i); },i*500) }
结果为 0 1 2,let声明的变量会包含在块作用域中,并且作为循环变量时,每一次的迭代都会声明一次变量并将变量结果保存,当访问变量时,由于闭包的作用,会得到每次迭代的值。