图解Javascript——作用域、作用域链、闭包

发布时间 - 2026-01-11 00:16:30    点击率:

什么是作用域?

作用域是一种规则,在代码编译阶段就确定了,规定了变量与函数的可被访问的范围。全局变量拥有全局作用域,局部变量则拥有局部作用域。 js是一种没有块级作用域的语言(包括if、for等语句的花括号代码块或者单独的花括号代码块都不能形成一个局部作用域),所以js的局部作用域的形成有且只有函数的花括号内定义的代码块形成的,既函数作用域。

什么是作用域链?

作用域链是作用域规则的实现,通过作用域链的实现,变量在它的作用域内可被访问,函数在它的作用域内可被调用。

作用域链是一个只能单向访问的链表,这个链表上的每个节点就是执行上下文的变量对象(代码执行时就是活动对象),单向链表的头部(可被第一个访问的节点)始终都是当前正在被调用执行的函数的变量对象(活动对象),尾部始终是全局活动对象。

作用域链的形成?

我们从一段代码的执行来看作用域链的形成过程。

function fun01 () {
 console.log('i am fun01...');
 fun02();
}
function fun02 () {
 console.log('i am fun02...');
}
fun01();

数据访问流程

如上图,当程序访问一个变量时,按照作用域链的单向访问特性,首先在头节点的AO中查找,没有则到下一节点的AO查找,最多查找到尾节点(global AO)。在这个过程中找到了就找到了,没找到就报错undefined。

延长作用域链

从上面作用域链的形成可以看出链上的每个节点是在函数被调用执行是向链头unshift进当前函数的AO,而节点的形成还有一种方式就是“延长作用域链”,既在作用域链的头部插入一个我们想要的对象作用域。延长作用域链有两种方式:

1.with语句

function fun01 () {
 with (document) {
  console.log('I am fun01 and I am in document scope...')
 }
}
fun01();

2.try-catch语句的catch块

function fun01 () {
 try {
  console.log('Some exceptions will happen...')
 } catch (e) {
  console.log(e)
 }
}
fun01();

ps:个人感觉with语句使用需求不多,try-catch的使用也是看需求的。个人对这两种使用不多,但是在进行这部分整理过程中萌发了一点点在作用域链层面的不成熟的性能优化小建议。

由作用域链引发的关于性能优化的一点不成熟的小建议

1.减少变量的作用域链的访问节点

这里我们自定义一个名次叫做“查找距离”,表示程序访问到一个非undefined变量在作用域链中经过的节点数。因为如果在当前节点没有找到变量,就跳到下一个节点查找,还要进行判断下一个节点中是否存在被查找变量。“查找距离”越长,要做的“跳”动作和“判断”动作也就越多,资源开销就越大,从而影响性能。这种性能带来的差距可能少数的几次变量查找操作不会带来太多性能问题,但如果是多次进行变量查找,性能对比则比较明显了。

(function(){
 console.time()
 var find = 1      //这个find变量需要在4个作用域链节点进行查找
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    while(i <= 100000000){
     if(find){
      i++
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

(function(){
 console.time()
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    var find = 1      //这个find变量只在当前节点进行查找
    while(i <= 100000000){
     if(find){
      i++
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

在mac pro的chrome浏览器下做实验,进行1亿次查找运算。

实验结果:前者运行5次平均耗时85.599ms,后者运行5次平均耗时63.127ms。

2.避免作用域链内节点AO上过多的变量定义

过多的变量定义造成性能问题的原因主要是查找变量过程中的“判断”操作开销较大。我们使用with来进行性能对比。

(function(){
 console.time()
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    var find = 10
    with (document) {
     while(i <= 1000000){
      if(find){
       i++
      }
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

在mac pro的chrome浏览器下做实验,进行100万次查找运算,借助with使用document进行的延长作用域链,因为document下的变量属性比较多,可以测试在多变量作用域链节点下进行查找的性能差异。

实验结果:5次平均耗时558.802ms,而如果删掉with和document,5次平均耗时0.956ms。

当然,这两个实验是在我们假设的极端环境下进行的,结果仅供参考!

关于闭包

1.什么是闭包?

函数对象可以通过作用域链相互关联起来,函数体内的数据(变量和函数声明)都可以保存在函数作用域内,这种特性在计算机科学文献中被称为“闭包”。既函数体内的数据被隐藏于作用于链内,看起来像是函数将数据“包裹”了起来。从技术角度来说,js的函数都是闭包:函数都是对象,都关联到作用域链,函数内数据都被保存在函数作用域内。

2.闭包的几种实现方式

实现方式就是函数A在函数B的内部进行定义了,并且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。如下:

如上两图所示,是在chrome浏览器下查看闭包的方法。两种方式的共同点是都有一个外部函数outerFun(),都在外部函数内定义了内部函数innerFun(),内部函数都访问了外部函数的数据。不同的是,第一种方式的innerFun()是在outerFun()内被调用的,既声明和被调用均在同一个执行上下文内。而第二种方式的innerFun()则是在outerFun()外被调用的,既声明和被调用不在同一个执行上下文。第二种方式恰好是js使用闭包常用的特性所在:通过闭包的这种特性,可以在其他执行上下文内访问函数内部数据。

我们更常用的一种方式则是这样的:

//闭包实例
function outerFun () {
 var outerV1 = 10
 function outerF1 () {
  console.log('I am outerF1...')
 }
 function innerFun () {
  var innerV1 = outerV1
  outerF1()
 }
 return innerFun //return回innerFun()内部函数
}
var fn = outerFun()  //接到return回的innerFun()函数
fn()     //执行接到的内部函数innerFun()

此时它的作用域链是这样的:

3.闭包的好处及使用场景

js的垃圾回收机制可以粗略的概括为:如果当前执行上下文执行完毕,且上下文内的数据没有其他引用,则执行上下文pop出call stack,其内数据等待被垃圾回收。而当我们在其他执行上下文通过闭包对执行完的上下文内数据仍然进行引用时,那么被引用的数据则不会被垃圾回收。就像上面代码中的outerV1,放我们在全局上下文通过调用innerFun()仍然访问引用outerV1时,那么outerFun执行完毕后,outerV1也不会被垃圾回收,而是保存在内存中。另外,outerV1看起来像不像一个outerFun的私有内部变量呢?除了innerFun()外,我们无法随意访问outerV1。所以,综上所述,这样闭包的使用情景可以总结为:

(1)进行变量持久化。

(2)使函数对象内有更好的封装性,内部数据私有化。

进行变量持久化方面举个栗子:

我们假设一个需求时写一个函数进行类似id自增或者计算函数被调用的功能,普通青年这样写:

 var count = 0
 function countFun () {
  return count++
 }

这样写固然实现了功能,但是count被暴露在外,可能被其他代码篡改。这个时候闭包青年就会这样写:

function countFun () {
 var count = 0
 return function(){
  return count++
 }
}
var a = countFun()
a()

这样count就不会被不小心篡改了,函数调用一次就count加一次1。而如果结合“函数每次被调用都会创建一个新的执行上下文”,这种count的安全性还有如下体现:

function countFun () {
 var count = 0
 return {
  count: function () {
   count++
  },
  reset: function () {
   count = 0
  },
  printCount: function () {
   console.log(count)
  }
 }
}
var a = countFun()
var b = countFun()
a.count()
a.count()
b.count()
b.reset()
a.printCount()  //打印:2 因为a.count()被调用了两次
b.printCount()  //打印出:0 因为调用了b.reset()

以上便是闭包提供的变量持久化和封装性的体现。

4.闭包的注意事项

由于闭包中的变量不会像其他正常变量那种被垃圾回收,而是一直存在内存中,所以大量使用闭包可能会造成性能问题。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!


# js作用域和闭包  # js  # 闭包  # 作用域  # 作用域链  # Javascript的作用域、作用域链以及闭包详解  # 详解JavaScript作用域、作用域链和闭包的用法  # JavaScript闭包与作用域链实例分析  # 深入理解Javascript中的作用域链和闭包  # JS闭包、作用域链、垃圾回收、内存泄露相关知识小结  # 浅析JavaScript作用域链、执行上下文与闭包  # javascript从作用域链谈闭包  # 深入Javascript函数、递归与闭包(执行环境、变量对象与作用域链)使用详解  # JavaScript中的作用域链和闭包  # JavaScript深入理解作用域链与闭包详情  # 是在  # 都是  # 的花  # 是一种  # 过程中  # 则是  # 不多  # 链表  # 第二种  # 不成熟  # 的是  # 体内  # 是一个  # 就会  # 都有  # 太多  # 就像  # 在这个  # 都在  # 第一个 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Laravel观察者模式如何使用_Laravel Model Observer配置  清除minerd进程的简单方法  SQL查询语句优化的实用方法总结  如何在阿里云购买域名并搭建网站?  Laravel怎么使用Collection集合方法_Laravel数组操作高级函数pluck与map【手册】  简单实现Android文件上传  如何用AWS免费套餐快速搭建高效网站?  Laravel怎么导出Excel文件_Laravel Excel插件使用教程  Laravel怎么做缓存_Laravel Cache系统提升应用速度的策略与技巧  关于BootStrap modal 在IOS9中不能弹出的解决方法(IOS 9 bootstrap modal ios 9 noticework)  如何快速重置建站主机并恢复默认配置?  Edge浏览器提示“由你的组织管理”怎么解决_去除浏览器托管提示【修复】  悟空浏览器如何设置小说背景色_悟空浏览器背景色设置【方法】  今日头条微视频如何找选题 今日头条微视频找选题技巧【指南】  企业在线网站设计制作流程,想建设一个属于自己的企业网站,该如何去做?  Laravel如何使用Gate和Policy进行授权?(权限控制)  猎豹浏览器开发者工具怎么打开 猎豹浏览器F12调试工具使用【前端必备】  教你用AI润色文章,让你的文字表达更专业  魔毅自助建站系统:模板定制与SEO优化一键生成指南  javascript中闭包概念与用法深入理解  电商网站制作多少钱一个,电子商务公司的网站制作费用计入什么科目?  如何快速生成可下载的建站源码工具?  Laravel如何使用Blade模板引擎?(完整语法和示例)  Laravel如何清理系统缓存命令_Laravel清除路由配置及视图缓存的方法【总结】  浏览器如何快速切换搜索引擎_在地址栏使用不同搜索引擎【搜索】  Laravel如何实现一对一模型关联?(Eloquent示例)  香港服务器建站指南:免备案优势与SEO优化技巧全解析  高防服务器:AI智能防御DDoS攻击与数据安全保障  java ZXing生成二维码及条码实例分享  Laravel如何监控和管理失败的队列任务_Laravel失败任务处理与监控  Linux虚拟化技术教程_KVMQEMU虚拟机安装与调优  Python文件操作最佳实践_稳定性说明【指导】  Laravel如何设置自定义的日志文件名_Laravel根据日期或用户ID生成动态日志【技巧】  Laravel如何处理异常和错误?(Handler示例)  Python自然语言搜索引擎项目教程_倒排索引查询优化案例  网页设计与网站制作内容,怎样注册网站?  Laravel全局作用域是什么_Laravel Eloquent Global Scopes应用指南  个人摄影网站制作流程,摄影爱好者都去什么网站?  详解阿里云nginx服务器多站点的配置  Laravel如何实现多级无限分类_Laravel递归模型关联与树状数据输出【方法】  如何快速辨别茅台真假?关键步骤解析  如何获取免费开源的自助建站系统源码?  猪八戒网站制作视频,开发一个猪八戒网站,大约需要多少?或者自己请程序员,需要什么程序员,多少程序员能完成?  如何在企业微信快速生成手机电脑官网?  弹幕视频网站制作教程下载,弹幕视频网站是什么意思?  javascript基本数据类型及类型检测常用方法小结  Laravel N+1查询问题如何解决_Eloquent预加载(Eager Loading)优化数据库查询  Python正则表达式进阶教程_复杂匹配与分组替换解析  如何彻底删除建站之星生成的Banner?  如何在IIS7中新建站点?详细步骤解析