写在前面
本文是在《你不知道的JS》基础上总结加工而来,中间有本人自己的理解和体会,希望对大家有所帮助,保证让大家理解什么是作用域。
而且本文不是入门级所以基础的知识就没有涉及,比如闭包、this指向、let、const。
作用域是规则
为什么需要作用域?
首先就一门语言而言,变量的声明、赋值和读取使用是一切的基础,数据结构、函数和算法都是基于此才能实现。对变量的声明、赋值和读取都涉及到一个最基本的问题:不能够全部定义成全局的,如果全部都是全局的话内存就第一个受不了了,而且对于GC(垃圾回收)也会有很大的问题,同时你也不会知道你的变量何时何地被谁给修改过。所以我们需要制定一套规则,保证变量的使用范围。就像画了一个圈,将变量圈在其中。 保证在圈内变量是私有的不能够被外界访问,在圈不需要的时候就将其整个抹去。(类似于java中的私有属性和公开属性)
这套规则也叫做作用域。作用域是变量声明、赋值、读取的一整套规则。
JS作用域
对于我们的代码,以《你不知道的JS》中例子: var a = 2分析:
首先是浏览器参与到对程序 var a = 2; 进行处理的部分。
- 引擎:从头到尾负责整个JavaScript程序的编译及执行过程。
- 编译器:引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
- 作用域:引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询(VO+this+ScopeChain),并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
下面我们将 var a = 2; 分解,看看如何协同工作的。 编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。但是当编译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内存,将其命名为 a ,然后将值 2 保存进这个变量。”然而,这并不完全正确。 事实上编译器会进行如下处理:
- 词法分析: 遇到 var a ,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a 。
- 代码生成: 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。 引擎运行时会首先询问作用域, 在当前的作用域集合中是否存在一个叫作 a 的 变量。 如果是, 引擎就会使用这个变量;如果否, 引擎会继续查找该变量。
- 如果引擎最终找到了 a 变量, 就会将 2 赋值给它。 否则引擎就会抛出一个异常! 变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
以上摘自《你不知道的JS》,看完想必大家对于JS的解析过程有了初步的了解,下面对作用域进行详尽的讨论:
Js作用域按照实际存在可以分为
- 全局作用域
- 函数作用域
- 块级作用域
按照内部实现可以分为两类:
- 词法作用域
- 动态作用域 (动态作用域跟 this 引用机制相关)
全局作用域
全局作用域是最常见的,基本上只要是在最外层定义的或者未声明直接赋值的变量就是属于全局作用域。生命周期将存在于整个程序之内。能被程序中任何函数或者方法访问。在 JavaScript 内默认是可以被修改的。全局变量,虽然好用,但是是非常可怕的,这是所有程序员公认的事实。
函数作用域
函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域, 也可以属于某个代码块(通常指 { .. } 内部)。从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域。在 ES6 中引入了 let 关键字( var 关键字的表亲), 用来在任意代码块中声明变量。 if(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块中。有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。
函数内部的变量理应不被外界直接访问,同时在函数执行完应该被销毁,所以,函数作用域产生了,规定:函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域。
函数作用域的含义是指, 属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。 这种设计方案是非常有用的, 能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。
但是也有些方法可以在外层访问到函数作用域内部值,比如在函数内部直接return该值或者通过闭包(闭包会导致函数内部的变量被外界访问并且一直不被销毁)。
在JS没有块级作用域的时代中(不包括try catch之类),为了分离全局作用域,通常会使用立即执行函数来形成一个单独的函数作用域,利用函数作用域的特性保证内部的值不会污染全局。
(functuin(){
var a = 1
}) ()
//它能够自动执行 (function() { //... })() 里面包裹的内容,能够很好地消除全局变量的影响;
块级作用域
现在基本上直接指的就是let和const的定义。更多见 es6.ruanyifeng.com/#docs/let 不再过多介绍。 但是要记住with和try/catch也是会形成一个独立的块级作用域。
下面才是本文的重点部分:词法、动态作用域
词法、动态作用域
作用域共有两种主要的工作模型:
- 第一种是最为普遍的,被大多数编程语言所采用的词法作用域。
- 第二种叫作动态作用域 ,仍有一些编程语言在使用(比如 Bash 脚本、Perl 中的一些模式等)。
而两者在JS中两者都有体现。
词法作用域和动态作用域更像是抽象化的概念,是作用域的一种实现方式甚至是相互对立的,只不过js中两者都有。
全局、函数、块级作用域在js中都是可以感受到存在的,就像是具体实现,而这三者基本上也是遵循词法作用域的规则。
JS中符合动态作用域的也就只有this的指向问题了。
词法作用域
- 首先,在JS中除去this,变量都是遵循词法作用域规则。
- 简单地说,词法作用域就是定义在词法阶段的作用域。
- 换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
- 词法作用域是指一个变量的可见性,及其文本表述的模拟值。当我们要使用声明的变量时:JS引擎总会从最近的一个域,向外层域查找(注意得是定义的位置而不是执行的位置)
- 作用域查找会在找到第一个匹配的标识符时停止。
- 在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。
- 抛开遮蔽效应, 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
- 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
- 词法作用域查找只会查找一级标识符, 比如 a 、 b 和 c 。 如果代码中引用了 foo.bar.baz , 词法作用域查找只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。(原型链相关知识点)
以上的总结中,做重要的结论是:无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定(this除外)。
举个例子:
var a = 1;
function fn10() {
console.log('fn10', a);
}
function fn11() {
var a = 2;
fn10();
console.log('fn11', a);
}
fn11();
以上的输出是什么?
答案是
fn10 1
fn11 2
因为无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定(this除外)。JS中除去this都是在词法作用域规则下行事,this是动态作用域的规则。
动态作用域:
在编程中,最容易被低估和滥用的概念就是动态作用域。在 JavaScript 中的仅存的应用动态作用域的地方:this 引用,所以这是一个大坑! 当然,JavaScript 除了this之外,其他,都是根据词法作用域查找
- 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用 。
- 动态作用域,作用域是基于调用栈的,而不是代码中的作用域嵌套;作用域嵌套,有词法作用域一样的特性,查找变量时,总是寻找最近的作用域
- 需要明确的是, 事实上 JavaScript 并不具有动态作用域。 它只有词法作用域, 简单明了。 但是 this 机制某种程度上很像动态作用域。
- 主要区别:词法作用域是在写代码或者说定义时确定的, 而动态作用域是在运行时确定的。( this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
由于篇幅限制本文暂时不讨论this的绑定问题,以后会详细叙述。
作用域链
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。
- 当代码进入到某个执行环境,准备执行时,会为该执行环境对应的变量对象创建一个作用域链。作用域链其实就相当于一个变量对象的集合,其第一个元素是当前执行环境的变量对象,最后一个元素是全局执行环境的变量对象(在浏览器中即window对象)。
- 作用域链其实就是个从当前函数的变量对象开始,从里到外取出所有变量对象,组成的一个列表。通过这个作用域链列表,就可以实现对上层作用域的访问。
- 通过作用域链,函数能够访问来自它上层作用域(执行环境)中的变量(作用域链的主要作用就是取值)
作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError。作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。
以上就是本人对js作用域的一些理解,希望对大家有所帮助,也欢迎大家指出我的错误。