【JavaScript】有趣的作用域和提升

1,799 阅读9分钟

前言

最近复习到了作用域这块的内容,打算归纳总结一下,加入自己的理解和尝试,更好的理解作用域和提升相关的知识点。

了解一下作用域

1. 什么是作用域

按照我的理解,在程序的运行中,需要获取和存储变量的值,并且在某些情况下需要获取这些值包括状态,那么这些值和状态的集合就可以称之为作用域。

2.作用域有哪些

从行为上分,作用域其实可以分为词法作用域动态作用域,我们假设JS分别使用两种作用域的表现有什么不同:

  • 词法作用域
var a = 3;
function test() {
    console.log(a); //从这里开始查找
}
function test2() {
    var a = 1;
    test();
}
test2(); //3

在词法作用域下,作用域的范围是静态的,由作用域声明的地方来决定。

这里test运行时,会以test函数声明的地方为起点,扩散寻找a变量,因此找到了a的值为3,关于作用域的查找规则我们下面会说到。

  • 动态作用域
var a = 3;
function test() {
    console.log(a);
}
function test2() {
    var a = 1;
    test(); //从这里开始查找
}
test2(); //1

在动态作用域下,作用域的范围是动态的,由具体调用的位置来决定。

在动态作用域下,会以test执行时的位置开始查找,和JS中的this规则非常接近。

讲了两种作用域的区别,那么JS属于哪种作用域?好叭好叭,机智的大家都知道了,JavaScript采用的就是词法作用域,我们下面详细讲解JavaScript中的作用域。

JavaScript中的作用域

上文我们讲到,js采用的是词法作用域,那么具体又分成哪几种类型呢?

我们可以分为这几种:全局作用域函数作用域块级作用域(ES6)

1. 全局作用域

全局作用域在创建时就会生成,关闭时则会销毁,属于作用域的最外层或者说最顶层。直接编写在js文件或者script标签中的代码都属于全局作用域,和window对象在同一层。

全局作用域还有以下特点:

  • 在全局作用域中声明变量,该变量会自动成为window对象的属性
  • 在全局作用域中声明函数,该函数会自动成为window对象的方法

举个简单的例子:

var a=1;
function test(){
	console.log(2);
}

window.a;	//1
window.test();  //2

2. 函数作用域

在JavaScript中声明一个函数,会创建一个属于函数本身的作用域集合。在函数中声明的变量,无法从外部访问到,而当函数执行结束以后,这个作用域集合会被释放掉。

举个简单的例子:

function test(){
	var a=3;
}
console.log(a); //Uncaught ReferenceError: a is not defined

3. 块级作用域

很多编程语言都支持块级作用域的概念,这也意味着在使用iffor时会创建出独立的作用域,但JavaScript在ES6之前的语法是没有块级作用域的概念的,这就会导致这样的情况:

var a=2;
if(a){
	var b=a;
}
b //2

if语句中声明的b变量,即使在语句之外也可以访问到。但在ES6之后有了let关键字,就变成了这样:

var a=2;
if(a){
	let b=a;  //var变成了let
}
b //Uncaught ReferenceError: b is not defined

可以看到,使用了let关键字后,if语句也有了和函数一样的独立作用域。

作用域的查找规则

我们刚刚在讲述不同的作用域时,可能会有些疑惑:

  • 作用域的外部和内部是什么意思?
  • 为什么外部的作用域没法访问内部的作用域?
  • 内部为什么又能拿到外部的变量?

这里就要讲到作用域的查找规则了,我们废话不多说,先请出我们的示例图:

我们执行这段代码后会发现,最后输出的内容是"我是outer作用域的name"

我们来看看这个过程发生了什么,首先看看输出name的地方:

function inner(){
	console.log(name);
}

我们之前说过,函数具有独立的作用域。我们这里要输出name,需要获取name的值,因此JS引擎会先去变量所在的作用域查找对应的值。

显然,这里name的作用域就是inner函数的作用域,而这里并没有定义name变量,那怎么办呢?

这里就要提到作用域的一个特点了:作用域嵌套

当一个块或函数嵌套在另一个块或者函数中时,就放生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外部嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

根据我们的示例图可以看出来,我们这里一共有三个作用域嵌套在一起,当引擎无法在inner的作用域中查询到name变量时,引擎会继续向他的外层作用域查找

到了inner作用域的外层outer的作用域时,在这里我们找到了一个声明的name我是outer作用域的name,于是乎就带着这个值心满意足的回去了,最后输出了这个值。要注意的是,由于已经在内部的作用域找到变量对应的值了,因此外部的同名变量的值就不会被访问到了,这就是**"遮蔽效应"**。

提升

1. 变量提升

跟作用域息息相关的一个概念就是提升,我们先举个简单的小例子来看看它长什么模样:

a=2;
var a;

看着很别扭的代码,按照代码的书写顺序的话,应该是先为a赋值2,但此时应该并不存在a变量,事实上,这是由于js预编译导致的,可以这么概括预编译做的事情:

  • 找到所有的声明,并提升到所在作用域的顶部

因此,刚刚的代码实际上在执行时的顺序就是这样的:

var a;
a=2;

需要注意的是,预编译时只会把变量声明提前,而变量的赋值还是会按照原来的顺序执行,如:

console.log(a);//undefined
var a=2;

这段代码输出的aundefined,说明在执行console.log(a)时,a已经声明了,否则就会抛出ReferenceError了。这也说明了变量的声明确实提前了,但赋值并没有提前,这段代码实际运行的样子是这样的:

var a;
console.log(a);//undefined
a=2;

这样就清晰多了。

2. 函数提升

除了刚刚举例的变量提升,其实函数的声明也是存在提升的,我们看个例子:

f()	//test
function f(){
	console.log('test');
}

可以看到,f()语句在声明之前,但最后成功输出了"test",说明函数的声明被提前了。但需要注意的是,**通过函数表达式声明函数并不会提升。**我们看个例子:

f()	//Uncaught TypeError: f is not a function
var f = function(){
	console.log('test');
}

这个例子中,通过表达式的方式声明了函数f,在执行f()语句时报错,可以看出这里没有发生函数提升。其实函数表达式声明函数,可以看做是声明一个变量,这样就好理解了,也就是这样:

var f
f()	//Uncaught TypeError: f is not a function
f = function(){
	console.log('test');
}

因为按照我们的说法,变量的声明会被提前,而赋值不会,所以就有了现在的结果。

3. 变量提升和函数提升

了解了变量提升和函数提升,我们做个尝试,当我们试图声明同名的函数和变量时会发生什么?

console.log(a);  //ƒ a(){}
var a=2;
function a(){}

可以看到,最终输出a的结果是一个函数而不是undefined,这不仅说明函数提升的优先级大于变量提升,而且从a的值是个函数也可以看出,函数的声明和赋值其实是一个整体过程,而不是变量提升的只提升声明,不提升赋值。所以刚刚这段代码的真面目就是这样:

function a(){}
var a;
console.log(a);  //ƒ a(){}
a=2;

所以如果在刚刚的代码中再加入一句输出a的语句,就会发现a的值会被2覆盖掉。

console.log(a);  //ƒ a(){}
var a=2;
function a(){};
console.log(a);  //2

补充提高

刚刚说的算是普通情况,我们再来看一个特殊情况下的例子:

if(function f(){}){
    console.log(f); //Uncaught ReferenceError: f is not defined
}

为什么在if语句中声明了函数f,但输出的时候却提示未定义?其实是因为,当在表达式中声明函数时,它不会视作函数声明,还是会作为表达式进行评估,评估大致做了这么些事情:

创建一个新的环境上下文,在这个环境中声明这个函数,然后返回这个函数对象。如果返回的对象没有被变量储存的话,这个新的环境上下文会失效,释放f函数对象。

这里的if()中的函数声明没有用变量存储,所以当执行到console.log(f)时,f已经被释放掉了,所以会报错。那我们试试用变量存储起来:

if(f=function(){}){
    console.log(f);  //ƒ (){}
}

完美收工!

总结

本文介绍了作用域的概念和JavaScript中的作用域、以及在作用域下的提升规则,并结合两者看了几个非(sang)常(xin)有(bing)趣(kuang)的例子,收获满满~

写在最后

都看到这里了,如果觉得对你有帮助的话不妨点个赞关注支持一下呗~

以后会陆续更新更多文章和知识点,感兴趣的话可以关注一波~

如果哪里有错误的地方或者描述不准确的地方,也欢迎大家指出交流~