JavaScript中的闭包-终结篇

闭包,一个似曾相识的家伙,总是在面试中被问到。今天,就让我们来好好的总结一下闭包吧。

一、什么是闭包

在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,就把这些变量的集合称为闭包。

这段话有点绕。我的理解是,闭包就是能够读取其它函数内部变量的函数,它是连接函数内部和函数外部的一座桥梁,它同时含有对函数对象以及作用域对象引用的对象。

闭包的实现也很简单,在一个函数中嵌套另一个函数或者将一个匿名函数作为值传入另一个函数中。

// 函数student1中嵌套了say,say作为参数返回,外部调用时仍能打印name,构成闭包
function student1() {
  const name = 10;
  function say() {
    console.log(name);
  }
  return say;
}

function student2() {
  const name = 20;
  // 定时器中的为一个匿名函数,其作为参数传入了,函数student2执行完毕之后,1s钟后才会执行定时器函数,但此时还能打印name,构成闭包
  setTimeout(function () {
    console.log(name);
  }, 1000);
}

二、JavaScript的作用域

在JavaScript中,变量的作用域有两种:全局变量和局部变量。

对于[全局变量],它的生存周期是永久的的,除非主动销毁变量;而对于[函数局部变量] ,当函数执行完毕,局部变量也就销毁了。

函数体是可以父作用域的变量的:

var name = "Tom";

function say() {
    console.log(name);
}

say(); // Tom

但是,我们在函数外部是无法直接获取函数内的局部变量:

function say() {
    var name = "Tom";
}

console.log(say()); // undefined

在JavaScript中,作用域链查找标识符的顺序是从当前作用域开始一级一级往上查找。因此,通过作用域链,JavaScript函数内部可以读取函数外部的变,但反过来,函数的外部通常则无法读取函数内部的变量。

三、如何从外部读取局部变量呢?

很多情况,我们是需要获取函数内部的局部变量的,但是直接获取又获取不到。我们试着在函数中再定义一个函数:

function Student() {
  let name = "Tom";

  function Say() {
    console.log(name);
  }
}

上面的代码,在函数Say中可以获取函数Student内部的所有的局部变量,反之却不行,函数Say内部的局部变量对函数Student是不可见的。

这就是JavaScript的链式作用域,当获取某个变量时,它会向上一层一层的找,所有父层级的变量对子层级都是可见的。

既然子层级的函数可以获取父层级的局部变量,那么我们通过返回子层级的函数不就可以获取父层级的局部变量的了么。

function Student() {
  let name = "Tom";

  function Say() {
    console.log(name);
  }

  return Say;
}

let result = Student();
result(); // Tom

3.1 为什么要函数套函数呢?

你可能会说,要获取name值,直接reutrn name不就可以了么?

function Student() {
    let name = "Tom";  
    return name;
  }
  
let result = Student();
console.log(result); // Tom

从单一目的来说,确实是可行的,但是不要忘记了闭包的另一个特性————隐藏变量。我们是需要创造一个局部变量,如果直接返回一个name,等同于创建一个全局变量的name,就达不到效果。

3.2 为什么要return

因为如果不return,你就无法使用这个闭包。把return Say 改成 window.Say = Say 也是一样的,只要让外面可以访问到这个 Say 函数就行了。

所以 return Say 只是为了 Say 能被使用,也跟闭包无关。

四、闭包的优缺点

闭包的特点有以下三点:

  • 1、函数嵌套函数
  • 2、函数内部可以引用外部的参数和变量
  • 3、参数和变量不会被垃圾回收机制回收

闭包的优点:

  • 可以重复使用变量,并且不会造成变量污染
  • 可以用来定义私有属性和私有方法

闭包的缺点:

  • 会产生不销毁的上下文,导致栈/堆内存消耗过大
  • 会造成内存泄露

4.1思考一个问题:

什么情况下闭包会被回收?

  • 1、如果闭包引入的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄露。
  • 2、如果引用闭包的函数是一个局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收时,判断闭包内容已经不再被使用,则js引擎的垃圾回收器就会进行回收。

五、闭包的用途

闭包可以用在许多地方。它的最大用处有两个:

  • 1、可以读取函数内部的变量
  • 2、可以变量的值始终保持在内存中,也可以创建私有变量。

读取函数内部的变量,换句话说就是隐藏一个变量。把局部变量隐藏,创建一个私有变量,对外暴露一个处理的方法。这样就能够避免局部变量随意的被修改。

如何理解闭包可以让变量保持在内存中呢?来看下面的代码:

function counter() {
  let initialValue = 0;
  
  // 这是一个全局函数
  add = function () {
    initialValue += 1;
  };

  function show() {
    console.log(initialValue);
  }

  return show;
}

let Counter = counter();

Counter(); // 0

add();

Counter(); // 1
Counter(); // 1

add();

Counter(); // 1

由此可知,initialValue是驻留在内存的,每次执行add函数都会对initialValue加1。counter运行结束后initialValue并没有被回收,那,这是为什么呢?

这是因为counter是show的父函数,而show函数被赋值给了全局变量,即Counter。这就导致了show函数始终在内存中,然而show依赖counter,因此counter也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

JavaScript通过标记清理引用计数这两种标记策略,JavaScript 最常用的垃圾回收策略是标记清理。

当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。

所以,show及其上下文都会存留在内存中。

五、闭包的注意事项

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。

六、经典的闭包问题

下面的代码输出什么?

for (var i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 1000);
}

上面的代码预期输出:1、2、3、4。但结果却是四个5。
原因是:多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。

解决方法1

变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找。

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

解决方法2

使用setTimeout包裹,通过第三个参数传入。(注:setTimeout后面可以有多个参数,从第三个参数开始其就作为回掉函数的附加参数)

for (var i = 1; i < 5; i++) {
    setTimeout(value => console.log(value), 1000, i);
}

解决方法3

使用 块级作用域,让变量成为自己上下文的属性,避免共享。

for (let i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 1000);
}

最后

欢迎大家关注我的公众号【前端技术驿站】~~~实战视频已准备好,回复[5678]获取

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • 一、介绍 作用域链就是根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。 想要...
    走着别浪阅读 387评论 2 6
  • 先上代码: 思考一下,这段代码执行结果是什么?执行顺序是怎么样的? 变量作用域 理解闭包之前,要先理解js的变量作...
    尘间小林阅读 280评论 0 0
  • 作用域 先来说下什么是作用域,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周...
    亖巠阅读 275评论 0 0
  • 什么是闭包? 闭包是指那些能够访问自由变量的函数。自由变量:指在函数中使用的,但既不是函数参数也不是函数的局部变量...
    BubbleM阅读 295评论 0 1
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,667评论 2 7