JavaScript进阶-作用域、函数进阶和解构赋值

本章节为JavaScript进阶篇的第一章,主要讲解了作用域的详细知识,函数的进阶技巧,以及解构赋值这个JavaScript中非常实用的语法。

作用域

在基础篇章中,我们简单介绍了作用域的概念,在深入讲解之前,让我们先来复习一下作用域的相关基础知识。

作用域的分类

在JavaScript中,作用域分为全局作用域和局部作用域。

  • 全局作用域:在全局作用域中声明的变量可以在整个程序中的任意位置访问和修改。
  • 局部作用域:在局部作用域中声明的变量只能在当前作用域中访问和修改,不能在全局作用域中访问和修改。
    • 函数作用域:在函数内部声明的变量只能在当前函数内部访问和修改。
    • 块级作用域:在ES6中引入了块级作用域的概念,使用letconst声明的变量只能在当前代码块中访问和修改。代码块是由一对花括号{}包裹的代码块,例如if代码块、for代码块等。

作用域链

作用域链是JavaScript中用于查找变量的机制。当在当前作用域中找不到某个变量时,JavaScript引擎会沿着作用域链向上查找,直到找到该变量或者到达全局作用域为止。我们使用一段代码进行演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 全局作用域
const a = 1
const foo = function() { // foo函数作用域
const b = 2
{
// 块级作用域,只在这一对花括号中有效
const c = 3
}
const bar = function() { // bar函数作用域
const d = 4
console.log(a, b, c, d) // Error: c is not defined
}
bar()
}

foo()

上述代码中存在四个不同的作用域,他们对应的起始作用范围我已经以注释的形式标出,那么当我们调用函数foo()时,究竟发生了什么呢?让我们一步步分析:

  1. 首先,当foo()函数被调用,程序跳转到foo()函数定义处执行,在foo()函数中定义了变量b,紧接着的一个代码块中定义了变量c,然后定义了一个函数bar(),最后调用函数bar()
  2. bar()函数被调用,程序再次跳转到bar()函数定义处执行,在bar()函数中定义了变量d,并调用console.log()输出了程序中出现的4个变量。可是在bar()函数作用域中貌似只有变量d啊,那么程序应该从哪里知道剩下三个变量是什么值呢?
  3. 当程序在当前作用域中找不到需要的变量是,他就会到他的父级作用域中找是否有这个变量。在例子代码中,bar()函数作用域的父级作用域是,foo()函数作用域,在这里我们可以找到变量b
  • 这时候可能有人要问了:笔者笔者,为什么这里你没说可以找到变量c呀?拖出去打五十大板!(doge)其实仔细观察的读者应该可以发现,虽然变量c所处的块级作用域也属于foo()函数作用域范围内,但是这个块级作用域和bar()函数作用域并不构成父子关系,他们是同辈的,所以bar()函数作用域无法访问到这里的变量c
  1. 由于此时还有两个变量没有找到,所以程序继续向上查找,直到全局作用域,在全局作用域中找到了变量a,而没有找到变量c的定义,所以最终浏览器开发者工具的控制台会报错,提示没有变量c的定义。

函数进阶

在基础篇中,我们介绍了函数的基本语法,这一节我们继续深入讲解函数的进阶知识。

函数参数进阶

我们可能会遇到“我不知道这个函数需要具体接收几个参数”的情况,比如,我们现在需要编写一个函数,不论用户输入多少个参数,我们最后都返回用户输入的所有参数的和。那么我们该如何实现呢?这时候可以使用函数的动态参数。

动态参数
动态参数arguments是只存在于函数内部的伪数组,它会在所有函数的内部自动创建,用于表示所有传入的函数参数。

  • 为什么说arguments是伪数组?因为他只有一部分数组的能力,比如下标访问和.length属性,但它无法使用数组的常用方法,归根结底,是因为它的原型链上没有Array.prototype(这句话学习了后面的知识大家就可以明白了,现在大家心里可以先记住这个说法,后面就会恍然大悟的)
    现在我们使用arguments动态参数来实现我们之前的需求:
1
2
3
4
5
6
7
8
const sum = function() {
let total = 0
for (let i = 0; i < arguments.length; i++) {
total += arguments[i] // arguments[i]表示第i个参数
}
return total
}
console.log(sum(1, 2, 3, 4, 5)) // 15

剩余参数
现在我们有了一个新的需求:规定用户至少输入两个参数,对传入的参数进行求和。针对这个需求,使用arguments貌似无法完成,因为arguments无法约束用户至少传入两个参数。那么我们该如何实现呢?这时候可以使用ES6中的剩余参数语法。

1
2
3
4
5
6
7
8
9
10
const sum = function(num1, num2, ...nums){    // ...nums表示剩余参数,在函数内部可以通过nums数组的形式访问
let total = num1 + num2
for (let i = 0; i < nums.length; i++) {
total += nums[i]
}
return total
}
console.log(sum(1)) // NaN,因为用户只传入了一个参数,不符合至少两个参数的要求
console.log(sum(1, 2)) // 3
console.log(sum(1, 2, 3, 4, 5)) // 15

箭头函数

ES6标准中引入了一个新的函数定义方式:箭头函数。箭头函数的语法非常简单,基本定义方式如下:

1
2
3
const func = (param1, param2, ...otherParams) => {
// 函数体
}

当然,箭头函数如果只有这种方法,那也没有必要将其引入进来,它还有一些特殊情况下的简便写法:

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 如果只有一个参数,那么可以省略参数的括号
const func = param1 => {
// 函数体
}

// 2. 如果函数体只有一行代码,那么可以省略函数体的花括号和 return 关键字,此时的返回值为函数体代码的执行结果
const func = (a, b) => a + b
console.log(func(1, 2)) // 3

// 3. 如果函数体只有一行代码,并且是返回一个对象,那么需要将对象包裹在括号中,否则会报错(这样是为了区分对象定义语法的花括号和函数体的花括号)
const func = (a, b) => ({x: a, y: b})
console.log(func(1, 2)) // {x: 1, y: 2}

箭头函数和普通函数的区别

  • 箭头函数没有自己的arguments动态参数,但是依然可以使用剩余参数语法来获取传入的参数;
  • 每个函数内部都会有一个this对象,对于普通函数而言,如果没有显式规定this指向,那么谁调用了这个函数,this就指向谁;对于箭头函数而言,箭头函数没有自己的this对象,它只能使用所在作用域中的this对象。

解构赋值

解构赋值是ES6中引入的一种新的语法,它可以将一个对象或者数组中的数据解构出来,赋值给变量。
我们先来看数组解构赋值,示例将涵盖数组解构赋值的常见情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arr = [1, 2, 3]
// 1. 常见情况
const [a, b ,c] = arr
// 2. 变量多单元值少
const [a, b, c, d] = arr // a:1 b:2 c:3 d:undefined
// 3. 变量少单元值多
const [a, b] = arr // a:1 b:2
// 4. 剩余参数解决变量少单元值多
const [a, ...b] = arr // a:1 b:[2, 3]
// 5. 可规定默认值防止undefined传递
const [a = 0, b = 0] = [] // a:0 b:0
// 6. 按需导入赋值
const [a, ,c] = arr // a:1 c:3,这里跳过了b
// 7. 多维数组解构
const [a, b, [c, d]] = [1, 2, [3, 4]] // a:1 b:2 c:3 d:4

再来看对象解构赋值:

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
const obj = {
name: 'Tom',
age: 18,
sayHi: () => console.log(`${name} ${age}`)
}
// 1. 基本语法
const {name, age, sayHi} = obj
// 2. 解构时改名,可防止命名冲突
const {name: uname, age, sayHi} = obj
// 3. 数组对象的解构
const arrObj = [
{
uname: 'Tom',
age: 18
},
{
uname: 'John',
age: 20
}
]
const [{uanme, age}, {uname: _uname, age: _age}] = arrObj
// 4. 多级对象解构
const multiObj = {
uname: 'Tom'
age: 18
family: {
mother: 'Lura'
father: 'John'
}
}
const {uname, age, family: {mother, father}} = multiObj

总结

这一章我们正式进入到了JavaScript进阶技巧的学习,进阶知识可能一开始比较难理解,但是我相信通过不断地练习,大家一定可以熟练掌握这些技巧,成为一个优秀的程序员!下一章我们将探讨JavaScript中两个比较重要的概念:垃圾回收机制和闭包。这两个知识点与本章的作用于知识点息息相关,所以希望大家在阅读完这一章之后,一定要多加练习,加深理解!