您的位置:首页 » 分类: JS & ES2015 (ES6) » 文章: 实例分析 JavaScript 作用域

实例分析 JavaScript 作用域

小编推荐:掘金是一个高质量的技术社区,从 ECMAScript 6 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。各大应用市场搜索「掘金」即可下载APP,技术干货尽在掌握..

了解作用域对于编写代码至关重要,作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。对于JavaScript中作用域我们可能已经了解了很多。建议看看 深入理解JavaScript中的作用域和上下文JavaScript 核心概念之作用域和闭包。今天从其他方面介绍一下 JavaScript 中作用域,以帮助我们更好的完整的了解 JavaScript 作用域。

作用域模型

作用域有两种常见的模型:词法作用域(Lexical Scope,通常也叫做 静态作用域) 和 动态作用域(Dynamic Scope)。其中词法作用域更常见,被 JavaScript 等大多数语言采用。(愚人码头注:这里避开了witheval特殊语句,不再做介绍)。

首先了解一下这两种模型的说明:

  • 词法作用域:词法作用域是指在词法分析阶段就确定了,不会改变。变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。
  • 动态作用域:动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们在何处调用。

JavaScript 的词法作用域

如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:

  1. 读入第一个代码段(js执行引擎并非一行一行地分析程序,而是一段一段地分析执行的)
  2. 做词法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
  3. var变量和function定义做“预解析“(永远不会报错的,因为只解析正确的声明)
  4. 执行代码段,有错则报错(比如变量未定义)
  5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
  6. 完成

JavaScript 解析过程

JavaScript 中每个函数都都表示为一个函数对象(函数实例),函数对象有一个仅供 JavaScript 引擎使用的[[scope]] 属性。通过语法分析和预解析,将[[scope]] 属性指向函数定义时作用域中的所有对象集合。这个集合被称为函数的作用域链(scope chain),包含函数定义时作用域中所有可访问的数据。

JavaScript 执行过程

执行具体的某个函数时,JS引擎在执行每个函数实例时,都会创建一个执行期上下文(Execution Context)和激活对象(active Object)(它们属于宿主对象,与函数实例执行的生命周期保持一致,也就是函数执行完成,这些对象也就被销毁了,闭包例外。)

执行期上下文(Execution Context)定义了一个函数正在执行时的作用域环境。它使用函数[[scope]]属性进行初始化。

随后,执行期上下文 顶部 的会创建一个激活对象(active Object),这个激活对象保存了函数中的所有形参,实参,局部变量,this 指针等函数执行时函数内部的数据情况。这个时候激活对象中的那些属性并没有被赋值,执行函数内的赋值语句,这才会对变量集合中的变量进行赋值处理。也就是说 激活对象是一个可变对象,里面的数据随着函数执行时的数据变化而变化。

具体请查看 JavaScript 核心概念之作用域和闭包 中的解释。有点啰嗦了。

考虑一下下图中的代码:

javascript的词法作用域

分析过程:

  • 作用域1 (绿色) :即全局作用域,包含变量foo;
  • 作用域2 (黄色) :foo函数的作用域,包含变量a,bar,b
  • 作用域3 (蓝色) :bar函数的作用域,包含变量c

bar 作用域里完整的包含了 foo 的作用域, 因为 bar 是定义在 foo 中的,产生嵌套作用域。值得注意的是,一个函数作用域只有可能存在于一个父级作用域中,不会同时存在两个父级作用域。还有诸如this , window , document等全局对象这里就不说了,避免混乱。

执行过程:

  • 语句console.log寻找变量a,b,c;
  • 其中c在自己的作用域中找到,
  • ab在自己的作用域中找不到,于是向上级作用域中查找,在foo的作用域中找到,并且调用。

函数在执行时,每遇到一个变量,都会去执行期上下文的作用域链的顶部,也就是执行函数的激活对象开始搜索,如果在第一个作用域链(即,Activation Object 激活对象)中找到了,那么就返回这个变量。如果没有找到,那么继续向下查找,直到找到为止。如果在整个执行期上下文中都没有找到这个变量,在这种情况下,该变量被认为是未定义的。也就是说如果foo的作用域中也定义了c,但bar函数只调用自己作用域里的c。这就是我们说的变量取值。

实例分析

上面讲了很多概念性的东西。下面我们用实际代码讨论一下变量的作用域。这些可能在一下“坑人”的面试题中很常见。

不同作用域中的同名变量

同名变量可能在存在于多个作用域中,具体取值在执行时变量查找时决定。

示例1,作用域中变量查找规则:

function DoSomething()
{
  var a = 2;
  console.log(a); // 2
  console.log(window.a); // 1
}
var a = 1;
DoSomething();

这个很好理解,全局作用域中定义并赋值了变量aDoSomething函数中也定义并赋值了变量aDoSomething函数在执行时,首先找到的是激活对象中的变量a,也就是2

console.log(window.a)也很好理解,就是首先找window对象,在激活对象中没找到,继续向上搜索,本例中,window对象在Global Object(全局对象)中找到,windowa属性值为1

这段代码我相信对大家来说,理解起来都没问题。那么我们稍微对代码做一下修改:

示例2,作用域中没赋值的变量:

function DoSomething()
{
  var a; // 注意这一行,定义了一个变量 a 但是不赋值。
  console.log(a); // 'undefined'
}
var a = 1;
DoSomething();

DoSomething函数中定义了变量a,但是没有赋值,那么这里打印结果是什么呢?结果是 undefined ,原因和示例1一样,只不过DoSomething函数在执行时,首先找到的是激活对象中的变量a,只不过这里的变量a并没有赋值,所以是 undefined

示例3,激活对象是一个可变对象

function DoSomething()
{
  console.log(a); // 'undefined'
  var a = 2; // 注意这一行。
  console.log(a); // 2
}
var a = 1;
DoSomething();

根据作用域中变量查找规则,当前的激活对象中找到了有变量 a 的定义,执行到第一条console.log(a)语句时值为 undefined 。执行到第二条console.log(a)语句时,变量 a 已经被赋值为2。也就是说,激活对象是一个可变对象,里面的数据随着函数执行时的数据变化而变化。

参数和同名变量

上面3个示例讲了不同作用域中的同名变量,这里我们来讲讲 形参(parameters)、实参(arguments)与同名变量的关系。

首先了解一下形参和实参:

function one(a,b,c) {
    console.log(one.length);//形参数量
}
function two(a,b,c,d,e,f,g){
    console.log(arguments.length);//实参数量
}
one(1)
two(1)

顾名思义,形参就是形式参数,是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传递的参数。实参就是实际参数,是在调用时传递给函数的参数,即传递给被调用函数的值。实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。

那么形参、实参与变量同名又该如何呢?

示例4,形参、实参与同名全局变量的关系

// DoSomething传入了一个 a 参数。
var b = 2;
function DoSomething(a,b)
{
  var a;
  console.log(a); // 1
  console.log(b); // 'undefined'
}
DoSomething( 1 );

打印的结果为1undefined,为什么呢?

首先来看形参b和全局变量b,这两者毫无关系,只是同名。完全符合作用域中变量查找规则,DoSomething函数在执行时,首先找到的是激活对象中的形参b,形参b没有值,所以是 undefined

接着来说说 参数a和局部变量a,看上面的代码似乎并不符合作用域中变量查找规则。如果作用域中变量查找规则去理解,应该是这样的,DoSomething函数在执行时,首先找到的是激活对象中的参数a,然后定义局部变量a,不赋值,所以console.log(a)undefined。但是结果恰恰是1。这又是为什么呢?

示例4并不能解释这个问题,让我们再来看一段代码。

示例5,形参、实参与同名局部变量的关系

function DoSomething(a)
{
  console.log(a); // 1
  console.log(arguments[0]); // 1
  var a = 2;
  console.log(a); // 2
  console.log(arguments[0]); // 2
}
DoSomething( 1 );

打印的结果是1,1,2,2。从上面的代码可以看到,参数a和局部变量a值是完全相同的,即使是局部变量a重新定义和赋值之后。这样就好理解了,参数和同名变量之间是 “引用” 关系,也就是说 JavaScript 引擎的处理参数和同名局部变量是都引用同一个内存地址。所以示例5中修改局部变量会影响到arguments的情况出现。

建议看看 深入理解JavaScript中的作用域和上下文JavaScript 核心概念之作用域和闭包。这篇文章只是上面两篇文章的后续。

正文完。下面还有一个推广让最好的人才遇见更好的机会!

互联网行业的年轻人,他们面对着怎样的职业瓶颈、困惑与未来选择?过去,这鲜有人关心。资深的职场人,也多半优先选择熟人去推荐机会。

100offer致力于改变现状,帮互联网行业最好的人才发现更好的机会。使用 100offer.com 或 100offer App ,可以一周内获得中国、美国等数千家优质企业的工作机会。

马上去遇见更好的机会
推广结束

关注WEB前端开发官方公众号

关注国内外最新最好的前端开发技术干货,获取最新前端开发资讯,致力于打造高质量的前端技术分享公众号

版权声明

本文仅用于学习、研究和交流目的,欢迎非商业性质转载。
转载请注明文章的完整链接:http://www.css88.com/archives/7300
作者(译者): 及 网站出处:CSS88.com

发表评论

电子邮件地址不会被公开。 必填项已用*标注