JavaScript 进阶
1 作用域
作用域(scope)规定了变量能够被访问的“范围”,若变量(常量)离开了这个“范围”便不能被访问,其分为 局部作用域 和 全局作用域。
1.1 局部作用域
局部作用域分为 函数作用域 和 块作用域,
函数作用域即
① 函数内部声明的变量,在函数外部无法被访问
② 函数的参数也是函数内部的局部变量
③ 不同函数内部声明的变量无法互相访问
④ 函数执行完毕后,函数内部的变量实际被清空了
块作用域即使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将有可能无法被访问,
① let 、const 声明的变量会产生块作用域,var 不会产生块作用域(推荐使用 let 或 const)
② 不同代码块之间的变量无法互相访问
③ 函数执行完毕后,函数内部的变量实际被清空了
1.2 全局作用域
<script> 标签 和 .js 文件 的最外层就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问,需要注意的是在书写代码时要尽量避免全局变量,防止全局变量污染问题,比如为 window 对象动态添加的属性、函数中未使用任何关键字声明变量等行为都会创建全局变量,因此此类行为是不被推荐的。
1.3 作用域链
作用域链本质上是底层的变量查找机制。在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域。
<script>
let a = 1
let b = 2
function fn(){
let a = 1
let b = 3
function gn(){
a = 2
console.log(a);
console.log(b);
}
gn()
}
fn()
</script>
函数 gn() 中需要打印 a 和 b 的值,而 gn() 中没有定义 a 和 b ,因此该函数会向其上一级的 fn() 函数查询是否定义了 a 和 b ,而 fn() 中定义了 a 和 b,因此 gn() 中会使用 fn() 中的 a 和 b ,又因为 gn() 中对 a 进行了重新赋值,因此 gn() 打印出来的 a 和 b 的值分别为 2 和 3。
1.4 闭包
闭包是一个函数以及其捆绑的周边环境状态的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。简单来说闭包就是内层函数+外层函数变量。
<script>
// 闭包形式 统计函数调用的次数
function count() {
let i = 0
function fn() {
i++
console.log(`函数被调用了${i}次`)
}
return fn
}
const fun = count()
fun()
fun()
</script>
上面的代码中, fn() 为内层函数,count() 为外层函数, let i = 0 中的 i 为外层函数变量,在 count() 函数的最后返回了 fn() 函数,所以在 count() 外接收到 fn() 函数后便可以调用到 count() 函数内的 i ,这样的形式就叫做闭包。
如果将 i 作为全局变量也能达到计数效果,但全局变量可能面临着被修改或者被污染的情况,而闭包就能很好的解决这一问题。
1.5 变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)
<script>
function fn() {
console.log(num)
var num = 10
}
fn()
console.log(str + 'world');
var str = 'hello'
</script>
在上面的代码中,无论是函数还是顺序结构都存在着先使用再声明变量的情况,而且声明变量的关键字是 var ,var 的存在会使得变量的声明提前,也就是放到 console.log() 之前,但赋值语句却不会提前,所以每个变量的值都为 undefined ,如下所示
<script>
function fn() {
var num
console.log(num)
num = 10
}
fn()
var str
console.log(str + 'world');
str = 'hello'
</script>
综上,
① 变量在未声明而被访问时会报语法错误(所以 var 也好,let 、const 也好,赶紧做声明吧)
② 变量在var声明之前即被访问,变量的值为 undefined
③ let、const 声明的变量不存在变量提升
④ 变量提升出现在相同作用域当中
⑤ 实际开发中推荐先声明再访问变量
2 函数进阶
2.1 函数提升
函数提升是指函数在声明之前即可被调用,不过这种提升仅限于普通函数,也就是使用 function 关键自定义并且具有函数名的函数,
<script>
function example(){
console.log(arguments);
}
example(1,2,3)
example(1,2,3,4,5,6)
</script>
<script>
fun()
var fun = function () {
console.log('函数表达式')
}
</script>
若是函数变量则无法实现函数提升,系统会报错,
2.2 函数参数
1.动态参数
动态参数是函数内置的伪数组变量,它包含了调用函数时传入的所有实参,在函数内部的名称为 arguments 。
<script>
function example(){
console.log(arguments);
}
example(1,2,3)
example(1,2,3,4,5,6)
</script>
即便传入的实参个数大于形参个数, arguments 同样能拿到所有传入函数的实参 ,
<script>
function example(a,b){
console.log(a,b);
console.log(arguments);
}
example(1,2,3)
example(1,2,3,4,5,6)
</script>
2.剩余参数
当我们无法确定用户传入实参个数,但又必须使用这些实参时,就可以使用剩余参数。剩余参数的作用就是允许我们将一个不定数量的参数表示为一个数组。
剩余参数需要写在形参的末端,参数名前需要写上三个英文句号(...),用于获取多余的实参,与arguments 获取的伪数组不一样,剩余参数获取的是真数组。
<script>
function example(a,b, ...arr){
console.log(arr);
console.log(arr.length);
}
example(1,2,3,4,5,6,7,8)
</script>
2.3 箭头函数
箭头函数是一种极简函数,它比较适合于替换匿名函数,它的基本写法为
只有一个形参时,可省略小括号
<script>
const fn = x => {
console.log(x)
}
fn(1)
</script>
只有一行代码时,可以省略大括号
<script>
const fn = x => console.log(x)
fn(1)
</script>
不仅可以省略大括号,还能省略 return 关键字,
<script>
const fn = x => x + x
console.log(fn(1))
</script>
若仅有一行代码还要返回一个对象,则需要在对象外再加一对小括号
<script>
const fn = (uname) => ({ uname: uname })
console.log(fn('hello world'))
</script>
需要注意的是,箭头函数并不会创建自己的 this ,而只会沿用自己作用域链上一层的 this 。
<script>
const obj = {
uname: 'xxxxxx',
sayHi: () => {
console.log(this) // this 指向 window
}
}
obj.sayHi()
</script>
因此,在使用箭头函数前需要考虑好函数体内是否需要使用 this ,否则可能会适得其反。
3 解构赋值
解构的意思就是将数组或者对象中的某些值在不使用额外算法的情况下快速批量地赋值给一系列变量的简洁语法。
3.1 数组解构
1.一维数组
① 数组内元素数量大于解构变量数量
<script>
let c = [1,2,3]
let [ax,vx] = c
console.log(ax,vx);
</script>
<script>
let c = [1,2,3]
let [,ax,vx] = c
console.log(ax,vx);
</script>
可以利用逗号,当做占位符
② 数组内元素数量等于解构变量数量
<script>
let c = [1,2,3]
let [cx,ax,vx] = c
console.log(ax,vx,cx);
</script>
③ 数组内元素数量小于解构变量数量
<script>
let c = [1,2,3]
let [cx,ax,vx,xx] = c
console.log(cx,ax,vx,xx);
</script>
2.二维数组
<script>
let c = [1,2,3,[9,10,11]]
let [cx,ax,vx] = c
console.log(cx,ax,vx);
</script>
<script>
let c = [1,2,3,[9,10,11]]
let [cx,ax,vx,dx] = c
console.log(cx,ax,vx,dx);
</script>
<script>
let c = [1,2,3,[9,10,11]]
let [cx,ax,vx,[ex,gx,fx]] = c
console.log(cx,ax,vx,ex,gx,fx);
</script>
<script>
let c = [1,2,3,[9,10,11]]
let [cx,ax,vx,[,gx,fx]] = c
console.log(cx,ax,vx,gx,fx);
</script>
3.利用剩余参数解决变量少 单元值多的情况
4. 防止有undefined传递单元值的情况,可以设置默认值
允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效
3.2 对象解构
1.基本用法
基本语法如下所示,
<script>
// 对象解构
const obj = {
uname: '小明',
age: 18
}
const { uname, age } = obj
// 等价于 const uname = obj.uname
// 要求属性名和变量名必须一直才可以
console.log(uname)
console.log(age)
</script>
需要注意的是,
① 对象属性的值只能被赋值给与属性名相同的变量
② 解构的变量名不要和外面的变量名冲突否则报错
③ 对象中找不到与变量名一致的属性时变量值为 undefined
<script>
// 对象解构
const obj = {
uname: '小明',
age: 18
}
const uname = 'red老师'
// 解构的语法
// const { uname, age } = {age: 18, uname: 'pink老师' }
const { uname, age, gender } = obj
console.log(uname)
console.log(age)
console.log(gender)
</script>
2.给新的变量名赋值
可以从一个对象中提取变量并同时修改新的变量名
冒号(:)左边为对象内属性名,右边为替换名,一旦定义了替换名, 冒号(:)左侧的名字就不能用了,否则会未定义的错误。
3.数组对象解构
<script>
// 解构数组对象
const pig = [
{
uname: '佩奇',
age: 6
}
]
const [{ uname, age }] = pig
console.log(uname)
console.log(age)
</script>
4.多级对象解构
① 单个多级对象
<script>
const pig = {
name: '小孩',
family: {
mother: '妈妈',
father: '爸爸',
sister: '妹妹'
},
age: 6
}
// 多级对象解构
const { name, family: { mother, father, sister } } = pig
console.log(name)
console.log(mother)
console.log(father)
console.log(sister)
</script>
② 多级对象数组
<script>
const person = [
{
name: '小孩',
family: {
mother: '妈妈',
father: '爸爸',
sister: '妹妹'
},
age: 6
}
]
const [{ name, family: { mother, father, sister } }] = person
console.log(name)
console.log(mother)
console.log(father)
console.log(sister)
</script>