Wendy
文章17
标签13
分类6
js高级-es6笔记

js高级-es6笔记

JavaScript 进阶 - 第1天

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

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

一、作用域

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

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

1.1 局部作用域

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

函数作用域

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

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); // 正常
}

// 超出了 str 的作用域
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 声明成常量。

1.2 全局作用域

<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.3 作用域链

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

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,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。

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

scope

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

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.4 垃圾回收机制

1.4.1 什么是垃圾回收机制?

垃圾回收机制(Garbage Collection) 简称 GC
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收

1.4.2 内存泄漏

正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题
但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法回收)的情况
不再用到的内存,没有及时释放,就叫做内存泄漏

1.4.3 内存的生命周期

JS环境中分配的内存,一般有如下生命周期:

  1. 内存分配: 当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用: 即读写内存,也就是使用变量、函数等
  3. 内存回收: 使用完毕,由垃圾回收自动回收不再使用的内容
  4. 说明:
    • 全局变量一般不会回收(关闭页面回收);
    • 一般情况下局部变量的值,不用了会被自动回收掉

1.4.4 垃圾回收算法说明

所谓垃圾回收,核心思想就是如何判断内存是否已经不再会被使用了,如果是,就视为垃圾,释放掉
有两种常见的浏览器垃圾回收算法: 引用计数法标记清除法

引用计数
IE采用的引用计数算法,定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。
算法:

  1. 跟踪记录每个值被引用的次数。
  2. 如果这个值的被引用了一次,那么就记录次数加1
  3. 多次引用会累加
  4. 如果减少一个引用就减1
  5. 如果引用次数是0,则释放内存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script>
    const person={ //定义了person,引用次数为1
    age:18,
    name:'ljw'
    }
    const p=person //因为引用的是地址,所以引用次数加一
    person=1 //person不再指向原来的结构体,所以引用次数减一
    p=null //p也不再指向原来的结构体,所以引用次数减一
    //此时引用次数为0,释放内存
    </script>
    由上面可以看出,引用计数算法是个简单有效的算法。
    但它却存在一个致命的问题:嵌套引用
    如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄漏。
1
2
3
4
5
6
7
8
9
10
<script>
function fn(){
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return '引用计数无法回收'
}
fn()
</script>

因为他们的引用次数永远不会是0,这样的相互引用如果很大量的存在就会导致大量的内存泄漏

标记清除法

现在的浏览器已经不再使用引用计数算法了。
现在浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:

  1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”
  2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
  3. 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收

1.5 闭包

闭包(closure)是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。
一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域

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

从代码形式上看闭包是一个做为返回值的函数,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
function foo() {
let i = 0;

// 函数内部分函数
function bar() {
console.log(++i);
}

// 将函数做为返回值
return bar;
}

// fn 即为闭包函数
let fn = foo();

fn(); // 1
</script>

常见闭包的形式:外部可以访问使用函数内部的变量

总结:

  1. 闭包本质仍是函数,只不是从函数内部返回的
  2. 闭包能够创建外部可访问的隔离作用域,避免全局变量污染
  3. 过度使用闭包可能造成内存泄漏

注:回调函数也能访问函数内部的局部变量。

1.6 变量提升

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

把所有var声明的变量提升到当前作用域的最前面,只提升声明,不提升赋值

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 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料

二、函数

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

2.1 函数提升

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

会把所有函数声明提升到当前作用域的最前面
只提升函数声明,不提升函数调用

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. 函数提升出现在相同作用域当中

2.2 参数

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

默认值
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
const arr = [1,2,3,4,5]
console.log(...arr) // 1 2 3 4 5

不会改变原数组

典型运用场景:求数组最大值(最小值)、合并数组等

1
2
3
4
5
6
//求最值
console.log(Math.max(...arr))
console.log(Math.min(...arr))
//合并数组
const arr2 = [3,4,5]
const arr = [...arr1,...arr2]

展开运算符的剩余参数的区别:
剩余参数:函数参数使用,得到真数组
展开运算符:数组中使用,数组展开

2.3 箭头函数

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

引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁
箭头函数更适用于那些本来需要匿名函数的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
// 箭头函数
const foo = () => { //本行相当于 const foo = function () {
console.log('^_^ 长相奇怪的函数...');
}
// 调用函数
foo()

//只有一个参数时可以省略圆括号 ()
const fn = a => {
console.log(a)
}

//函数体只有一行代码时可以省略花括号 `{}`,并自动做为返回值被返回
const fn = a => a + a

// 更简洁的语法
const form = document.querySelector('form')
form.addEventListener('click', ev => ev.preventDefault())

//可以直接返回一个对象
const fn = (uname) => ({ uname: uname })

</script>

箭头函数中的this

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
// 以前this的指向:  谁调用的这个函数,this 就指向谁
console.log(this) // window
// 普通函数
function fn() {
console.log(this) // window
}
window.fn()
// 对象方法里面的this
const obj = {
name: 'andy',
sayHi: function () {
console.log(this) // obj
}
}
obj.sayHi()

// 2. 箭头函数的this 是上一层作用域的this 指向
const fn = () => {
console.log(this) // window
}
fn()
// 对象方法箭头函数 this
const obj = {
uname: 'pink老师',
sayHi: () => {
console.log(this) // this 指向谁? window
}
}
obj.sayHi()

const obj = {
uname: 'pink老师',
sayHi: function () {
console.log(this) // obj
let i = 10
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()

总结:

  1. 箭头函数属于表达式函数,因此不存在函数提升
  2. 箭头函数只有一个参数时可以省略圆括号 ()
  3. 箭头函数函数体只有一行代码时可以省略花括号 {},并自动做为返回值被返回
  4. 箭头函数中没有 arguments,只能使用剩余函数 ... 动态获取实参
  5. 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层延用this
  6. DOM事件回调函数不推荐使用箭头函数,特别是要用到this的时候
  7. 事件回调函数使用箭头函数时,this为全局的window

三、解构赋值

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

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

3.1 数组解构

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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

let a = 1
let b = 2; //这里必须加分号
[b,a]=[a,b] //交换变量值


</script>

数组解构的细节

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
// 1. 变量多, 单元值少 , undefined
const [a, b, c, d] = [1, 2, 3]
console.log(a) // 1
console.log(b) // 2
console.log(c) // 3
console.log(d) // undefined
// 2. 变量少, 单元值多
const [a, b] = [1, 2, 3]
console.log(a) // 1
console.log(b) // 2
// 3. 剩余参数 变量少, 单元值多
const [a, b, ...c] = [1, 2, 3, 4]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [3, 4] 真数组
// 4. 防止 undefined 传递
const [a = 0, b = 0] = [1, 2]
const [a = 0, b = 0] = []
console.log(a) // 1
console.log(b) // 2
// 5. 按需导入赋值
const [a, b, , d] = [1, 2, 3, 4]
console.log(a) // 1
console.log(b) // 2
console.log(d) // 4

const arr = [1, 2, [3, 4]]
console.log(arr[0]) // 1
console.log(arr[1]) // 2
console.log(arr[2]) // [3,4]
console.log(arr[2][0]) // 3

// 多维数组解构
const arr = [1, 2, [3, 4]]
const [a, b, c] = [1, 2, [3, 4]]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [3,4]

const [a, b, [c, d]] = [1, 2, [3, 4]]
console.log(a) // 1
console.log(b) // 2
console.log(c) // 3
console.log(d) // 4

总结:

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

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

3.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
32
<script>
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user

const {name, age} = {name: 'lili',age: 18}

console.log(name) // 小明
console.log(age) // 18

//对象解构的变量名 可以重新改名 旧变量名: 新变量名
const { uname: username, age } = { uname: 'pink', age: 18 }
console.log(username)
console.log(age)

// 解构数组对象
const pig = [
{
uname: '佩奇',
age: 6
}
]
const [{ uname, age }] = pig
console.log(uname)
console.log(age)

</script>

多级对象解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

总结:

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

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

两种必须加分号的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 立即执行函数要加
(function () { })();
(function () { })();
// 2. 使用数组的时候
// const arr = [1, 2, 3]
const str = 'pink';
[1, 2, 3].map(function (item) {
console.log(item)
})

let a = 1
let b = 2
;[b, a] = [a, b]

console.log(a, b)
forEach遍历数组放法

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

1
2
3
4
5
6
7
8
9
10
11
被遍历的数组.forEach(function(当前数组元素,当前元素索引号){
//函数体
})

// 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)

注意:1.forEach主要是遍历数组
2. 参数当前数组元素是必须要写的,索引号可选
适合于遍历数组对象

filter筛选数组方法

filter方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景:**筛选符合条件的元素,并返回筛选之后元素的新数组
返回值: 返回数组,包含了符合条件的所有元素,如果没有符合条件的元素则返回空数组

1
2
3
4
5
6
7
被遍历的数组.filter(function (currentValue,index){
return 筛选条件
})

const newArr = arr.filter(item => item >= 20)
console.log(newArr)

本文作者:Wendy
本文链接:https://wendy-ljw.github.io/2022/11/17/js-es6-1/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×