重学Javascript(三)一文彻底理解JavaScript作用域

514 阅读7分钟

前言

从这篇文章开始,算是正式进入了重学JavaScript的正轨,作用域与作用域链是JavaScript的重要概念之一,深入理解作用域对理解JavaScript闭包,执行上下文等概念都有很大的帮助。

骚话没想好,直接开始正文吧

没想到

什么是作用域

作用域(scope)是指程序源代码中定义变量的区域,简单来说,一段程序代码中所用到的变量并不总是有效的,而限定这个变量的可用性的代码范围就是这个变量的作用域。

在JavaScript中使用的作用域是静态作用域(词法作用域),特点就是变量的作用域在变量定义时确定。下文所说的作用域都指代静态作用域。

全局作用域

全局作用域是最外围的一个作用域。根据 ECMAScript 实现所在的宿主环境不同,表示全局作用域的对象也不一样。在浏览器中,全局作用域就是window对象,node则是global对象。

拥有全局作用域的变量可以在所有作用域中被访问,假如将全局作用域比喻为中国,中国人这个属性拥有全局作用域,无论你是浙江人还是杭州人理所担任都可以说是中国人。

在JavaScript中一般有以下三种情形拥有全局作用域:

1.window(global)下的属性或者方法

global.nationality = 'Chinese'

function province () {
    var province = '浙江'
    console.log(nationality)
}

province()   //Chinese

2.最外层的变量或者函数

var country = '中国'

function province () {
    var province = '浙江'
    return province
}

function city () {
    var city = '杭州'
    console.log(country,province(),city)
}

city()   //中国 浙江 杭州

3.未定义直接赋值的变量

function province () {
    var province = '浙江'
    Country = '中国'
    return province
}

function city () {
    var city = '杭州'
    console.log(Country,province(),city)
}

city()  //中国 浙江 杭州

局部作用域

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的是函数内部。

函数作用域

定义在函数中的变量就处于函数作用域中。不同函数作用域中,变量不能相互访问。还是按照上面的例子举例,

如果你是浙江人,你可以说你是中国人,但是你不能说你是福建人。当然如果你是杭州人,你可以说自己是浙江人,因为浙江的函数作用域包含了杭州的函数作用域。

简单来说就是里面的可以访问外面的作用域,但是不能访问里层的和同级的作用域

块级作用域

JavaScript 本身是没有块级作用域,这就经常会导致理解上的困惑。

function province () {
    console.log(province)  //undefined
    var province = '浙江'
}

比如上述的代码,我希望打印出的其实是province函数本身,但是由于,下方声明了province变量,导致其province变量得到提升,等价于下面代码。

function province () {
  	var province
    console.log(province)  //undefined
    province = '浙江'
}

因此ES6 引入了块级作用域,让变量的生命周期更加可控,使用let和const声明的变量在指定块(简单理解就是一对花括号)的作用域外无法被访问。

但是这个并不能解决上面我的问题,因为let声明会导致暂时性死区,我并不能获取全局的province函数,而且还会报错。后面应该还会写一个ES6的系列,用于整理ES6的新特性。感兴趣的可以去搜索相关的文章,这边就不赘述了。

预编译

再讲作用域链之前,还是希望在这边讲一下JavaScript的预编译,理解这一部分,作用域链就如同脱光衣服的。。。

上面其实我们已经接触到一部分了,但是并不系统,在很久之前,我会用函数变量声明提升,函数申明的优先级优于变量申明,函数申明会带函数体一起提升等口诀来解决这些问题。但是随着学习的深入,这些口诀并不能准确有效的解决复杂场景下的问题,例如下面这个例子

function a(a){
  var a =a+1
  console.log(a)  // 2
}
var a
a(1)
console.log(a) // [Function: a]

遇到这种问题,最好的方法就是了解预编译,因为它的存在本就是用于解决执行顺序问题。

我们需要知道JavaScript在运行中会进行三个步骤,语法分析、预编译和解释执行

在执行完JavaScript的语法分析后,就会进入全局预编译环节

1.生成一个全局对象或者叫做全局执行上下文(我一直不习惯这么叫,觉得太拗口)所以下面就用GO(Global Object)来,GO只有一个,浏览器中的GO其实就是 window 对象 GO={}

2.将变量声明作为属性名挂载在GO上,值为undefined GO.a=undefined

3.找函数声明,并将函数名作为GO对象的属性名,值为函数体。GO.a=[Function: a]

如果执行过程中遇到函数,就会触发局部预编译,函数的局部预编译发生在函数执行前的一刻。 a(1)

1.生成一个活动对象或者叫做局部执行上下文,为了统一下面就用AO(Active Object)来表示,AO可以有无数个,每次调用函数都会创建一个新的AO. AO={}

2.找形参和变量声明,将变量和形参名作为AO的属性名,值同样为undefined. AO.a=undefined

3.将AO的形参的值改为实参值. AO.a=1

4.在该函数体里面找函数声明,值为函数体

彻底理解预编译,就可以明白不同执行阶段变量的值,无论再复杂的的题,对你来说都没有区别了。

easy

作用域链

消化吸收了之前的预编译之后,我们就可以去了解作用域链了,作用域链最泛用的场景就是函数,所以下面例子都是以函数为主体,话不多说,先看例子。

var z = 1 
function tim(){
   function cope(){
       var x= 2;
       y=3
   }
   var y = 4
   cope()
   console.log(y)  // 3
}

tim()

运行代码之后我们就可以发现输出为3,那么作为cope作用域中的值3是怎么在tim作用域中输出的呢?

答案就是作用域链。

每个JavaScript对象都有属性,有些可以被我们访问,有些却不行,这些属性仅供js引擎存取,而我们接下来要讲的[[scope]]就是这样的一个属性。

当函数被定义时它就被绑定了[[scope]]属性,而[[scope]]中存储的就是执行上下文的集合(GO | AO),其呈链式链接,我们称之为作用域链。让我们再回头来看之前的例子

var z = 1 
//1.tim 定义 tim.[[scope]] = GO:{z:1,tim:[Function: tim]}
function tim(){
  //3.cope定义 cope.[[scope]] = tim.[[scope]] = tim(AO):{cope:[Function: cope],y:undefined}=>GO
   function cope(){
       var x= 2;
     	 // 6.cope(AO)上没有y属性,就会沿着作用域链往上找,一直没有就会挂载在GO上
       y=3
   }
  //4.tim(AO):{cope:[Function: cope],y:4}
   var y = 4
   //5.cope.[[scope]] = cope(AO):{x:undefined}=>tim(AO)=>GO
   cope()
  //7.tim(AO):{cope:[Function: cope],y:4}
   console.log(y)  // 3
}
//2.tim 执行 tim.[[scope]] = tim(AO):{cope:[Function: cope],y:undefined}=>GO:{z:1,tim:[Function: tim]}
tim()

按照代码运行的顺序大致就是如此,不熟悉的可能需要三五分钟仔细消化一下这个流程。

简单总结一下

1.作用域链存储的就是执行上下文的集合

2.当前作用域中没有使用未在在作用域定义的变量时,会沿着作用域链向上找。

写在最后

文章写到这差不多就结束了,在语义表达和理解方面可能存在不少漏洞,如果存在问题希望各位大佬不吝指正。讲述完了作用域与作用域链,那接下来就不得不提以前很让我头疼的闭包了,不过有了这篇文章铺垫,我相信理解闭包的过程将变得十分愉快,所以下篇见咯。

参考

  1. Dmitry Soshnikov dmitrysoshnikov.com/ecmascript/…