重温JavaScript(lesson4):作用域和闭包(2)

在lesson3中我们重温了JS作用域有关的内容,理解了JS作用域再来看闭包就非常easy了图片

1.闭包的概念

先来补充一个知识点(PS:如果你觉得不好理解,就看之后代码吧~),词法作用域:“函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现词法作用域,JS函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量可以保存在函数作用域内,这种特性被称为闭包”(《JavaScript权威指南》)

2.闭包的作用

闭包允许函数访问并操作函数外部的变量。只要变量或者函数存在于声明函数时的作用域内,闭包就可以使函数能够访问这些变量或函数。看一段非常简单的代码:

var  outerValue = 'New_Name';
function outerFunc() {
  console.log(outerValue);
  //New_Name
}
outerFunc();

我们在同一作用域内(本例是全局作用域)声明了变量outerValue和函数outerFunc,然后执行outerFunc。它可以“看见”和访问outerValue。你可能写过很多类似的代码片段,但你或许从来没有认识到你正在创建一个闭包。

我们再来看一个大家都熟悉的例子吧:

var outerValue = 'New_Name';
var laterFun;
function outerFun() {
  var innerValue = '重温新知';
  function innerFun(){
    console.log(outerValue);
    console.log(innerValue);
  }
  laterFun = innerFun;
}
outerFun();
laterFun();
//New_Name
//重温新知

上述代码中的语句:laterFun = innerFun; 的作用是将内部函数innerFun的引用存储变量laterFun上,因为laterFun在全局作用域内,所以可以对其进行调用。语句:laterFun();的作用是调用内部函数,我们不能在全局作用域直接调用innerFun,因为它是在outerFun的函数作用域内。

所以我们看到了闭包使得我们可以在内部函数的作用域消失之后还可以执行内部函数。这是因为当在外部函数中声明内部函数时,不但定义了内部函数的声明,而且还创建了一个闭包。此闭包不仅包含了内部函数的声明,而且还包含了在声明内部函数时该作用域中的所有变量。当最终执行内部函数时,尽管声明的作用域已经没了,但是通过闭包,仍然能够访问到原始作用域。这是不是很像内部函数将变量“包裹”起来呢?所以谓之闭包。

再来看一个其他形式的闭包的例子:

let outerFun;//未定义的全局函数
{
  let blockValue = 'New_Name';//块作用域变量
  outerFun = function () {
    console.log(blockValue)//New_Name
  }
}
outerFun();

outerFun在块内被赋值,该块(以及它的父级作用域,即全局作用域)构成了一个闭包。无论到哪里调用outerFun,它都有权限访问闭包内的变量。我们要注意,尽管调用outerFun时,程序已经退出了blockValue的作用域,但是仍有有权限访问它。一般情况下,某个作用域退出后,该作用域内的变量就会消亡。而对于此例中的闭包,JS会检测到函数outerFun被定义在指定块作用域内,并且这个函数outerFun可以在该块作用域外被引用,所以outerFun被允许持有该块作用域的访问权限。也就是闭包内的函数outerFun影响了闭包的声明周期。

3.闭包与循环

many people 在讲闭包的时候都和循环一起讲,我们也来看看吧~

for(var i=1; i<=5;i++) {
  console.log(i);
}
//1 2 3 4 5

这段代码完全没问题,一次输出1-5。但是加点料可就完全不一样了啊:

for(var i=1; i<=5;i++) {
  setTimeout(()=>{
    console.log(i);
  },i*100) 
}
//6 6 6 6 6

我们本来想在控制台中看到这样的情况:以100ms的间隔分别打印出1~5。但实际却是这样的情况:以100ms为间隔分别打印出 6,6,6,6,6。为啥是这样的,6是从哪里跑出来的?循环的种植条件是i不满足小于等于5。首次成立时i的值为6,因此输出显示的是循环结束时i的最终值。

其实仔细看看,这也是符合预期的。setTimeout的回调函数是在循环结束后才执行的,因此每一次都会输出一个6。那是什么原因导致这段代码的行为和语义所暗示的不一样呢?

原因是我们想在循环的每次迭代中获取一个i大的副本。但是根据作用域的工作原理,尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都在一个共享的全局作用中(或者说它们封闭在一个作用域中),因此实际上只有一个i。如何解决呢?就是引进更多的闭包作用域。(节选自《你不知道的JavaScript(上卷)》)IIFE会通过声明并立即执行一个函数来创建作用域。先看一下下面的代码:

 for(var i=1;i<=5;i++) {
  (function() {
    setTimeout(()=>{
      console.log(i);
    },i*100)
  })();
}
// 6 6 6 6 6 

这段代码运行的结果还是每隔100ms输出一个6,结果也是不符合预期的。这是因为虽然每个setTimeout函数都会讲IIFE在每次迭代中创建的作用域封闭起来。但是作用域中是空的,没有实质内容,所引用的i还是那个公共的i。那么我们给它加点料:

for(var i=1;i<=5;i++) {
  (function() {
    var j = i;
    setTimeout(()=>{
      console.log(j);
    },j*100)
  })();
}
// 1 2 3 4 5

在IIFE当中,我们定义了局部变量j来存储i的值,这样使得每一setTimeout函数都有属于自己的j,并将其封闭保持对变量j的访问权限。下面的代码使用参数的方式对其进行改进:

for(var i=1;i<=5;i++) {
  (function(j) {
    setTimeout(()=>{
      console.log(j);
    },j*100)
  })(i);
}
// 1 2 3 4 5

原理同上,在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代都会含有一个具有正确值的变量供我们访问。

说了这么多,闭包在实际开发中有啥使用场景啊?

4.闭包的使用场景

我们说说闭包的两个常见使用场景:(1)封装私有变量(2)回调函数 。先看一下封装私有变量的例子:

function People() {
  var age = 0;
  this.getAge = function() {
    return age;
  }
  this.grow = function() {
    age ++;
  }
}
var someone = new People();
someone.grow();
console.log(someone.age)
//undefined 说明我们无法直接获取该变量值
console.log(someone.getAge());
//1 通过getAge方法可以获取
var anotherone = new People();
console.log(anotherone.getAge());
//0 anotherone拥有自己的age属性

我们创建了一个People构造器,在构造器内部定义了age。由于JS作用域规则的限制,我们只能在构造器内部访问该变量。为了让作用域外的代码能够访问age,我们定义了访问该变量的getAge方法。

回调函数是另外一种常见的使用闭包的情景,在回调函数中我们可能要频繁地访问外部数据,所以我们可以创建具有外部数据访问权限的闭包。关于回调函数和我们上面介绍的setTimeout的本质原理差不多,这里就不作过多的介绍了。

以上就是我们要了解的有关闭包的一些主要内容,你会了吗?反正我是会了~~ 图片,咱们一起做点测试题吧

5.测试题

猜猜如下代码的运行结果:

var outerValue = 'outer';
var laterFun;
function outerFun() {
  var innerValue='inner';
  function innerFun(param){
    console.log(outerValue);
    console.log(innerValue);
    console.log(param);
    console.log(amazing);
  }
  laterFun = innerFun;
}
var amazing = 'New_Name';
outerFun();
laterFun("so fun");

结果是:控制台依次输出outer,inner, so fun,New_Name 。这里值得一提的是(1)我们向innerFun添加了一个参数,这个参数也在闭包内。(2)另外是变量amazing虽然出现在函数的声明之后,但是也包含在闭包内。总之就是外围作用域中的所有变量都包含在闭包中。

好的,今天我们的重温JS就到这里了,我们下次将一起重温有关函数的知识~

如有错误,请不吝指正。温故而知新,欢迎和我一起重温旧知识,攀登新台阶~

图片

参考资料:

[1]《你不知道的JavaScript(上卷)》

[2] 《JavaScript忍者秘籍 第二版》

[3] 《JavaScript权威指南 第6版》

[4]《JavaScript编程精粹》

[5]《JavaScript学习指南 第3版》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

重温新知

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值