前言
前面的博客我已经说完了块级作用域的实现,站在变量环境和词法环境的角度下去看待这些个问题,会让你对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 = 100
if (1) {
let myname = 'Chrome浏览器'
console.log(test)
}
}
function foo() {
var myname = '凯隐'
let test = 2
{
let test = 3
bar()
}
}
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,我下篇博客详细介绍
好了 今天就到这里了
3241

被折叠的 条评论
为什么被折叠?



