Web/04-JavaScript基础/21-作用域&变量提升&函数提升.md
2022-07-24 23:22:23 +08:00

285 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 21-作用域&变量提升&函数提升
publish: true
---
<ArticleTopAd></ArticleTopAd>
> 作用域、变量提升、函数提升的知识点,面试时会经常遇到。
## 作用域
### 作用域Scope的概念和分类
- **概念**:通俗来讲,作用域是一个变量或函数的作用范围。作用域在**函数定义**时,就已经确定了。
- **目的**:为了提高程序的可靠性,同时减少命名冲突。
在 JS 中一共有两种作用域ES5 中)
- **全局作用域**:作用于整个 script 标签内部,或者作用于一个独立的 JS 文件。
- **函数作用域**(局部作用域):作用于函数内的代码环境。
### 全局作用域 和 window 对象
直接编写在 script 标签中的 JS 代码,都在全局作用域。全局作用域在页面打开时创建,在页面关闭时销毁。
在全局作用域中有一个全局对象 window它代表的是一个浏览器的窗口由浏览器创建我们可以直接使用。相关知识点如下
- 创建的**变量**都会作为 window 对象的属性保存。比如在全局作用域内写 `var a = 100`,这里的 `a` 等价于 `window.a`
- 创建的**函数**都会作为 window 对象的方法保存。
### 作用域的访问关系
在内部作用域中可以访问到外部作用域的变量,在外部作用域中无法访问到内部作用域的变量。
代码举例:
```javascript
var a = 'aaa';
function foo() {
var b = 'bbb';
console.log(a); // 打印结果aaa。说明 内层作用域 可以访问 外层作用域 里的变量
}
foo();
console.log(b); // 报错Uncaught ReferenceError: b is not defined。说明 外层作用域 无法访问 内层作用域 里的变量
```
### 变量的作用域
根据作用域的不同,变量可以分为两类:全局变量、局部变量。
**全局变量**
- 在全局作用域下声明的变量,叫「全局变量」。在全局作用域的任何一地方,都可以访问这个变量。
- 在全局作用域下,使用 var 声明的变量是全局变量。
- 特殊情况:在函数内不使用 var 声明的变量也是全局变量(不建议这么用)。
**局部变量**
- 定义在函数作用域的变量,叫「局部变量」。仅限函数内部访问这个变量。
- 在函数内部,使用 var 声明的变量是局部变量。
- 函数的**形参**也是属于局部变量。
从执行效率来看全局变量和局部变量:
- 全局变量:只有浏览器关闭时才会被销毁,比较占内存。
- 局部变量:当其所在的代码块运行结束后,就会被销毁,比较节约内存空间。
### 作用域的上下级关系
当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(**就近原则**)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错 ReferenceError。
在函数中要访问全局变量可以使用 window 对象。(比如说,全局作用域和函数作用域都定义了变量 a如果想访问全局变量可以使用`window.a`
## 全局作用域的预处理
**预处理(预解析)**的概念JS在解析代码之前有一个“预处理预解析”阶段将当前 JS 代码中所有变量的定义和函数的定义,放到所有代码的最前面。
(打个比方,学生在学习文言文之前,会扫读整篇文章,做简单的预习。)
这种预解析,也称之为声明提前。
### 变量的声明提前(变量提升)
使用 var 关键字声明的变量( 比如 `var a = 1`**会在所有的代码执行之前被声明**(但是不会赋值)。但是如果声明变量时不是用 var 关键字(比如直接写`a = 1`),则变量不会被声明提前。
**举例 1**
```javascript
console.log(a);
var a = 123;
```
打印结果undefined。注意打印结果并没有报错而是 undefined说明变量 a 被提前声明了,只是尚未被赋值。
**举例 2**
```javascript
console.log(a);
a = 123; //此时a相当于window.a
```
程序会报错:`Uncaught ReferenceError: a is not defined`。
**举例 3**
```javascript
a = 123; //此时a相当于window.a
console.log(a);
```
打印结果123。
**举例 4**
```javascript
foo();
function foo() {
if (false) {
var i = 123;
}
console.log(i);
}
```
打印结果undefined。注意打印结果并没有报错而是 undefined。这个例子再次说明了变量 i 在函数执行前,就被提前声明了,只是尚未被赋值。
例 4 中, `if(false)`里面的代码虽然不会被执行,但是整个代码有**解析**的环节,解析的时候就已经把 变量 i 给提前声明了。
**总结**
既然 JS 中存在变量提升的现象,那么,在实战开发中,为了避免出错,建议先声明一个变量,然后再使用这个变量。
### 函数的声明提前(函数提升)
**函数声明**
使用`函数声明`的形式创建的函数`function foo(){}`**会被声明提前**。
也就是说,整个函数会在所有的代码执行之前就被**创建完成**。所以,在代码顺序上,我们可以先调用函数,再定义函数。
代码举例:
```javascript
fn1(); // 虽然 函数 fn1 的定义是在后面,但是因为被提前声明了, 所以此处可以调用函数
function fn1() {
console.log('我是函数 fn1');
}
```
**函数表达式**
使用`函数表达式`创建的函数`const foo = function(){}`**不会被声明提前**,所以不能在声明前调用。
很好理解,因为此时只是变量 foo 被提升了,且值为 undefined并没有把 `function(){}` 赋值给 foo。
所以,下面的例子会报错:
```js
// 不会报错,可以正常执行函数,正常打印结果
fun1();
// 此时 fun2 相当于 undefined。执行时会报错Uncaught ReferenceError: Cannot access 'fun2' before initialization
fun2();
// 函数声明,会被提前声明
function fun1() {
console.log('我是 fun1 函数');
}
// 函数表达式,不会被提前声明
const fun2 = function () {
console.log('我是 fun12 函数');
};
```
## 函数作用域的预处理
上一段讲的是全局作用域中的声明提前。在函数作用域中,也有声明提前的现象:
- 函数中,使用 var 关键字声明的变量,会在函数中所有代码执行之前被提前声明。
- 函数中,没有 var 声明的变量都是**全局变量**,且并不会被提前声明。
举例:
```javascript
var a = 1;
function foo() {
console.log(a);
a = 2; // 此处的a相当于window.a
}
foo();
console.log(a); //打印结果是2
```
上方代码中,执行 foo() 后,函数里面的打印结果是`1`。如果去掉第一行代码,执行 foo() 后,函数里面的打印结果是`Uncaught ReferenceError: a is not defined`。
**补充**:定义形参就相当于在函数作用域中声明了变量。举例如下:
```javascript
function fun(e) {
// 这个函数中,因为有了形参 e此时相当于在函数内部的第一行代码里写了 var e;
console.log(e);
}
fun(); //打印结果为 undefined
fun(123); //打印结果为123
```
## JavaScript 没有块级作用域ES6 之前)
在其他编程语言中(如 Java、C#等),存在块级作用域,由`{}`包括起来。比如在 Java 语言中if 语句里创建的变量,只能在 if 语句内部使用:
```java
if(true){
int num = 123;
system.out.print(num); // 123
}
system.out.print(num); // 报错
```
但是,在 JS 中没有块级作用域ES6 之前)。举例如下:
```javascript
if (true) {
var num = 123;
console.log(num); //123
}
console.log(num); //123可以正常打印
```
## 作用域链
引入:
- 只要是代码,就至少有一个作用域
- 写在函数内部的局部作用域
- 如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
基于上面几条内容,我们可以得出作用域链的概念。
**作用域链**:内部函数访问外部函数的变量,采用的是链式查找的方式来决定取哪个值,这种结构称之为作用域链。查找时,采用的是**就近原则**。
代码举例:
```javascript
var num = 10;
function fn() {
// 外部函数
var num = 20;
function fun() {
// 内部函数
console.log(num);
}
fun();
}
fn();
```
打印结果20。
## 赞赏作者
创作不易,你的赞赏和认可,是我更新的最大动力:
![](https://img.smyhvae.com/20220401_1800.jpg)