JavaScript 之理解闭包

什么是闭包?

闭包的概念:
《JavaScript》权威指南: 函数对象可以通过作用域链相互关联起来,函数体内部的变量可以保存在函数作用域内,这种特性称为“闭包”。

通俗的说:所谓闭包,就是一个函数,这个函数能够访问其他函数作用域中的变量。
或者说:闭包,有权访问另一个函数作用域中的变量的函数;一般情况就是在一个函数中包含另一个函数(被包含的函数就是闭包)。

函数的作用域是独立的、封闭的,外部的执行环境是访问不了的,但是闭包具有这个能力和权限。

那闭包是怎样的一个表现形式呢?
第一,闭包是一个函数,而且存在于另一个函数当中
第二,闭包是可以访问到父级函数的变量,且该变量不会销毁

function person () {
  var name = '小樱'
  function cat() {   // 这个是一个内部的函数,是一个闭包
    console.log(name)
  }
  return cat
}
var per = person() // per 的值就是return后的结果,即cat函数
per();  // 结果: 小樱,per()就相当于cat()
per();  // 小樱
per();  // 小樱

闭包的原理

闭包的实现原理,其实是利用了作用域链的特性,我们都知道作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条。
例如:

var age = 18
function cat () {
  age++
  console.log(age)  // cat函数内输出了age,该作用域没有,则向外层寻找,找到了,输出19
}
cat() // 19

这个时候如果继续调用,结果就会一直增加,也就是变量age的值一直在递增

cat() // 20
cat() // 21
cat() // 22

如果程序还有其他函数,也需要用到age的值,则会受到影响,而且全局变量还容易被人修改,比较不安全,这就是全局变量容易污染的原因,所以我们必须解决变量污染问题,那就是把变量封装到函数内部,让它成为局部变量。
如下:

// f2 加括号表示返回的是函数值,不加括号返回的是函数体

// 示例一
function f1() {
  var age = 18
  function f2() { // 是一个内部函数,是一个闭包
    age++
    console.log('年龄-->', age)
  }
  return f2();
}
f1() // 19
f1() // 19


// 示例二
function f1() {
  var age = 18
  function f2() {  // 是一个内部函数,是一个闭包
    age++
    console.log('年龄-->', age)
  }
  return f2;
}
var a = f1()
a()  // 19
a()  // 20

示例一:内部函数 f2() 在执行前,从外部函数返回。
示例二:f1( ) 执行后,将其返回值(也就是内部的 f2() 函数)赋值给变量a, 并调用a(),实际上只是通过不同的标示符引用调用了内部的函数f2。

f1() 函数执行后,正常情况下f1() 的整个内部作用域被销毁,占用的内存被回收。但是现在的f1() 的内部作用域f2() 还在使用,所以不会对其进行回收。f2() 依然持有对改作用域的引用,这个引用叫做闭包。这个函数在定义的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

常见的闭包

function foo(a) {
  setTimeout(function timer() {
    console.log(a)
  }, 1000)
}
foo(2)

foo执行1000ms后,它的内部作用域不会消失,timer 函数依然保有foo 作用域的引用。timer函数就是一个闭包。

定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers 或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。

循环和闭包

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

上面的这段代码,预期是每隔一秒,分别输出0,1,2,3,4,但实际上依次输出的是5,5,5,5,5。首先解释一下5从哪里来的,这个循环的终止条件是 i 不在 < 5,条件首次成立时 i 的值是5,因此,输出显示的是循环结束时 i 的最终值。

延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的都是setTimeout(..., 0) ,所有的回调函数依然是在循环结束后才执行。因此每次都输出一个5来。

我们预期的是每个迭代在运行时都会给自己“捕获”一个 i 的的副本。但实际上,根据作用域的原理,尽管循环中的五个函数都是在各自迭代中分别定义的,但是他们都封闭在一个共享的全局作用域中,因此实际上只有一个 i。即所有函数共享一个 i 的引用。

改成下面这样,就可以按照我们期望的方式进行工作了。这样修改之后,在每次迭代内使用 IIFE (立即执行函数)会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代内部都会含有一个具有正确值的变量可以访问。

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

当然,使用ES6 块级作用域的 let 替换 var 也可以达到我们的目的。

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

闭包的优缺点

优点:

  1. 隐藏变量,避免全局污染
  2. 可以读取函数内部的变量
    使用不当,优点就变成了缺点:
  3. 导致变量不会被垃圾回收机制回收,造成内存消耗
  4. 不恰当的使用闭包可能会造成内存泄漏的问题。

为什么使用闭包时变量不会被垃圾回收机制销毁呢?

JS垃圾回收机制:
JS规定在一个函数作用域内,程序执行完以后变量就会被销毁,这样可以节省内存;使用闭包时,按照作用域链的特点,闭包(函数)外面的变量不会被销毁,因为函数会一直被调用,所有一直存在,如果闭包使用过多会造成内存泄漏。

理解闭包

理解闭包首先要了解嵌套函数的词法作用域规则,如下代码

// 示例一
var str = 'Hello World'    // 全局变量
var a = function () {
  var str = 'Hello 你好'  // 局部变量
  function f() {
    return str
  }
  return f()
}
console.log('示例1 ---->', a())   // 打印结果: Hello 你好

a () 函数声明了一个局部变量,并定义了一个新的函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。

变量提升

变量提升即将变量声明提升到它所在作用域的最开始的地方。
只有 var 可以将变量进行提升,const 跟 let 不会
下面举个例子:
变量提升(全局作用域)

// 示例1
console.log(a)   // undefined
var a = 8

// 示例1 等价于
var a;
console.log(a)   // undefined
a = 8

// 示例2 
var a = 8;
function fn() {
  console.log('1--->', a);   // undefined
  var a = 9;
  console.log('2--->', a);   // 9
}
fn()

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

推荐阅读更多精彩内容

  • js之闭包 1、到底什么是闭包 闭包已经成为近乎神话的概念,它非常重要又难以掌握,而且还难以定义。 1.1 古老的...
    道无虚阅读 699评论 0 0
  • 一个简单的闭包 例1 理解闭包 闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的...
    howell5阅读 334评论 0 3
  • 什么是闭包? 闭包是指那些能够访问自由变量的函数。自由变量:指在函数中使用的,但既不是函数参数也不是函数的局部变量...
    BubbleM阅读 295评论 0 1
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zock阅读 1,075评论 2 6
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zouCode阅读 1,271评论 0 13