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


Posted in Javascript onMarch 21, 2017

什么是作用域?

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

什么是作用域链?

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

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

作用域链的形成?

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

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

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

数据访问流程

如上图,当程序访问一个变量时,按照作用域链的单向访问特性,首先在头节点的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();

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

2.try-catch语句的catch块

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

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

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()
})()

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

(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()
})()

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

在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()
})()

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

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

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

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

关于闭包

1.什么是闭包?

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

2.闭包的几种实现方式

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

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

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

如上两图所示,是在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()

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

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

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.闭包的注意事项

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

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

Javascript 相关文章推荐
JavaScript 闭包在封装函数时的简单分析
Nov 28 Javascript
jQuery Tips 为AJAX回调函数传递额外参数的方法
Dec 28 Javascript
jQuery 网易相册鼠标移动显示隐藏效果实现代码
Mar 31 Javascript
使用firebug进行调试javascript的示例
Dec 16 Javascript
基于Bootstrap+jQuery.validate实现Form表单验证
Dec 16 Javascript
理解javascript中try...catch...finally
Dec 25 Javascript
JS验证 只能输入小数点,数字,负数的实现方法
Oct 07 Javascript
详解vue-cli 构建Vue项目遇到的坑
Aug 30 Javascript
vue-cli2打包前和打包后的css前缀不一致的问题解决
Aug 24 Javascript
微信小程序组件传值图示过程详解
Jul 31 Javascript
Vue使用mixin分发组件的可复用功能
Sep 01 Javascript
详解vue高级特性
Jun 09 Javascript
Bootstrap警告框(Alert)插件使用方法
Mar 21 #Javascript
Bootstrap标签页(Tab)插件使用方法
Mar 21 #Javascript
JavaScript数组和对象的复制
Mar 21 #Javascript
Vue响应式添加、修改数组和对象的值
Mar 20 #Javascript
zTree实现节点修改的实时刷新功能
Mar 20 #Javascript
Vue指令的钩子函数使用方法
Mar 20 #Javascript
非常实用的vue导航钩子
Mar 20 #Javascript
You might like
用Php编写注册后Email激活验证的实例代码
2013/03/11 PHP
PHP实现阳历到农历转换的类实例
2015/03/07 PHP
IE 缓存策略的BUG的解决方法
2007/07/21 Javascript
jquery 上下滚动广告
2009/06/17 Javascript
一个可以随意添加多个序列的tag函数
2009/07/21 Javascript
Extjs学习笔记之九 数据模型(上)
2010/01/11 Javascript
在JQuery dialog里的服务器控件 事件失效问题
2010/12/08 Javascript
实现变速回到顶部的JavaScript代码
2011/05/09 Javascript
原生javascript实现Tab选项卡切换功能
2015/01/12 Javascript
Angularjs编写KindEditor,UEidtor,jQuery指令
2015/01/28 Javascript
jquery控制显示服务器生成的图片流
2015/08/04 Javascript
JS判断是否长按某一键的方法
2016/03/02 Javascript
深入理解JavaScript中Ajax
2016/08/02 Javascript
Backbone View 之间通信的三种方式
2016/08/09 Javascript
Node.js实现兼容IE789的文件上传进度条
2016/09/02 Javascript
js转html实体的方法
2016/09/27 Javascript
利用Angular.js限制textarea输入的字数
2016/10/20 Javascript
JS生成和下载二维码的代码
2016/12/07 Javascript
js实现鼠标拖拽多选功能示例
2017/08/01 Javascript
vue router带参数页面刷新或回退参数消失的解决方法
2019/02/27 Javascript
vue防止花括号{{}}闪烁v-text和v-html、v-cloak用法示例
2019/03/13 Javascript
mpvue实现小程序签到金币掉落动画(api实现)
2019/10/17 Javascript
Vue+penlayers实现多边形绘制及展示
2020/12/24 Vue.js
Python装饰器基础详解
2016/03/09 Python
用python写一个windows下的定时关机脚本(推荐)
2017/03/21 Python
Python+request+unittest实现接口测试框架集成实例
2018/03/16 Python
Python numpy中矩阵的基本用法汇总
2019/02/12 Python
matplotlib设置颜色、标记、线条,让你的图像更加丰富(推荐)
2020/09/25 Python
荷兰之家英文站:Holland at Home
2016/10/26 全球购物
Lowe’s加拿大:家居装修、翻新和五金店
2019/12/06 全球购物
工商管理专业学生的自我评价
2013/10/01 职场文书
初中地理教学反思
2014/01/11 职场文书
《平移和旋转》教学反思
2016/02/19 职场文书
90条交通安全宣传标语
2019/10/12 职场文书
Redis Cluster 集群搭建你会吗
2021/08/04 Redis
Consul在linux环境的集群部署
2022/04/08 Servers