资讯专栏INFORMATION COLUMN

作用域链、垃圾回收机制、闭包及其应用(oop)

EastWoodYang / 1097人阅读

摘要:执行环境变量对象活动对象作用域链执行环境,为简单起见,有时也称为环境是中最为重要的一个概念。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。闭包垃圾回收机制先介绍下垃圾回收机制。

执行环境、变量对象 / 活动对象、作用域链
执行环境(executioncontext,为简单起见,有时也称为“环境”)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variableobject),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scopechain)。

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activationobject)作为变量对象。

活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

---- 摘自 JavaScript高级程序设计

注意: 除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

---- 摘自 https://www.cnblogs.com/wangf...

理论说完,直接上代码。

function Fn() {
  var count = 0
  function innerFn() {
    count ++
    console.log("inner", count)
  }
  return innerFn
}

var fn = Fn()
document.querySelector("#btn").addEventListener("click", ()=> {
  fn()
  Fn()()
})

1、 浏览器打开,进入全局执行环境,也就是window对象,对应的变量对象就是全局变量对象。

在全局变量对象里定义了两个变量:Fn和fn。

2、当代码执行到fn的赋值时,执行流进入Fn函数,Fn的执行环境被创建并推入环境栈,与之对应的变量对象也被创建,当Fn的代码在执行环境中执行时,会创建变量对象的一个作用域链,这个作用域链首先可以访问本地的变量对象(当前执行的代码所在环境的变量对象),往上可以访问来自包含环境的变量对象,如此一层层往上直到全局环境。

Fn的变量对象里有两个变量:count和innerFn,其实还有arguments和this,这里先忽略。然后函数返回了innerFn函数出去赋给了fn。

3、手动执行点击事件。

首先,执行流进入了fn函数,实际上是进入了innerFn函数,innerFn的执行环境被创建并推入环境栈,执行innerFn代码,通过作用域链对Fn的活动对象中的count进行了+1,并且打印。执行完毕,环境出栈。

然后,执行流进入了Fn函数,Fn的执行跟第2步的一样,返回了innerFn。接着执行了innerFn函数,innerFn的执行跟前面的一样。

每一次点击都执行了fn, Fn, innerFn,而fn和innerFn其实是一样逻辑的函数,但控制台打印出来的结果却有所不同。


点击了3次的结果,接下来进入闭包环节。

闭包 垃圾回收机制

先介绍下垃圾回收机制。

离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。

“标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。

---- 摘自 JavaScript高级程序设计

通俗点说就是:
1、函数执行完了,其执行环境会出栈,其变量对象自然就离开了作用域,面临着被销毁的命运。但是如果其中的某个变量被其他作用域引用着,那么这个变量将继续保持在内存当中。
2、全局变量对象在浏览器关闭时才会被销毁。

接下来看看上面的代码。
对了先画张图。

现在就解释下为什么会有不同的结果。

Fn()() --- 执行Fn函数,return了innerFn函数并立即执行了innerFn函数,因为innerFn函数引用了Fn变量对象中的count变量,所以即使Fn函数执行完了,count变量还是保留在内存中。等innerFn执行完了,引用也随之消失,此时count变量被回收。所以每次运行Fn()(),count变量的值都是1。

fn() --- 从fn的赋值开始说起,Fn函数执行后return了innerFn函数赋值给了fn。从这个时候开始Fn的变量对象中的count变量就被innerFn引用着,而innerFn被fn引用着,被引用的都存在于内存中。然后执行了fn函数,实际上执行了存在于内存中的innerFn函数,存在于内存中的count++。执行完成后,innerFn还是被fn引用着,由于fn是全局变量除了浏览器关闭外不会被销毁,以至于这个innerFn函数没有被销毁,再延申就是innerFn引用的count变量也不会被销毁。所以每次运行fn函数实际上执行的还是那个存在于内存中的innerFn函数,自然引用的也是那个存在于内存中的count变量。不像Fn()(),每次的执行实际上都开辟了一个新的内存空间,执行的也是新的Fn函数和innerFn函数。

闭包的用途

1、通过作用域访问外层函数的私有变量/方法,并且使这些私有变量/方法保留再内存中

在这里补充一道闭包的面试题,当然还涉及到了递归。编写一个add函数,使得add(1)(2)(3)(4)...()返回1+2+3+4+...的值。

function add(num) {
  var count = num
  function addTemp(otherNum) {
    if (!otherNum) return count
    count += otherNum
    return addTemp  
  }
  return addTemp 
}

2、避免全局变量的污染
3、代码模块化 / 面向对象编程oop

举个例子

function Animal() {
  var hobbies = []
  return {
    addHobby: name => {hobbies.push(name)},
    showHobbies: () => {console.log(hobbies)}
  }
}
var dog = Animal()
dog.addHobby("eat")
dog.addHobby("sleep")
dog.showHobbies()

定义了一个Animal的方法,里面有一个私有变量hobbies,这个私有变量外部无法访问。全局定义了dog的变量,并且把Animal执行后的对象赋值给了dog(其实dog就是Animal的实例化对象),通过dog对象里的方法就可以访问Animal中的私有属性hobbies。这么做可以保证私有属性只能被其实例化对象访问,并且一直保留在内存中。当然还可以实例化多个对象,每个实例对象所引用的私有属性也互不相干。

当然还可以写成构造函数(类)的方式

function Animal() {
  var hobbies = []
  this.addHobby = name => {hobbies.push(name)},
  this.showHobbies = () => {console.log(hobbies)}
}
var dog = new Animal()

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102682.html

相关文章

  • js闭包的理解

    摘要:一般来讲,函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域,但是闭包的情况有所不同理解闭包的前提先理解另外两个内容作用域链垃圾回收作用域链当代码在执行过程中,会创建变量对象的一个作用域链。 闭包是javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包来实现。个人的理解是:函数中嵌套函数。 闭包的定义及其优缺点 闭包是指有权访问另一个函数作用域中的变量的...

    EasonTyler 评论0 收藏0
  • 温故js系列(14)-闭包&垃圾回收&内存泄露&闭包应用&作用域链&

    摘要:该对象包含了函数的所有局部变量命名参数参数集合以及,然后此对象会被推入作用域链的前端。如果整个作用域链上都无法找到,则返回。此时的作用域链包含了两个对象的活动对象和对象。 前端学习:教程&开发模块化/规范化/工程化/优化&工具/调试&值得关注的博客/Git&面试-前端资源汇总 欢迎提issues斧正:闭包 JavaScript-闭包 闭包(closure)是一个让人又爱又恨的somet...

    Amio 评论0 收藏0
  • Javascript难点知识运用---递归,闭包,柯里化等

    摘要:作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。对语句来说,会将指定的对象添加到作用域链中。 前言 ps: 2018/05/13 经指正之后发现惰性加载函数细节有问题,已改正在这里也补充一下,这些都是根据自己理解写的例子,不一定说的都对,有些只能查看不能运行的要谨慎,因为我可能只是将方法思路写出来,没有实际跑...

    hqman 评论0 收藏0
  • [ JS 进阶 ] 闭包作用域链垃圾回收,内存泄露

    摘要:执行返回的内部函数,依然能访问变量输出闭包中的作用域链理解作用域链对理解闭包也很有帮助。早期的版本里采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。 关于闭包,我翻了几遍书,看了几遍视频,查了一些资料,可是还是迷迷糊糊的,干脆自己动手来个总结吧 !欢迎指正... (~ o ~)~zZ 1. 什么是闭包? 来看一些关于闭包的定义: 闭包是指有权...

    clasnake 评论0 收藏0
  • 前端基础进阶(四):详细图解作用域链闭包

    摘要:之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。而的作用域链,则同时包含了这三个变量对象,所以的执行上下文可如下表示。下图展示了闭包的作用域链。其中为当前的函数调用栈,为当前正在被执行的函数的作用域链,为当前的局部变量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初学JavaScrip...

    aikin 评论0 收藏0

发表评论

0条评论

EastWoodYang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<