说到作用域,同学们可以一起想象一下它的词义 “在某一个区域里产生作用“。让我们看一段代码:
毫无疑问,这段代码输出的是张三。
这行代码输出的也是张三,很简单,name是window全局作用域下的变量。
任何javascript代码片段都要在执行前进行编译。变量的赋值操作会执行两个操作,一是编译器会在当前作用域下声明一个变量,二是运行时引擎会在作用域中查找变量,如果找到就为变量赋值,否则就会抛出一个异常 。
我们先来了解一个概念LHS and RHS 。当变量出现在赋值操作的左边时,采用LHS查询,出现在右边,则采用RHS查询 。
consoloe.log(name)这句代码name采用的是RHS引用,需要查找并赋值给name,name = '张三',这里的name则是采用LHS引用,查找到为 = '张三‘存放的容器 。赋值操作的左右并不意味着等号的左右,LHS可以理解为赋值操作的目标是谁,RHS可以理解为谁是赋值操作的源头 。
我们看看下面这段代码:
这段代码到底发生了什么呢:
- 引擎对foo进行RHS引用,查找foo的目标是什么
- 编译器查找作用域,找到foo是一个函数
- 引擎执行foo函数
- 引擎一行一行执行下来,对name进行LHS引用,查找name的源头是谁
- 编译器查找作用域,找到foo的形式参数
- 引擎把张三赋值给name
- 引擎对consoleRHS引用
- console是一个内置对象
- 引擎查找console,找到log()函数
- 引擎再次对name进行RHS
- 编译器查找作用域
- 引擎把name的值传入log()函数中
在上面是用var来声明变量的,值得注意的是:
在浏览器对象中,所有用var所声明的全局变量和函数都会变成window对象的方法和属性。
如果把var换成let和const,就会是如下的情况:
因为let和const不会被放进window对象里,this是运行时的东西,现在this是对window的映射,所以无法在window对象里找到变量。
接下来我们来看看词法作用域
词法作用于是由你在写代码时把变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变:
在这个例子中有三个逐级嵌套的作用域,包含着整个全局作用域,其中只有一个标识符: foo 。包含着 foo 所创建的作用域,其中有三个标识符: a 、 bar 和 b 。包含着 bar 所创建的作用域,其中只有一个标识符: c 。
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止,这个也被称为作用域链。
现在我们来看看函数作用域
在函数作用域中,函数外部访问函数内部。这个特点可以用来做隐藏效果。从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。
在这段代码中,foo2()和b应该是foo1的私有内容,这样暴露在全局,很危险,我们可以做以下改造。
在上面代码中最外层foo也是暴露在全局中的,我们可以用一个立即调用函数来实现隐藏
我们再来看看块作用域
我们在这个循环体外面打印i,得到的是10,这是因为在这段代码中i是绑定在外部作用域的。这个时候就可以用到块作用域了,就是在es6引入的let const。
我们再来看看提升的问题:
这是因为js引擎会先对js代码编译,然后再执行,变量和函数在内的所有声明都会在任何代码被执行前首先被处理。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
所以代码会处理成这样:
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫作提升。
有一个注意的点,函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量。
这段代码被处理成了这样:
var foo 尽管出现在 function foo()的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
最后我们就可以来学习闭包了:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
我们看看下面这段代码
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz() ,实际上只是通过不同的标识符引用调用了内部的函数 bar() 。bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。js引擎有垃圾回收机制,在变量使用完后会被销毁,但是这段程序内部会被保存下来,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
我们来看看这段代码
这个循环的终止条件是 i 不再 <=5 。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i 。
所以我们做出以下改动:
每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的,作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。当然也可以用let const来解决,因为它们也有单独的块作用域。
最后来总结一下:无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
本文章参考《你不知道的javascript上卷》《javascript高级程序设计》
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!