JavaScript 学习笔记(6):作用域、闭包与函数进阶

学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。

  • 理解作用域对程序执行的影响
  • 能够分析程序执行的作用域范围
  • 理解闭包本质,利用闭包创建隔离作用域
  • 了解什么变量提升及函数提升
  • 掌握箭头函数、解析剩余参数等简洁语法

作用域

了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。

作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。

局部作用域

局部作用域分为函数作用域和块作用域。

函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

1
2
3
4
5
6
7
8
9
10
11
12
<script>
// 声明 counter 函数
function counter(x, y) {
// 函数内部声明的变量
const s = x + y
console.log(s) // 18
}
// 设用 counter 函数
counter(10, 8)
// 访问变量 s
console.log(s)// 报错
</script>

总结:

  1. 函数内部声明的变量,在函数外部无法被访问
  2. 函数的参数也是函数内部的局部变量
  3. 不同函数内部声明的变量无法互相访问
  4. 函数执行完毕后,函数内部的变量实际被清空了

块作用域

在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
{
// age 只能在该代码块中被访问
let age = 18;
console.log(age); // 正常
}

// 超出了 age 的作用域
console.log(age) // 报错

let flag = true;
if(flag) {
// str 只能在该代码块中被访问
let str = 'hello world!'
console.log(str); // 正常
}

// 超出了 age 的作用域
console.log(str); // 报错

for(let t = 1; t <= 6; t++) {
// t 只能在该代码块中被访问
console.log(t); // 正常
}

// 超出了 t 的作用域
console.log(t); // 报错
</script>

JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
// 必须要有值
const version = '1.0.0';

// 不能重新赋值
// version = '1.0.1';

// 常量值为对象类型
const user = {
name: '小明',
age: 18
}

// 不能重新赋值
user = {};

// 属性和方法允许被修改
user.name = '小小明';
user.gender = '男';
</script>

总结:

  1. let 声明的变量会产生块作用域,var 不会产生块作用域
  2. const 声明的常量也会产生块作用域
  3. 不同代码块之间的变量无法互相访问
  4. 推荐使用 letconst

注:开发中 letconst 经常不加区分的使用,如果担心某个值会不小心被修改时,则只能使用 const 声明成常量。

全局作用域

<script> 标签和 .js 文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。

1
2
3
4
5
6
7
8
9
<script>
// 此处是全局

function sayHi() {
// 此处为局部
}

// 此处为全局
</script>

全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
// 全局变量 name
const name = '小明'

// 函数作用域中访问全局
function sayHi() {
// 此处为局部
console.log('你好' + name)
}

// 全局变量 flag 和 x
const flag = true
let x = 10

// 块作用域中访问全局
if(flag) {
let y = 5
console.log(x + y) // x 是全局的
}
</script>

总结:

  1. window 对象动态添加的属性默认也是全局的,不推荐!
  2. 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
  3. 尽可能少的声明全局变量,防止全局变量被污染

JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。

作用域链

在解释什么是作用域链前先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
// 全局作用域
let a = 1
let b = 2
// 局部作用域
function f() {
let c
// 局部作用域
function g() {
let d = 'yo'
}
}
</script>

函数内部允许创建新的函数,f 函数内部创建的新函数 g,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。

如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。

作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
// 全局作用域
let a = 1
let b = 2

// 局部作用域
function f() {
let c
// let a = 10;
console.log(a) // 1 或 10
console.log(d) // 报错

// 局部作用域
function g() {
let d = 'yo'
// let b = 20;
console.log(b) // 2 或 20
}

// 调用 g 函数
g()
}

console.log(c) // 报错
console.log(d) // 报错

f();
</script>

总结:

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

闭包

闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<script>
// 1. 闭包 : 内层函数 + 外层函数变量
// function outer() {
// const a = 1
// function f() {
// console.log(a)
// }
// f()
// }
// outer()

// 2. 闭包的应用: 实现数据的私有。统计函数的调用次数
// let count = 1
// function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }

// 3. 闭包的写法 统计函数的调用次数
function outer() {
let count = 1
function fn() {
count++
console.log(`函数被调用${count}次`)
}
return fn
}
const re = outer()
// const re = function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
re()
re()
// const fn = function() { } 函数表达式
// 4. 闭包存在的问题: 可能会造成内存泄漏
</script>
</body>

总结:

1.怎么理解闭包?

  • 闭包 = 内层函数 + 外层函数的变量

2.闭包的作用?

  • 封闭数据,实现数据私有,外部也可以访问函数内部的变量
  • 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来

3.闭包可能引起的问题?

  • 内存泄漏

变量提升

变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,

1
2
3
4
5
6
7
<script>
// 访问变量 str
console.log(str + 'world!');

// 声明变量 str
var str = 'hello ';
</script>

总结:

  1. 变量在未声明即被访问时会报语法错误
  2. 变量在声明之前即被访问,变量的值为 undefined
  3. let 声明的变量不存在变量提升,推荐使用 let
  4. 变量提升出现在相同作用域当中
  5. 实际开发中推荐先声明再访问变量

注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料

函数

知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。

函数提升

函数提升与变量提升比较类似,是指函数在声明之前即可被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 调用函数
foo()
// 声明函数
function foo() {
console.log('声明之前即被调用...')
}

// 不存在提升现象
bar() // 错误
var bar = function () {
console.log('函数表达式不存在提升现象...')
}
</script>

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式不存在提升的现象
  3. 函数提升出现在相同作用域当中

函数参数

函数参数的使用细节,能够提升函数应用的灵活度。

默认值

1
2
3
4
5
6
7
8
9
10
<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
</script>

总结:

  1. 声明函数时为形参赋值即为参数的默认值
  2. 如果参数未自定义默认值时,参数的默认值为 undefined
  3. 调用函数时没有传入对应实参时,参数的默认值被当做实参传入

动态参数

arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 求生函数,计算所有参数的和
function sum() {
// console.log(arguments)
let s = 0
for(let i = 0; i < arguments.length; i++) {
s += arguments[i]
}
console.log(s)
}
// 调用求和函数
sum(5, 10)// 两个参数
sum(1, 2, 4) // 两个参数
</script>

总结:

  1. arguments 是一个伪数组
  2. arguments 的作用是动态获取函数的实参

剩余参数

1
2
3
4
5
6
7
8
<script>
function config(baseURL, ...other) {
console.log(baseURL) // 得到 'http://baidu.com'
console.log(other) // other 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
</script>

总结:

  1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
  2. 借助 ... 获取的剩余实参,是个真数组

箭头函数

箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<body>
<script>
// const fn = function () {
// console.log(123)
// }
// 1. 箭头函数 基本语法
// const fn = () => {
// console.log(123)
// }
// fn()
// const fn = (x) => {
// console.log(x)
// }
// fn(1)
// 2. 只有一个形参的时候,可以省略小括号
// const fn = x => {
// console.log(x)
// }
// fn(1)
// // 3. 只有一行代码的时候,我们可以省略大括号
// const fn = x => console.log(x)
// fn(1)
// 4. 只有一行代码的时候,可以省略return
// const fn = x => x + x
// console.log(fn(1))
// 5. 箭头函数可以直接返回一个对象
// const fn = (uname) => ({ uname: uname })
// console.log(fn('刘德华'))

</script>
</body>

总结:

  1. 箭头函数属于表达式函数,因此不存在函数提升
  2. 箭头函数只有一个参数时可以省略圆括号 ()
  3. 箭头函数函数体只有一行代码时可以省略花括号 {},并自动做为返回值被返回

箭头函数参数

箭头函数中没有 arguments,只能使用 ... 动态获取实参

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<script>
// 1. 利用箭头函数来求和
const getSum = (...arr) => {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
const result = getSum(2, 3, 4)
console.log(result) // 9
</script>

箭头函数 this

核心区别

普通函数和箭头函数的本质区别在于:

  • 普通函数有自己的 thisthis 指向调用它的对象
  • 箭头函数没有自己的 this,它的 this 继承自上层作用域
代码解析
  1. 全局作用域中的 this

    1
    console.log(this)  // window

    在全局作用域中,this 指向 window 对象

  2. 普通函数中的 this

    1
    2
    3
    4
    function fn() {
    console.log(this) // window
    }
    window.fn()

    普通函数由 window 调用,所以 this 指向 window

  3. 对象方法中的普通函数 this

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    name: 'andy',
    sayHi: function () {
    console.log(this) // obj
    }
    }
    obj.sayHi()

    函数由 obj 调用,所以 this 指向 obj

  4. 箭头函数中的 this

    1
    2
    3
    4
    const fn = () => {
    console.log(this) // window
    }
    fn()

    箭头函数没有自己的 this,继承上层作用域(这里是全局作用域)的 this,即 window

  5. 对象方法中的箭头函数 this

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    uname: 'pink老师',
    sayHi: () => {
    console.log(this) // window
    }
    }
    obj.sayHi()

    箭头函数的 this 继承自定义时的上层作用域(全局作用域),而非调用它的对象,所以是 window

  6. 嵌套在普通函数中的箭头函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const obj = {
    uname: 'pink老师',
    sayHi: function () {
    console.log(this) // obj
    const count = () => {
    console.log(this) // obj
    }
    count()
    }
    }
    obj.sayHi()

    箭头函数 countthis 继承自上层作用域(sayHi 函数的作用域),而 sayHi 作为普通函数,它的 this 指向 obj,所以 countthis 也是 obj

总结:箭头函数的 this 是在定义时确定的,而非调用时,它会捕获上层作用域的 this 并永久保持。这一特性使得箭头函数特别适合作为回调函数,避免了 this 指向混乱的问题。

解构赋值

知道解构的语法及分类,使用解构简洁语法快速为变量赋值。

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。

数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:

1
2
3
4
5
6
7
8
9
10
<script>
// 普通的数组
let arr = [1, 2, 3]
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
let [a, b, c] = arr
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
</script>

总结:

  1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作
  3. 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
  4. 变量的数量小于单元值数量时,可以通过 ... 获取剩余单元值,但只能置于最末位
  5. 允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效

注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析

对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user

console.log(name) // 小明
console.log(age) // 18
</script>

总结:

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 对象中找不到与变量名一致的属性时变量值为 undefined
  4. 允许初始化变量的默认值,属性不存在或单元值为 undefined 时默认值才会生效

注:支持多维解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<body>
<script>
// 1. 这是后台传递过来的数据
const msg = {
"code": 200,
"msg": "获取新闻列表成功",
"data": [
{
"id": 1,
"title": "5G商用自己,三大运用商收入下降",
"count": 58
},
{
"id": 2,
"title": "国际媒体头条速览",
"count": 56
},
{
"id": 3,
"title": "乌克兰和俄罗斯持续冲突",
"count": 1669
},

]
}

// 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面
// const { data } = msg
// console.log(data)
// 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
// const { data } = msg
// msg 虽然很多属性,但是我们利用解构只要 data值
function render({ data }) {
// const { data } = arr
// 我们只要 data 数据
// 内部处理
console.log(data)

}
render(msg)

// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
function render({ data: myData }) {
// 要求将 获取过来的 data数据 更名为 myData
// 内部处理
console.log(myData)

}
render(msg)

</script>

综合案例

forEach遍历数组

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数

注意:

1.forEach 主要是遍历数组

2.参数当前数组元素是必须要写的, 索引号可选。

3.0-base的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<body>
<script>
// forEach 就是遍历 加强版的for循环 适合于遍历数组对象
const arr = ['red', 'green', 'pink']
const result = arr.forEach(function (item, index) {
console.log(item) // 数组元素 red green pink
console.log(index) // 索引号
})
// console.log(result)




// 数组元素是基本类型(数字)
const numbers = [1, 2, 3];
numbers.forEach(num => {
num = num * 2; // 修改的是副本,原数组不变
});
console.log(numbers); // [1, 2, 3]

// 数组元素是引用类型(对象)
const numberObjects = [{ value: 1 }, { value: 2 }];
numberObjects.forEach(obj => {
obj.value = obj.value * 2; // 通过引用修改原对象,原数组会变
});
console.log(numberObjects); // [{ value: 2 }, { value: 4 }]
</script>
</body>

filter筛选数组

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<script>
const arr = [10, 20, 30]
// const newArr = arr.filter(function (item, index) {
// // console.log(item)
// // console.log(index)
// return item >= 20
// })
// 返回的符合条件的新数组

const newArr = arr.filter(item => item >= 20)
console.log(newArr)
</script>
</body>

1. map ():转换数组元素

map() 方法创建一个新数组,其结果是该数组中的每个元素调用指定函数后的返回值。
核心作用:对数组中的每个元素进行处理(如转换、计算等),返回处理后的新数组,不改变原数组。

语法

1
2
3
array.map(function(currentValue, index, array) {
// 处理逻辑,返回新值
}, thisArg);
  • currentValue:当前元素(必选)
  • index:当前元素索引(可选)
  • array:原数组(可选)
  • 返回值:新数组,长度与原数组一致

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将数组中的数字乘以 2
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]
console.log(numbers); // [1, 2, 3, 4](原数组不变)

// 将对象数组中的某个属性提取为新数组
const users = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 25 }
];
const names = users.map(user => user.name);
console.log(names); // ['Alice', 'Bob']

注意

  • map 必须有返回值,否则新数组会充满 undefined
  • 适合场景:数据转换(如格式转换、计算新值)、提取属性等。

2. filter ():筛选数组元素(补充)

filter() 方法创建一个新数组,包含原数组中所有符合条件的元素。
核心作用:根据条件筛选元素,返回筛选后的新数组,不改变原数组。

语法

1
2
3
array.filter(function(currentValue, index, array) {
// 返回布尔值,true 则保留元素
}, thisArg);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 筛选出偶数
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6]

// 筛选出年龄大于 18 的用户
const users = [
{ name: 'Alice', age: 17 },
{ name: 'Bob', age: 20 },
{ name: 'Charlie', age: 25 }
];
const adults = users.filter(user => user.age > 18);
console.log(adults); // [{ name: 'Bob', age: 20 }, { name: 'Charlie', age: 25 }]

注意

  • 返回值是新数组,长度可能与原数组不同(取决于符合条件的元素数量)。
  • 适合场景:数据筛选(如过滤无效值、按条件提取元素)。

3. reduce ():累加 / 聚合数组元素

reduce() 方法对数组中的每个元素执行回调函数,将其累加 / 聚合为一个单一的值(也可以是对象、数组等)。
核心作用:将数组 “缩减” 为一个值,如求和、求积、统计次数、合并对象等。

语法

1
2
3
array.reduce(function(accumulator, currentValue, index, array) {
// 处理逻辑,返回累计结果
}, initialValue);
  • accumulator:累计器(上一次回调的返回值)
  • currentValue:当前元素
  • initialValue:初始值(可选,若不提供则用数组第一个元素作为初始值,且从第二个元素开始遍历)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 数组求和
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 10

// 统计元素出现次数
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana'];
const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {}); // 初始值为一个空对象
console.log(count); // { apple: 2, banana: 2, orange: 1 }

// 合并对象数组的属性
const users = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 25 }
];
const merged = users.reduce((acc, user) => {
acc.names.push(user.name);
acc.totalAge += user.age;
return acc;
}, { names: [], totalAge: 0 }); // 初始值为包含两个属性的对象
console.log(merged); // { names: ['Alice', 'Bob'], totalAge: 45 }

注意

  • 若不提供 initialValue 且数组为空,会报错;若数组只有一个元素,则直接返回该元素。
  • 适合场景:累加计算、数据聚合、对象合并、数组扁平化等。

总结:四大方法对比

方法 核心作用 返回值 是否改变原数组 典型场景
forEach 遍历元素执行操作 undefined 不直接改变 简单遍历、打印日志
map 转换元素 新数组(长度不变) 不改变 数据格式转换、属性提取
filter 筛选元素 新数组(符合条件元素) 不改变 过滤数据、条件筛选
reduce 累加 / 聚合元素 单一值(或复杂结构) 不改变 求和、统计、合并数据

这些方法均为纯函数风格(不直接修改原数组,返回新数据),适合配合使用(如先 filter 筛选,再 map 转换,最后 reduce 聚合)。