JavaScript 学习笔记(6):作用域、闭包与函数进阶
JavaScript 学习笔记(6):作用域、闭包与函数进阶
woodfish学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。
- 理解作用域对程序执行的影响
- 能够分析程序执行的作用域范围
- 理解闭包本质,利用闭包创建隔离作用域
- 了解什么变量提升及函数提升
- 掌握箭头函数、解析剩余参数等简洁语法
作用域
了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
局部作用域
局部作用域分为函数作用域和块作用域。
函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
1 | <script> |
总结:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
块作用域
在 JavaScript 中使用 {}
包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。
1 | <script> |
JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
1 | <script> |
总结:
let
声明的变量会产生块作用域,var
不会产生块作用域const
声明的常量也会产生块作用域- 不同代码块之间的变量无法互相访问
- 推荐使用
let
或const
注:开发中 let
和 const
经常不加区分的使用,如果担心某个值会不小心被修改时,则只能使用 const
声明成常量。
全局作用域
<script>
标签和 .js
文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
1 | <script> |
全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:
1 | <script> |
总结:
- 为
window
对象动态添加的属性默认也是全局的,不推荐! - 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染
JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
作用域链
在解释什么是作用域链前先来看一段代码:
1 | <script> |
函数内部允许创建新的函数,f
函数内部创建的新函数 g
,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。
如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。
作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:
1 | <script> |
总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
闭包
闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:
1 | <body> |
总结:
1.怎么理解闭包?
- 闭包 = 内层函数 + 外层函数的变量
2.闭包的作用?
- 封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
3.闭包可能引起的问题?
- 内存泄漏
变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,
1 | <script> |
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefined
let
声明的变量不存在变量提升,推荐使用let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。
函数
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
1 | <script> |
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
默认值
1 | <script> |
总结:
- 声明函数时为形参赋值即为参数的默认值
- 如果参数未自定义默认值时,参数的默认值为
undefined
- 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
动态参数
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
1 | <script> |
总结:
arguments
是一个伪数组arguments
的作用是动态获取函数的实参
剩余参数
1 | <script> |
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参- 借助
...
获取的剩余实参,是个真数组
箭头函数
箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
1 | <body> |
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()
- 箭头函数函数体只有一行代码时可以省略花括号
{}
,并自动做为返回值被返回
箭头函数参数
箭头函数中没有 arguments
,只能使用 ...
动态获取实参
1 | <body> |
箭头函数 this
核心区别
普通函数和箭头函数的本质区别在于:
- 普通函数有自己的
this
,this
指向调用它的对象 - 箭头函数没有自己的
this
,它的this
继承自上层作用域
代码解析
全局作用域中的 this
1
console.log(this) // window
在全局作用域中,
this
指向 window 对象普通函数中的 this
1
2
3
4function fn() {
console.log(this) // window
}
window.fn()普通函数由 window 调用,所以
this
指向 window对象方法中的普通函数 this
1
2
3
4
5
6
7const obj = {
name: 'andy',
sayHi: function () {
console.log(this) // obj
}
}
obj.sayHi()函数由 obj 调用,所以
this
指向 obj箭头函数中的 this
1
2
3
4const fn = () => {
console.log(this) // window
}
fn()箭头函数没有自己的
this
,继承上层作用域(这里是全局作用域)的this
,即 window对象方法中的箭头函数 this
1
2
3
4
5
6
7const obj = {
uname: 'pink老师',
sayHi: () => {
console.log(this) // window
}
}
obj.sayHi()箭头函数的
this
继承自定义时的上层作用域(全局作用域),而非调用它的对象,所以是 window嵌套在普通函数中的箭头函数
1
2
3
4
5
6
7
8
9
10
11const obj = {
uname: 'pink老师',
sayHi: function () {
console.log(this) // obj
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()箭头函数
count
的this
继承自上层作用域(sayHi
函数的作用域),而sayHi
作为普通函数,它的this
指向 obj,所以count
的this
也是 obj
总结:箭头函数的 this
是在定义时确定的,而非调用时,它会捕获上层作用域的 this
并永久保持。这一特性使得箭头函数特别适合作为回调函数,避免了 this
指向混乱的问题。
解构赋值
知道解构的语法及分类,使用解构简洁语法快速为变量赋值。
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
1 | <script> |
总结:
- 赋值运算符
=
左侧的[]
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量 - 变量的顺序对应数组单元值的位置依次进行赋值操作
- 变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
- 变量的数量小于单元值数量时,可以通过
...
获取剩余单元值,但只能置于最末位 - 允许初始化变量的默认值,且只有单元值为
undefined
时默认值才会生效
注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:
1 | <script> |
总结:
- 赋值运算符
=
左侧的{}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量 - 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
undefined
- 允许初始化变量的默认值,属性不存在或单元值为
undefined
时默认值才会生效
注:支持多维解构赋值
1 | <body> |
综合案例
forEach遍历数组
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
注意:
1.forEach 主要是遍历数组
2.参数当前数组元素是必须要写的, 索引号可选。
3.0-base的索引
1 | <body> |
filter筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组
1 | <body> |
1. map ():转换数组元素
map()
方法创建一个新数组,其结果是该数组中的每个元素调用指定函数后的返回值。
核心作用:对数组中的每个元素进行处理(如转换、计算等),返回处理后的新数组,不改变原数组。
语法:
1 | array.map(function(currentValue, index, array) { |
currentValue
:当前元素(必选)index
:当前元素索引(可选)array
:原数组(可选)- 返回值:新数组,长度与原数组一致
示例:
1 | // 将数组中的数字乘以 2 |
注意:
map
必须有返回值,否则新数组会充满undefined
。- 适合场景:数据转换(如格式转换、计算新值)、提取属性等。
2. filter ():筛选数组元素(补充)
filter()
方法创建一个新数组,包含原数组中所有符合条件的元素。
核心作用:根据条件筛选元素,返回筛选后的新数组,不改变原数组。
语法:
1 | array.filter(function(currentValue, index, array) { |
示例:
1 | // 筛选出偶数 |
注意:
- 返回值是新数组,长度可能与原数组不同(取决于符合条件的元素数量)。
- 适合场景:数据筛选(如过滤无效值、按条件提取元素)。
3. reduce ():累加 / 聚合数组元素
reduce()
方法对数组中的每个元素执行回调函数,将其累加 / 聚合为一个单一的值(也可以是对象、数组等)。
核心作用:将数组 “缩减” 为一个值,如求和、求积、统计次数、合并对象等。
语法:
1 | array.reduce(function(accumulator, currentValue, index, array) { |
accumulator
:累计器(上一次回调的返回值)currentValue
:当前元素initialValue
:初始值(可选,若不提供则用数组第一个元素作为初始值,且从第二个元素开始遍历)
示例:
1 | // 数组求和 |
注意:
- 若不提供
initialValue
且数组为空,会报错;若数组只有一个元素,则直接返回该元素。 - 适合场景:累加计算、数据聚合、对象合并、数组扁平化等。
总结:四大方法对比
方法 | 核心作用 | 返回值 | 是否改变原数组 | 典型场景 |
---|---|---|---|---|
forEach | 遍历元素执行操作 | undefined | 不直接改变 | 简单遍历、打印日志 |
map | 转换元素 | 新数组(长度不变) | 不改变 | 数据格式转换、属性提取 |
filter | 筛选元素 | 新数组(符合条件元素) | 不改变 | 过滤数据、条件筛选 |
reduce | 累加 / 聚合元素 | 单一值(或复杂结构) | 不改变 | 求和、统计、合并数据 |
这些方法均为纯函数风格(不直接修改原数组,返回新数据),适合配合使用(如先 filter
筛选,再 map
转换,最后 reduce
聚合)。