目录
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
引擎:从头到尾负责整个JAvaScript程序的编译及执行过程。
编译器:负责语法分析及代码生成等脏活累活。
作用域:负责收集并维护由所有声明的变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
下面来看看他们是怎么工作的:
遇到var a,编译器会问作用域是否已经有一个该名称的变量存在于同一个作用域集合中。如果是,编译器会忽略该声明,继续编译;否则它会要求作用域在当前作用域的集合中声明一个新变量,并命名为a。
接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a=2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫a的变量。如果是,引擎会使用这个变量;否则,引擎会继续查找该变量。
总结:变量的赋值操作会执行两个动作,首先编译器在当前作用域声明变量(如果没声明过),然后运行时引擎会在作用域查找该变量,找到会对它赋值。
作用域是一套规则,用于确定在何处以及如何查找变量。因此在查找变量的时候有着两种查询,分别是RHS查询和LHS查询。
怎么区分呢?如果查找的目的是对变量进行赋值,那么就会使用LHS查询,如果目的是获取变量的值,就会使用RHS查询。可以理解为LHS查询就是赋值操作的目标是谁,RHS查询就是谁是赋值操作的源头。
现在,对作用域就大概理解了吧,继续。
作用域有两种主要的工作模型,一种是词法作用域,一种是动态作用域。
而JavaScript采用的就是常用的词法作用域。
那么什么是词法作用域呢?
简单来说,词法作用域就是定义在词法阶段的作用域。换句话说就是,词法作用域在写代码时将变量和块作用域写在哪里决定的。
因为JS采用的是词法作用域,在写代码时作用域就确定;而在实际情况中,通常需要同时顾及几个作用域。
当一个块或函数嵌套在另一个块或函数中,就发生了作用域的嵌套。因此,在查找变量的时候,当前作用域无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(全局作用域)为止。
上面这个查找的过程,就形成了我们常说的作用域链。所以遍历作用域链的规则很简单;引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当地大最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
现在对词法作用域和作用域链的过程了解了吧应该,梳理一下:
总结:词法作用域意味着作用域是由书写代码时函数声明的位置决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及如何声明的,从而能够预测在执行过程中如何查找。
而查找的过程呢,LHS和RHS查询都会先在当前执行作用域中开始,如果有需要(没找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后抵达全局作用域,无论找到或没找到都将停止。
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。作用域内部能够获取到作用域外部声明的变量,而作用域外部无法获取到作用域内部声明的变量。不同作用域下同名变量不会有冲突。
函数作用域也可以看成对函数起隔离保护作用的一块区域,我们对函数的传统认知是先声明一个函数,然后再想里面添加代码。反过来想的话可以带来一些启示:
从所写的代码中挑选一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”了。(最小授权或最小暴露原则)
我们已经知道,在任意的代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
虽然这样可以解决一些问题,但不理想。为什么这么说呢,因为会导致一些额外的问题,必须显示地通过函数名调用这个函数才能运行其中的代码。
在这个情况下,如果函数不需要函数名(或者至少函数名不污染所在作用域),并且能够自动运行,就更理想了。
由此,出现了匿名函数表达式和立即执行函数表达式(IIFE)。
尽管函数作用域是最常见的作用域单元,当然也是现行大多数JavaScript中最普遍的设计方法,但其他类型的作用域单元也同样存在,并且通过使用其他作用域单元可以实现维护起来更加优秀、简洁的代码。
块作用域是一个用来对之前的最小授权原则进行拓展的工具,将代码从在函数中隐藏信息拓展为在块中隐藏信息。
JS本不支持块作用域,但ES6改变了现状,let和const由此出现。
let用法和var相同,用let代替var声明变量,就可以把变量的作用域限制在当前代码块中(临时死区),由于let声明不会被提升,一般都将let声明语句放在封装代码块的顶部。
同一作用域中不能用let重复定义已经存在的标识符
const关键字声明的是常量,其值一旦被设定后不再更改。
因此,每个通过const声明的常量必须进行初始化。
虽然const声明不允许修改绑定,但允许修改值,比如const对象,修改对象属性的值是可以的。
在声明前访问let和const的变量是报错的,因为绑定还在临时死区中(TDZ)。
目前使用块级绑定的最佳实践是:默认使用const,只有确实需要改变变量的值时用let。
总结:函数是JavaScript中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块。
首先我们回忆一下前面所说的,引擎会在解释JavaScript代码之前对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
因此,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
我们习惯将var a = 2看做一个声明,实际上JavaScript引擎并不这么认为。它将var a 和 a=2当做两个单独的声明,一个是编译阶段的任务,第二个是执行阶段的任务。
要注意的是,只有声明本身会提升,而赋值或其他运行逻辑会留在原地。
- foo();
- function foo(){
- console.log(a); //undifined
- var a = 2;
- }
还有一点就是:重复声明会被忽略。
- foo();//1
- var foo;
- function foo(){
- console.log(1);
- }
- foo = function(){
- console.log(2);
- }
函数声明会被提升,函数表达式foo=function(){}不会被提升;还有里面的var foo尽管出现在function foo()的前面,但它是重复的声明(自动被忽略)。
大部分编程语言都是先声明变量再使用,但在JS中,事情有些不一样:
- console.log(a)// undefined
- var a = 10
上述代码正常输出undefined
而不是报错Uncaught ReferenceError: a is not defined
,这是因为声明提升(hoisting),相当于如下代码:
- var a; //声明 默认值是undefined “准备工作”
- console.log(a);
- a=10; //赋值
我们都知道,创建一个函数的方法有两种,一种是通过函数声明function foo(){}
另一种是通过函数表达式var foo = function(){}
,那这两种在函数提升有什么区别呢?
- console.log(f1) // function f1(){}
- function f1() {} // 函数声明
- console.log(f2) // undefined
- var f2 = function() {} // 函数表达式
接下来我们通过一个例子来说明这个问题:
- function test() {
- foo(); // Uncaught TypeError "foo is not a function"
- bar(); // "this will run!"
- var foo = function () { // function expression assigned to local variable 'foo'
- alert("this won't run!");
- }
- function bar() { // function declaration, given the name 'bar'
- alert("this will run!");
- }
- }
- test();
在上面的例子中,foo()调用的时候报错了,而bar能够正常调用。
我们前面说过变量和函数都会上升,遇到函数表达式 var foo = function(){}
时,首先会将var foo
上升到函数体顶部,然而此时的foo的值为undefined,所以执行foo()
报错。
而对于函数bar()
, 则是提升了整个函数,所以bar()
才能够顺利执行。
有个细节必须注意:当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。
- alert(a);//输出:function a(){ alert('我是函数') }
- function a(){ alert('我是函数') }//
- var a = '我是变量';
- alert(a); //输出:'我是变量'
function声明的优先级比var声明高,也就意味着当两个同名变量同时被function和var声明时,function声明会覆盖var声明
总结:这意味着无论作用域的声明在什么地方,都将在代码本身被执行前进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
本文复习作用域相关的全面内容,包括作用域的理解,作用域在js工作原理中充当的角色;还有js的词法作用域,并由此引出作用域的嵌套以及作用域链的出现;了解完之后学习js中作用域的应用场景,函数作用域的隐藏内部实现功能中一些不理想的问题,让我们明白函数作用域中匿名函数表达式和立即执行函数表达式存在的意义;还有块作用域将代码从在函数中隐藏信息扩展为在块中隐藏信息的含义。最后,学习了解函数提升和变量提升,从而以此为基础,为之后闭包的充分理解做铺垫。