讲解「闭包」

# 定义

闭包 是指有权访问另一个函数作用域中的变量的函数。注意别混淆匿名函数和闭包的概念。
创建闭包 需要达到两个条件,如果不满足第二条,也只能称作是匿名函数
(1)在一个函数内部创建另一个函数
(2)内部函数访问外部函数的变量

function createCompareFn(attr) {
  return function(obj1, obj2) {
    return obj1[attr] >= obj2[attr]
  }
}

  上例中,内部函数(一个匿名函数) 访问了包含函数(即外部函数)的变量attr,即使这个内部函数被返回且在其他地方被调用了,它仍然可以访问变量attr。之所以能够访问,是因为内部函数的作用域链中含有包含函数的作用域。

# 作用域链

  当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,包含函数的活动对象处于第二位,包含函数的包含函数的活动对象处于第三位……,直到找到全局环境为止。这些活动对象使用链表来连接,形成作用域链
  在函数执行过程中,为了读取和写入变量的值,需要在作用域链中查找变量。看一个简单的函数声明及调用来解释:

function compare(val1, val2) {
  return val1 >= val2
}
var result = compare(5, 10)

  上述代码定义了compare函数,然后又在全局作用域中调用。当调用compare()时,会创建一个包含argument, val1, val2的活动对象。而全局环境的变量对象处在作用域链的第二位

A

  后台的每个执行环境都有一个标识变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()这样的局部环境的变量对象,只有在函数执行过程中菜户存在。在创建compare()函数时,会创建一个包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中,当执行compare()函数时,回味函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行的作用域链,以此类推形成作用域链。如图所示,作用域链本身只是一个指向变量对象的指针列表,它只引用但不实际包含对象
  无论什么时候在函数中访问一个白能量时,就会从作用域链中搜索具有相应名字的变量。通常当函数执行完成后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)

# 闭包的作用域链

  由于闭包存在函数内部定义函数,内部定义的函数将包含函数的活动对象到它的作用域链中。假设有闭包如下:

function createCompareFn(attr) {
  return function(obj1, obj2) {
    return obj1[attr] >= obj2[attr]
  }
}
var compare = createCompareGn('name')
var result = compare({ name: 'Nic' }, { name: 'Goe' })

  则它的作用域链关系为


B

由于闭包会携带它包含函数的作用域链,因此会比其他函数占用更多内存,过度使用闭包容易导致占用内存过多,需谨慎。

# 闭包与变量

  由于作用域链本身只是一个指向变量对象的指针列表,它只引用并不真正存储它们。而这种配置机制引出了一个副作用,即闭包只能取到包含函数中任何变量的最终值
  因为闭包作用域链与包含函数的活动对象之间只是引用关系,当包含函数中由于某些运算导致它活动对象中的属性发生更新时,该更新会被带到闭包作用域中,当闭包再访问变量时,取到的就是被更新后的变量的值。经典例子如下:

function createFunctions() {
  var result = new Array()
  for (var i = 0; i < 10; i++) {
    result[i] = function() {
      return i
    }
  }
  return result // 10 个 10
}

  该例中,在闭包中返回包含函数的变量i并压入给结果数组。表象上看应该得到1~10的数组。打印发现是10个10。原因是:每个函数的作用域链中,都保存着createFunctions()函数的活动对象,引用的都是同一个变量i;当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用者保存变量i的同一个变量对象。所以在每个函数内部i的值都是10。作用域引用关系如下:

C

  我们可以通过创建另一个匿名函数强制让闭包的行为符合预期

function createFunction() {
  var result = new Array()
  for (var i = 0; i < 10; i++) {
    result[i] = function(num) {
      return function() {
        return num
      }
    }(i)
  }
return result // 1~10
}

  经过如上改造,我们没有闭包直接赋值给数组,取而代之的是定义了一个匿名函数,并将立即执行该函数的结果赋给数组。匿名函数有个参数num,存在于匿名函数的作用域链中。程序执行for循环调用每个匿名函数时,由于函数参数是按值传递的,在其内部我们创建了一个直接访问num的闭包。这样一来,result数组中存储的值就是每次执行的num的一个副本了。

# 闭包的this变量

  我们知道,this对象是在运行时基于函数的执行环境绑定的。在全局函数中,this等于window。而当函数被某个对象的方法调用时,this等于那个对象。
  由于匿名函数的执行环境具有全局性,因此其this对象通常指向window(除使用call()apply()来改变函数执行环境外)。看以下例子:

var name = 'the window'
var object = {
  name: 'my object',
  getNameFn: function() {
    return function() {
      return this.name
    }
  }
}
console.log(object.getNameFn()())  // the window

  本例中,getNameFn是对象中的一个方法属性。object.getNameFn()返回一个函数,object.getNameFn()()立即执行该函数得到一个字符串。
  我们知道,每个函数在被调用时都会自动取得两个特殊变量:thisarguments。内部函数在搜索这两个变量时,只会搜索到它自己的活动对象为止(它们自身能获取到不必去包含函数活动对象中获取)。在本段代码执行时,程序发现需要返回逻辑想要返回的this.name,于是搜索匿名函数自身的作用域,取到自己的this对象,该对象因匿名函数执行环境全局性的特征指向了window,从而输出了"the window"

  注意本例的称呼是匿名函数而不是闭包!原因就是匿名函数内部有自身的this变量,它无需也无法获取到外部objectthis,没有达到内部函数访问外部函数的这么一个行为,因此不称呼为闭包。以下改造后就符合了闭包的特征

  如果我们想获取到object中的name,只需如下简单改造即可

var name = 'the window'
var object = {
  name: 'my object',
  getNameFn: function() {
    var that = this
    return function() {
      return that.name
    }
  }
}
console.log(object.getNameFn()()) // myobject

  经过如上改造后,但执行object.getNameFn()()调用内部闭包函数时,需要搜索that,而在自身作用域内并没有找到that,于是顺着作用域链查找包含函数的作用域,得到结果。

# 闭包与内存泄漏

  由于闭包作用域链包含着包含函数的作用域,因此会比普通函数占用更多的内存,当使用闭包不当,且未得到合适的释放情况下,就容易造成大量内存空间的占用。看一个例子

function assignHandler() {
  var element = document.getElementById('someElement')
  element.onclick = function() {
    alert(element.id)
  }
}

  以上代码实现了对某个元素进行点击时的点击响应事件。onclick是一个闭包,在这个闭包内循环引用了element.id。因此,只要匿名函数存在,element的引用数至少是1。那么,在垃圾回收机制规则中就无法判定element是一个需要被回收的元素。导致其一直占用在内存空间中。解决办法如下

function assignHandler() {
  var element = document.getElementById('someElement')
  var id = element.id
  element.onclick = function() {
    alert(id)
  }
element = null
}

  如此改造有两点:(1)对element.id保存副本目的是在闭包中取消对元素变量的循环引用。(2)由于闭包会引用包含函数的整个活动对象,其中还包含着element,因此包含函数的活动对象中也会保存有一个引用。因此有必要把element变量设置为null

# 闭包与应用场景

(1)模仿块级作用域
  在闭包中创建的变量,不受外部变量的影响。看以下函数,在for循环这个块级空间中定义了变量,但在外部依然能访问alert(i)依然能访问到该变量。

function Counter(count) {
  for (var i = 0; i < count; i ++) {
    console.log(i)
  }
  var i // 重新声明变量
  alert(i) // 计数
}

  如何让该变量私有化?当然可以使用ES6的let定义。本例笔者不想说这个,我们来看以下这个更熟悉的函数格式

(function() {
  // 块级作用域
})()

  我们都称它为自执行函数,初学者可能觉得这个格式很难理解,我们做如下拆解来帮助理解它。
一个正常函数表达式定义及调用如下

var func = function() {
  // 块级作用域
}
func()

  普通的函数调用使用函数名加圆括号来执行一个函数,如果改成函数对象,即如下

function() {
  // 块级作用域
}()  // 报错。。

  执行报错了。因为JS将 function 关键字当做一个函数声明的开始,而函数声明后面是不允许跟圆括号,而函数表达式后面可以跟圆括号,因此需要把匿名函数加一个圆括号来告诉JS这是一个函数表达式,从而形成了自执行函数的表现形式,也实现了块级作用域的目的。

  利用自执行函数的特征,我们可以改写outputNumber函数如下

function outputNumber() {
  (function() {
    for(var i = 0; i < number; i++) {
      console.log(i)
    }
  })()
  console.log(i)  // 报错
}

  以上代码中,匿名函数是一个闭包,他能够访问包含函数作用域链中的所有变量,而外部无法访问比包内的变量。

(2)创建私有变量
  创建一个可以访问私有变量和私有函数的共有方法。该方法也称作特权方法。
  创建私有变量需要从一个计数器来说起:

function Counter() {
  var count = 0
  this.clear = function () {
    this.count = 0
  }
  this.add = function() {
    this.count++
  }
  this.decrease = function () {
    this.count--
  }
}

  这个构造函数中,有一个私有变量count,它只能在函数内部被访问,函数中有三个特权方法用于清空,累加和累减计数。除了它们外,灭有别的方法可以访问到count变量。而特权方法可以被实例化的实例访问。count就是一个私有变量

【缺点】每次实例化的时候,都需要重新生成一次特权方法

以上方法为构造函数法创建私有变量,除此之外,还有通过私有作用域定义静态私有变量,为单例创建私有变脸个特权方法的模块模式等,这里不展开说明。

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