前言

前面的博客我已经说完了块级作用域的实现,站在变量环境和词法环境的角度下去看待这些个问题,会让你对js的工作模式有更加清晰的认识.
接下来我们看一段代码

function bar() {console.log(myname)
}
function foo() {var myname = '凯隐'bar()
}
var myname = '拉亚斯特'
foo()

直接告诉我们这段代码应该打印什么呢?应该打印凯隐,可是结果却出人意料
打印的是拉亚斯特,
唉?不对啊,按照我们之前所说的,bar函数入栈之后查找myname,找不到就去上一个执行上下文去查找,那么应该打印的是凯隐的啊,为什么不一样呢?要解释这个情况,我们就要搞清楚作用域链了

作用域链

关于作用域链,很多人一开始学习的时候会很费解,因为这总是会出现与我们直觉不符合的东西,但是如果我们理解了调用栈,执行上下文,词法环境,变量环境等概念,理解起来也相对容易一点.所以建议把我之前的博客好好看看,认真理解一下,并且动手实验(实际上动手验证自己的想法是加深自己理解最好的一种方式)

其实在每个变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer,
其次,outer的绑定规则并不是我们直觉理解的那样,直接指向父级的执行上下文,这个一定要牢记.

首先我们理解什么是作用域,作用域简单的来说,可以理解变量的使用区域,它限制了变量的使用范围,比如你把自己比作一个变量,那么你所能活动的区域可能就是学校,那么作用域就是学校,

作用域又分为静态作用域和动态作用域,一定要记住的就是,js是静态作用域语言,尽管js中的this在某种程度上很像动态作用域,但是js本质一定是静态作用域的,这个一定要搞清楚,

什么是静态作用域呢,静态作用域是指,一个变量在声明时,它的作用域范围只绑定在声明时候的环境,至于它什么时候运行,何时运行,是不会影响到静态作用域的

接下来,我们通过分析上面的代码,具体解释一下静态作用域

首先,bar函数声明的时候,会产生一个作用域,这个没问题,然后这个作用域里面有myname变量,这个也没有问题
,但是outer的指向就有问题了,如果理解了我上面所说的静态作用域的规则,那么想必大家已经知道了,没错,这个outer指向的全局的myname--拉亚斯特.
同理,foo函数的outer同样指向全局,我这么说的话大家也就不难理解了.

一定要记住,js的作用域和作用域链的查找,跟函数何时调用,怎么调用是无关的,(有this干涉情况除外)

看代码:

function bar() {var myname = '拉亚斯特'let test1 = 100if (1) {let myname = 'Chrome浏览器'console.log(test)}
}
function foo() {var myname = '凯隐'let test = 2{let test = 3bar()}
}
var myname = '影流之镰'
let myAge = 10
let test = 1
foo()

打印的是1
分析流程.

  • 首先函数的声明,变量的提升,赋值,然后词法环境中的let声明的变量赋值
  • 执行foo函数,foo函数执行上下文入栈,进行一些操作
  • 执行到foo函数词法环境中的test赋值,执行bar函数
  • bar函数入栈,进行一些操作,执行到if块中的打印操作,需要打印test
  • 首先当前块中查找,也就是当前词法环境中那个块查找
  • 没有找到test,去变量环境中查找test,也没有找到
  • 查找变量环境中的outer,根据outer来到父级(bar函数)作用域,重复以上操作,先查找词法环境,然后变量环境
  • 没有找到test,继续根据outer,来到全局作用域,查找词法环境,找到了test,打印结果

所以结果是全局下的test —1
所以说作用域就是当前变量和常量的作用范围,而作用域链,就是outer连接起来各个作用域的一种查找变量的规则

闭包

了解了作用域和作用域链之后,接下来我们来讲讲闭包,对于初学者来说,初次接触闭包,很可能让人产生一种挫败感,因为背后的原理对于一个初学者来说理解起来还是有一定难度的,更要命的是我们编写项目时,总是会充斥着大量的闭包代码

但是我们了解了变量环境,词法环境作用域链等概念之后我们理解起来也就容易多了
其实我还是建议大家,多自己动手去验证自己的理解

先看下面的代码,通过闭包实现对象的私有变量

function Person(name, age) {return {name,getAge: function () {return age},}
}
console.log(Person('张三', 18).name)
console.log(Person('张三', 18).age)
console.log(Person('张三', 18).getAge())

以上代码输出的结果:张三,undefined,18

看了我前几篇博客的都知道,函数在执行完之后是要出栈的,esp(指向当前执行栈工作的执行上下文的指针)会从当前的执行上下文下移,然后上面的执行上下文会出栈,并且引起垃圾回收工作(这个以后讲).

但是这段代码Person函数执行完了,本该销毁的name却能够在返回的对象中访问到?这是为什么呢?
这跟垃圾回收机制有关了,这里简单说一下,在我们运行代码的时候,有一个概念叫做可达性,就是说对于代码中的变量,如果能够以某种方式访问到,那么就代表这个变量是可达的,如果一个变量无论以什么方式都访问不到了,就做这个变量是不可达的,js引擎对于那些不可达的变量是会进行回收的,回收方法现在一般都是标记清除,这个以后讲到了js的垃圾回收器再深入讲解.

总而言之,上面的代码中return了一个对象,对象中声明了一个name,那么根据作用域链查找规则,name会查找到它的上层作用域,也就是Person函数中的name会和return对象中的name进行一个绑定,也就是函数中的name和返回对象中的name是同一个玩意儿

那么函数中的name此时就能通过返回的对象中的name来查找到了,此时这个name就是一个可达的变量,js引擎会为这些本该销毁的变量却没有销毁的变量创建一个闭包对象来保存

具体可以通过浏览器的开发者工具

来查看到闭包
所以我们打印返回的对象的name的时候,是可以打印出张三的
之后我们打印age,而age在对象中我们是没有具体声明的,我们只在对象的函数getAge中声明了age,但是根据作用域链的规则,上层作用域是无法访问到下层作用域的,也就是说返回的这个匿名对象里是没有age的,所以打印出来是undefined.(对象.age相当于是追加了一个age属性,不会报age is not undefined的错误,只会赋值undefined)
然后我们执行getAge函数,由于我们在getAge函数内部隐式声明了age,那么age就会沿着作用域链查找,直到找到Person函数中的age,进行一个绑定,js引擎就会将这个age加入闭包对象中,所以我们就能访问到啦

建议初学者将我上面所说的流程仔细捋一遍,然后自己动手写一些,验证一下,加深理解

当然很多人不懂得地方可能就在于为什么age没有声明就访问不到闭包呢?前面的代码没有声明也会沿着作用域链查找的啊.我们要发现段代码的不同点,仔细观察会发现,闭包具体是什么时候产生的呢?没错,函数执行完将要销毁的时候,这里需要一个关键字return,我们前面的代码就是在函数没有执行完的时候访问,那么没有声明也能沿着作用域链查找,但是函数执行完后产生闭包必须的就是函数要执行完,进行销毁流程的时候才会进行一个闭包检查,产生闭包

好了,说完了这些,最后弄一个题目

var bar = {myname: '拉亚斯特',printName: function () {console.log(myname)},
}
function foo() {let myname = '凯隐'return bar.printName
}
let myname = '影流之镰'
let _printName = foo()
_printName()
bar.printName()

你觉得结果是什么呢?
都是拉亚斯特?
其实最后的结果两次都是影流之镰
foo()函数返回的是bar对象里的printName,我们用_printName来接收了一下
_printName这个变量指向的就是bar这个对象的堆内存地址(js的内存存储我以后将)

接下来最关键的来了,我们执行_printName(),其实执行的就是堆内存中的bar.printName,
而这个打印的myname沿着作用域链查找找到的是全局下的myname:影流之镰刀
为什么呢?为什么不去堆内存中查找bar这个对象的myname:拉亚斯特呢?
所以我们又了解了一点作用域链的查找规则是作用在调用栈中的,对于保存在堆内存中的引用型对象是不起作用的(函数是一种特殊的对象,因为函数执行是要入调用栈的)
所以以上代码的作用域查找是会避开bar对象中的拉亚斯特,而去查找全局的myname--影流之镰的

但是我们稍加改动就可以打印拉亚斯特

     var bar = {myname: '拉亚斯特',printName: function () {console.log(this.myname)},}function foo() {let myname = '凯隐'return bar.printName}let myname = '影流之镰'let _printName = foo()_printName()bar.printName()

我将bar对象中打印的myname前加了一个this
此时_printName打印的是undefined,(因为_printName声明是在全局状态下声明的,它的this默认绑定是指向是window,windwo下没有myname这个属性,全局下let声明的属性是不会挂载到window上的,var声明的变量才会挂载到window上,改成var就会打印影流之镰了,这个前面我讲过)

bar.printName打印的就是拉亚斯特了,因为this进行了一个隐式绑定,this指向的是bar这个对象,所以打印的就是bar这个对象的myname–拉亚斯特了

关于this,我下篇博客详细介绍

好了 今天就到这里了

作用域链和闭包?JS引擎如何选择顺序的选择变量?相关推荐

  1. JavaScript作用域、上下文、执行期上下文、作用域链、闭包

      作用域.上下文.执行期上下文.作用域链.闭包是JavaScript中关键概念之一,是JavaScript难点之一,在应聘面试时必定会问到的问题,作为前端工程师必须理解和掌握.相信大家已经阅读了很多 ...

  2. javaScript执行环境、作用域链与闭包

    一.执行环境 执行环境定义了变量和函数有权访问的其他数据,决定了他们各自的行为:每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中.虽然我们编写的代码无法访问这个对象 ...

  3. JavaScript重难点解析4(作用域与作用域链、闭包详解)

    JavaScript重难点解析4(作用域与作用域链.闭包详解) 作用域与作用域链 作用域 作用域与执行上下文 作用域链 闭包 闭包理解 将函数作为另一个函数的返回值 将函数作为实参传递给另一个函数调用 ...

  4. 在chrome开发者工具中观察函数调用栈、作用域链、闭包

    在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化.因此,断点调试对于快 ...

  5. JS 之 (三)作用域链、闭包、面向对象

    作用域链 定义 变量在当前环境now.内部环境f1.内部深层环境f2/f3-.都起作用的现象形成了一个链条,这个链条就称为变量的"作用域链" <html> <he ...

  6. 预编译、作用域链和闭包理解

    在理解预编译之前,首先了解一下JS的解析过程:JS引擎在解析脚本的过程分为两个阶段,预编译和执行,首先预编译然后再从上之下一行一行的执行代码.其次,要了解作用域,作用域是一个变量或者函数能够使用的空间 ...

  7. Javascript的作用域,作用域链,闭包

    1,作用域和作用域链概念 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1.1 全局作用域,在代码 ...

  8. 如何延长作用域链_第4部分2:作用域(链)和 闭包

    知识列表作用域/作用域链 闭包(涉及JS垃圾回收机制 )https://zhuanlan.zhihu.com/p/27110726 [ js 基础 ][读书笔记]作用域和闭包https://jueji ...

  9. Javascript读书笔记(六)函数作用域,声明提前,作用域链,闭包

    参照内容来自<JavaScript权威指南>,<JavaScript高级程序设计> 函数作用域 类似C语言的编程语言中有块级作用域,JavaScript中没有块级作用域,取而代 ...

最新文章

  1. jvm性能调优实战 - 46堆区OOM解析
  2. Kafka基础知识入门
  3. 数据分析项目某电商app行为数据分析(1)
  4. mysql function函数_详解MySQL如何按表创建千万级的压测数据
  5. 打开eclipse出现Failed to load the JNI shared library “D:\java\jdk\bin\...\jre\bin\server\jvm.dll”如何解决?
  6. 使用IntelliJ IDEA 2020 高效开发 springboot项目
  7. 使用Linux进行c或c++编程
  8. css 列表相关的属性 列表前的小点点 0302
  9. selenium基础入门
  10. 将Tomcat集成到eclipse中并写出第一条web语句
  11. 運行命令”msiexec”查看Windows Installer的版本
  12. C/C++程序员桌面壁纸---简尚黑
  13. 【FICO】SAP中的银行
  14. oracle adjusting parallel,ora-29740故障求救
  15. 中国房企加速并购重组
  16. Ubuntu cd 命令
  17. Docker hub配置国内加速器
  18. 1微秒等于多少皮秒_皮秒(ps)是一个时间单位。它是这样换算的↓1秒s_圈子-新氧美容整形...
  19. Linux学习笔记(3)- 网络编程以及范例程序
  20. KSO-docker命令大全,基于Linux服务器CentOS7.5 安装docker

热门文章

  1. 中职计算机技能高考课件,中职技能高考 语文复习交流 19版.ppt
  2. Tensorflow中transpose 解析
  3. 上位机通讯及其数据云存储项目总结
  4. 多目标跟踪数据关联的二部图解:CVPR18多目标跟踪开创性深度端到端二部图匹配佳作《Deep Learning of Graph Matching》读后有感
  5. 考NPDP有什么好处
  6. 传统图像处理之随机脉冲噪声检测(二)
  7. img,br标签以及img标签的路径问题
  8. 2020年百度搜索算法规范(算法盘点)
  9. 解耦神器之观察者模式
  10. 2020安徽程序设计省赛 I美丽几何