js:作用域(scope)是什么?

481 阅读6分钟

几乎所有的编程语言都具备存储变量中的值,并且能够对该变量的值进行访问和修改,JavaScript也不例外,那你是否思考过这些变量的值存在哪里?我们是如何找到他们的?因此需要一套良好的规则存储变量,这套规则就叫做作用域。

v2-8afc2f093326918255ec53793d5c06e1_720w.jpg

编译

程序在执行一段代码之前一般会经历三个步骤进行编译,在学习作用域之前可以先简单了解一下JavaScript代码在计算机是如何进行编译的。

举个栗子:

var a=1

首先编译器会先将这个程序分解为词法单元

在将词法单元解析为一个数行结构

代码生成时遇到var a编译器就会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,

如果存在,编译器会忽略该声明,继续进行编译

否则他会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。

接下来编译器就会为引擎生成运行时所需要代码

这些代码被用来处理a=2这个赋值操作

引擎运行时会首先询问作用域,当前的作用域集合中是否存在一个叫做a的变量

如果有,引擎就会使用这个变量

如果否,引擎会继续查找该变量

如果引擎最终找到了a变量,就会将1赋值给它,否则引擎就会举手示意并抛出一个异常

步骤内容
分词/词法分析这个过程会将字符串分解为计算机认为有意义的代码块,这些代码块被称为词法单元。拿var a=1来举例,通常会被分解为 var、a、=、1、;空格是否被作为词法单元,取决于空格是否有意义。
解析/语法分析这个过程是将词法单元流转换成一个有元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为抽象语法树(Abstract Syntax Tree,AST)
代码生成将ATS转换为可执行代码的过程被称为代码生成。简单来说就是引擎应用某种方法将var a=1转化为一组机器指令,用来创建一个叫做a的变量(包括分配内存等),并将一个值存储在a中。

在引擎查询过程中可以分为LHS查询和RHS查询

LHS查询是试图找到变量容器的本身,从而可以对其赋值。a=1,当前引擎对变量a查找的目的就会对其赋值,这种情况下,引擎不会去管变量a 的原始值是什么,将值1赋值a变量。 如果在查询时找不到会像上级查找直到全局作用域,如果还是找不到,就会在全局作用域中创建一个具有该名称的变量,并返回给引擎。

RHS查询是查找某个变量。例如console.log(a)中,引擎对于a的查找就是RHS查找,引擎的目的就是找到a变量是什么。RHS查询在所有的作用域中都找不到变量的话,引擎就会抛出ReferenceError异常。

让我们再看一下这段代码:

    function fool(a){//LHS
console.log(a) //RHS
}
fool(2) //RHS

接下来做个小练习:下面代码中有3处LHS查询和4处RHS查询

function foo(a){
var b=a
return a+b
}
var c=foo(2)

LHS和RHS作用的位置就是作用域,简单来说作用域就是程序定义变量的区域。

全局作用域、函数作用域、块级作用域

作用域最大的用处就是隔离变量。

全局作用域:定义的变量在哪里都能访问到,window 对象的内置属性都拥有全局作用域。在函数或者代码块{}外定义,不过,在函数或者代码块{}内没定义的变量也是拥有全局作用域的。

举个栗子:

var a=1  //定义在全局的变量
function foo(){
函数内可以调用外部的a变量
console.log(a) // 1
}

如果变量在函数内没有声明,这个变量会因为LHS查找机制变为全局变量。

function too(){
var a=2
b=1}
foo()
console.log(b) //1
console.log(a) //报错

函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用,对外是封闭的,从外层无法直接访问函数内部的作用域。

举个栗子:

function too(){
var a=2
}
foo()
console.log(a) //报错

如果想获取函数作用域内的变量需要借助return

function foo(){
var a=2
return a
}
foo()//2

块级作用域:块作用域指的是变量和函数不仅可以属于所处的作用域,还可以属于某个代码块内。(一般就是指{}内)

ES3中的try/catch也为块作用域,try/catch结构在catch分句子中具有块作用域。

try{
undefind() //执行一个非法操作来强行制作一个异常
}
catch(err){
console.log(err) //能够正常执行
}
console.log(err) //ReferenceError
err仅仅存在在catch分句的内部,当试图从别处引用它时会抛出错误

ES6中引入了let关键字用来在代码块中声明变量。就会存在这个代码块中{},{}外是无法访问到的。

{
let a=2
console.log(a)//2
}
console.log(a) //referenceError

作用域链

作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到就像上一级继续查找。当抵达最外层的全局作用域时无论找到还是没找到查找都会停止。

var a=1  
function foo1(){
var b=1
function foo2(){
var c=1 
console.log(a,b,c)//1,1,1
a变量是在全局作用域中查找
b是在父函数作用域中查找的
}
}

修改作用域

如果作用域是在写代码期间函数声明所在的位置决定的,那在运行的时候能否修改呢? 在JavaScript中有两种方法能够实现。 修改作用域会造成性能的下降,所以尽量不要修改作用域。

eval

eval ()函数可以在你写的代码中用程序生成代码并运行,这样eval()中的代码执行位置就是在eval()这个方法所在的位置。 举个栗子:

function foo(str,a){
eval(str)
console.log(a,b)//3,1
}
var b=2
foo('var b=3',1)  
在严格模式中eval()在运行时有自己的作用域,严格模式下的声明无法修改所在的作用域。

with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

举个栗子

var obj={
a:1,
b:1,
c:1
}

//引用时需要重复obj
obj.a=2;
obj.b=2
obj.c=2

//with方法
with(obj){
a=3;
b=4;
c=5;
}
function foo(obj){
with(obj){
a=2
}
}
var o1={
a:3
}
var o2={
b=5
}
foo(o1)
console.log(o1.a)//2

foo(o2)
console.log(o2.a)//undefind
console.log(a)//2

当o2作为作用域的时后,并没有找到a标识符因此进行了LHS查找,在非严格模式下,O2作用域。foo()的作用域和全局作用域中都有找到a,因此在当a=2执行时,自动创建了一个全局变量a。