一、什么是作用域
作用域指一个变量的作用的范围。 简单来说,作用域的作用是存放变量的值,并能在之后对这个值进行访问和修改。作用域的使用可以提高程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。
在JavaScript中有两种作用域类型:
(1)全局作用域: 全局作用域是最外层的作用域,在浏览器中,全局作用域被认为是window对象
(2)函数作用域(局部作用域): 函数作用域是函数被调用时创建的作用域,函数执行完毕后,函数作用域就会被销毁
(3)块级作用域(es6新增): 块级作用域是由{}包括的作用域,在es6中,let和const命令都可以创建块级作用域
二、作用域的巧妙使用
接下来我们通过代码,边学边理解
示例1
js
var a = 10;
function fn()
{
console.log(a);
var a = 20;
}
fn();
1. 代码中的作用域划分
javascript
php
var a = 10; // 全局作用域变量 a
function fn() { // 函数作用域开始
console.log(a); // 访问当前作用域的 a(变量提升但未赋值)
var a = 20; // 声明并赋值函数作用域变量 a
} // 函数作用域结束
fn(); // 调用函数
2. 全局作用域(Global Scope)
- 变量
a
:在全局作用域中声明并初始化为10
。 - 函数
fn
:在全局作用域中定义,但函数内部的代码属于独立的函数作用域。
3. 函数作用域(Function Scope)
- 变量提升(Hoisting) :
var a
声明被提升到函数作用域的顶部,但赋值保留在原处。
因此,函数内部的变量a
在整个函数作用域内都存在,但在赋值前值为undefined
。 - 作用域屏蔽(Shadowing) :
函数内部声明的a
与全局变量a
同名但不同源,它会屏蔽(Shadow)全局变量的访问。
4. 执行流程详解
-
全局作用域初始化:
- 创建变量
a
并赋值为10
。 - 定义函数
fn
,其内部代码形成独立作用域。
- 创建变量
-
调用
fn()
:- 进入函数作用域,变量
a
被提升(值为undefined
)。 console.log(a)
:访问当前作用域的a
(undefined
)。a = 20
:将函数内部的a
赋值为20
,不影响全局变量。
- 进入函数作用域,变量
-
函数执行结束:
- 函数作用域销毁,全局变量
a
仍为10
。
- 函数作用域销毁,全局变量
5. 输出结果
plaintext
javascript
undefined
6. 关键概念总结
概念 | 解释 |
---|---|
全局作用域 | 代码最外层的作用域,变量和函数可被全局访问。 |
函数作用域 | 每个函数创建独立作用域,内部变量无法被外部访问。 |
变量提升 | var 声明的变量会被提升到当前作用域顶部,但赋值不会提升。 |
作用域屏蔽 | 函数内部声明的变量会屏蔽同名的全局变量(若同名)。 |
7. 对比实验
实验 1:移除函数内部的 var
javascript
ini
var a = 10;
function fn() {
console.log(a); // 访问全局变量 a(值为 10)
a = 20; // 修改全局变量 a
}
fn();
console.log(a); // 输出 20(全局变量被修改)
实验 2:使用 let
/const
声明
javascript
ini
var a = 10;
function fn() {
console.log(a); // 报错:Cannot access 'a' before initialization(暂时性死区)
let a = 20; // let 声明的变量不提升赋值前不可用
}
fn();
总结
- 全局作用域:变量和函数可被全局访问,但易被污染。
- 函数作用域 :通过
var
声明的变量具有函数作用域,内部变量会屏蔽同名全局变量。 - 变量提升 :导致函数内部的
var
变量在声明前已存在(值为undefined
)。 - 最佳实践 :优先使用
let
/const
并避免变量命名冲突,减少对全局作用域的依赖。
示例二:
第一步:定义对象和函数
javascript
javascript
var o1 = { a: 1 }; // 全局作用域:创建对象 o1,包含属性 a=1
var o2 = { b: 1 }; // 全局作用域:创建对象 o2,包含属性 b=1
function foo(obj) { // 全局作用域:定义函数 foo
with(obj) { // 将 obj 的属性添加到作用域链前端
a = 2; // 赋值操作:尝试在当前作用域链中查找变量 a
}
}
第二步:调用 foo(o1)
-
进入
with
块 :obj
是o1
(包含a: 1
)。 -
执行
a = 2
:with
块的作用域链前端是o1
,其中存在属性a
。- 赋值操作直接修改
o1.a
,值变为2
。
-
输出
o1
:javascript
css{ a: 2 } // o1.a 被修改
第三步:调用 foo(o2)
-
进入
with
块 :obj
是o2
(包含b: 1
)。 -
执行
a = 2
:with
块的作用域链前端是o2
,其中没有 属性a
。- JavaScript 继续在全局作用域查找
a
,未找到。 - 隐式全局变量 :由于未使用
var/let/const
声明,a = 2
在全局作用域创建变量a
(值为2
)。
-
输出
o2
:javascript
css{ b: 1 } // o2 未被修改
第四步:输出全局变量 a
javascript
arduino
2 // 由 foo(o2) 隐式创建的全局变量
2. 关键知识点
with
语句的作用
- 将对象的属性添加到作用域链的前端,允许直接访问对象属性而无需
obj.
前缀。 - 危险特性:可能导致变量查找路径模糊,增加代码复杂性。
变量赋值规则
- 优先修改现有变量 :
若作用域链中存在变量a
(如o1.a
),则直接修改。 - 隐式全局变量 :
若作用域链中不存在a
,则在全局作用域创建新变量(非严格模式下)。
3. 输出结果
javascript
css
{ a: 2 } // o1 被修改
{ b: 1 } // o2 未被修改
2 // 全局变量 a 被隐式创建
4. 严格模式下的差异
若代码在严格模式('use strict';
)下执行:
- 错误 :
foo(o2)
中a = 2
会抛出ReferenceError
,因为无法隐式创建全局变量。 - 安全性:严格模式禁止隐式全局变量,强制声明变量。
5. 最佳实践
避免使用 with
语句:
-
现代 JavaScript 已不推荐使用
with
,因其会导致作用域链模糊,增加调试难度。 -
改用显式对象属性访问(如
obj.a = 2
)。
示例改写:
javascript
ini
function foo(obj) {
obj.a = 2; // 直接修改对象属性,无需 with
}
示例三:
1. 代码片段 1:块级作用域与 let
javascript
javascript
if (true) {
let a = 10; // 块级作用域变量(仅在 if 内部可见)
console.log(a); // 输出 10(访问块内变量)
}
console.log(a); // 报错:ReferenceError: a is not defined
关键点:
- 块级作用域 :
let
/const
声明的变量仅在声明所在的块({}
)内有效。 - 暂时性死区(TDZ) :块内的
let a
会屏蔽外部同名变量,且变量在声明前不可用。
2. 代码片段 2:变量提升与 var
javascript
ini
var a = 1;
if (true) {
console.log(a); // 输出 undefined(变量提升,但未赋值)
var a = 10; // 变量声明被提升到函数/全局作用域顶部
}
执行流程:
-
变量提升 :
var a
被提升到全局作用域顶部,但赋值保留在原处。javascript
inivar a; // 提升声明(值为 undefined) a = 1; // 初始赋值 if (true) { console.log(a); // 访问当前作用域的 a(undefined) a = 10; // 修改当前作用域的 a }
-
输出结果:
plaintext
javascriptundefined
3. 对比实验:混合 let
和 var
javascript
ini
let a = 1;
if (true) {
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 10; // 块内的 let a 屏蔽外部变量,形成 TDZ
}
错误原因:
- 块内的
let a
屏蔽了外部的a
,但let
不存在变量提升,导致访问时处于 TDZ。
4. 关键概念总结
特性 | var |
let /const |
---|---|---|
作用域范围 | 函数作用域 | 块级作用域({} 内有效) |
变量提升 | 声明会提升,值为 undefined |
不存在提升(TDZ 限制访问) |
重复声明 | 允许(后声明覆盖前声明) | 不允许(SyntaxError) |
全局变量绑定 | 成为 window 属性 |
不绑定 |