# ES6-ES13

ECMA新描述概念

ECMA新描述概念

  • 在执行学习JavaScript代码执行过程中,我们学习了很多ECMA文档的术语:

    • 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
    • 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
    • 变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;
    • 全局对象:Global Object,全局执行上下文关联的VO对象;
    • 激活对象:Activation Object,函数执行上下文关联的VO对象;
    • 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
  • 在新的ECMA 代码执行描述中(ES5以及之上),对于代码的执行流程描述改成了另外的一些词汇:

    • 基本思路是相同的,只是对于一些词汇的描述发生了改变;
    • 执行上下文栈和执行上下文也是相同的;

词法环境

  • 词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符;
    • 一个词法环境是由环境记录(Environment Record)和一个外部词法环境(oute;r Lexical Environment)组成;
    • 一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来;

1674715027356

  • 也就是在ES5之后,执行一个代码,通常会关联对应的词法环境;
    • 那么执行上下文会关联哪些词法环境呢?

1674715053861

  • LexicalEnvironment用于处理let、const声明的标识符:

1674715087198

  • VariableEnvironment用于处理var和function声明的标识符:

1674715092632

环境记录

  • 在这个规范中有两种主要的环境记录值:声明式环境记录和对象环境记录。
    • 声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与ECMAScript语言值关联起来的Catch子句。
    • 对象式环境记录:对象环境记录用于定义ECMAScript元素的效果,例如WithStatement,它将标识符绑定与某些对象的属性关联起来。

1674715125454

  • 新ECMA描述内存图

1674715163644

let/const的使用

let/const的使用

  • 在 ES5 中我们声明变量都是使用的 var关键字,从ES6开始新增了两个关键字可以声明变量:let、const

    • let、const 在其他编程语言中都是有的,所以也并不是新鲜的关键字;
    • 但是 let、const 确确实实给 JavaScript 带来一些不一样的东西;
  • let 关键字:

    • 从直观的角度来说,let 和 var 是没有太大的区别的,都是用于声明一个变量;
  • const 关键字:

    • const 关键字是 constant 的单词的缩写,表示常量、衡量的意思;
    • 它表示保存的数据一旦被赋值,就不能被修改;
    • 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;
  • 注意:

    • 另外 let、const 不允许重复声明变量,否则会报错
    • var 声明过的变量也不能再通过 let / const 再次声明,否则会报错

let/const无作用域提升

  • let、const 和 var 的另一个重要区别是作用域提升:
    • 我们知道 var 声明的变量是会进行作用域提升的;
    • 但是如果我们使用 let 声明的变量,在声明之前访问会报错;
1
2
console.log(foo) // 报错
let foo = "foo"
  • 那么是不是意味着foo变量只有在代码执行阶段才会创建的呢?
    • 事实上并不是这样的,我们可以看一下 ECMA262 对 let 和 const 的描述;
    • 这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;
  • 从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的。
  • 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
    • 事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;
      • 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
      • 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;
    • 所以我的观点是 let、const 没有进行作用域提升,但是会在解析阶段被创建出来

暂时性死区

  • 我们知道,在let、const定义的标识符真正执行到声明的代码之前,是不能被访问的
    • 从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)
1
2
3
4
{
console.log(name)
let name = "why"
}
  • 使用术语 “temporal” 是因为区域取决于执行顺序(时间),而不是编写代码的位置;
1
2
3
4
5
function foo(){
console.log(message)
}
let message = "Hello World"
foo()

window对象添加属性

  • 我们知道,在全局通过 var 来声明一个变量,事实上会在 window 上添加一个属性:

  • 但是 let、const 是不会给 window 上添加任何属性的。

  • 那么我们可能会想这个变量是保存在哪里呢?

1674715256606

var 的作用域

  • 在我们前面的学习中,JavaScript只会形成两个作用域:全局作用域和函数作用域

1674715309954

  • ES5中放到一个代码中定义的变量,外面是可以访问的:

1674715335028

let/const的块级作用域

  • 在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的:
    • 使用 let 和 const 声明的变量是有块级作用域的
1
2
3
4
5
6
7
8
9
10
{
let foo = "foo"
function bar() {
console.log("bar")
}
class Person { }
}
// console.log(foo) // 报错: Uncaught ReferenceError: foo is not defined
bar() // bar
// let p = new Person; // 报错: Uncaught ReferenceError: Person is not defined
  • 但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的:
    • 这是因为引擎会对函数的声明进行特殊的处理,允许像 var 那样进行提升;

块级作用域的应用

  • 点击按钮,输出不同的索引值:
1
2
3
4
5
6
const btnEl = document.querySelectorAll("button")
for (let i = 0; i < btnEl.length; i++) {
btnEl[i].onclick = () => {
console.log(i)
}
}

var/let/const的选择

  • 那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?
  • 对于var的使用:
    • 我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;
    • 其实是JavaScript在设计之初的一种语言缺陷;
    • 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;
    • 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;
  • 对于let、const:
    • 对于let和const来说,是目前开发中推荐使用的;
    • 我们会优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改;
    • 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;
    • 这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;

模板字符串

  • 在ES6之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)。
  • ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
    • 首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;
    • 其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;
1
2
3
4
5
// 1.1.ES6之前
const name = "why"
const age = 18
const info = "my name is" + name + ", age is " + age
console.log(info); // my name iswhy, age is 18
1
2
3
4
5
// 1.2.ES6之后
const name = "why"
const age = 18
const info = `my name is ${name}, age is ${age}`
console.log(info) // my name is why, age is 18

标签模板字符串使用

  • 模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。
  • 我们一起来看一个普通的JavaScript的函数:
  • 如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:
    • 模板字符串被拆分了;
    • 第一个元素是数组,是被模块字符串拆分的字符串组合;
    • 后面的元素是一个个模块字符串传入的内容;
1
2
3
4
5
6
7
8
9
// 2.标签模板字符串的用法
function foo(...args) {
console.log("参数:", args)
}
foo("why", 18, 1.88) //  ['why', 18, 1.88]

const name = "why"
const age = 18
foo `my name is ${name}, age is ${age}, height is ${1.88}` //  [Array(4), 'why', 18, 1.88] // 也是函数调用的一种方式

箭头函数

箭头函数简介

  • 箭头函数是 ES6 之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:
  • 箭头函数 不会绑定 this 、arguments 属性
  • 箭头函数 不能作为构造函数来使用 (不能和 new 一起来使用,会抛出错误);

箭头函数的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.之前声明函数的方式
function foo1() {}
var foo2 = function (name, age) {
console.log('函数体代码', this, arguments)
console.log(name, age)
}

// 2.箭头函数完整写法
var foo3 = (name, age) => {
console.log('箭头函数的函数体')
console.log(name, age)
}
foo3('why', 18) // 函数调用
1
2
3
4
5
6
7
8
9
10
11
// 3.箭头函数的练习
// 3.1. forEach , forEach的三个参数依次分别为:每次遍历的数组元素,数组索引,被遍历的数组
var names = ['abc', 'cba', 'nba']
names.forEach((item, index, arr) => {
console.log(item, index, arr)
})

// 3.2. setTimeout
setTimeout(() => {
console.log('setTimeout')
}, 3000)

箭头函数的简写

1
2
3
4
5
6
7
8
9
10
11
var names = ['abc', 'cba', 'nba']
var nums = [20, 30, 11, 15, 111]

// 1.优化一: 如果箭头函数只有一个参数, 那么()可以省略
names.forEach((item) => {
console.log(item) // abc cba nba
})
var newNums = nums.filter((item) => {
return item % 2 === 0
})
console.log(newNums) // [20, 30]
1
2
3
4
5
6
7
8
9
10
var names = ['abc', 'cba', 'nba']
var nums = [20, 30, 11, 15, 111]

// 2.优化二: 如果函数体中只有一行执行代码, 那么{}可以省略,并且这行代码的返回值会作为整个函数的返回值
names.forEach((item) => console.log(item))
// 一行代码中不能带return关键字, 如果省略, 需要带return一起省略
var newNums = nums.filter((item) => {
return item % 2 === 0
})
console.log(newNums) // [20, 30]
1
2
3
4
// 3.优化三: 只有一行代码时, 这行代码的表达式结果会作为函数的返回值默认返回的
var nums = [20, 30, 11, 15, 111]
var newNums = nums.filter((item) => item % 2 === 0)
console.log('newNums:', newNums) // newNums: [20, 30]
1
2
3
4
5
// 4.优化四: 如果默认返回值是一个对象, 那么这个对象必须加()
var arrFn = () => ['abc', 'cba'] // ['abc', 'cba'] 将会作为返回值
var arrFn = () => {} // 注意: 这里是{}执行体
var arrFn = () => ({ name: 'why' }) // {name: 'why'} ,要返回一个对象,必须加小括号才能作为返回值
console.log(arrFn())
1
2
3
4
5
6
7
// 箭头函数实现nums的所有偶数平方的和
var nums = [20, 30, 11, 15, 111]
var result = nums
.filter((item) => item % 2 === 0)
.map((item) => item * item)
.reduce((prevValue, item) => prevValue + item)
console.log(result) // 1300

箭头函数的 this 指向

  • 箭头函数根据外层作用域来决定 this 的指向
1
2
3
4
5
6
7
// 1 普通函数中是有this的标识符
function foo() {
console.log('foo', this)
}

foo() // window
foo.apply('aaa') // String {'aaa'}
1
2
3
4
5
6
7
8
// 2.箭头函数中, 压根没有this
var bar = () => {
console.log('bar:', this)
}

bar() // window
bar.apply('aaaa') // window , 通过apply调用时, 也是没有this
console.log('全局this:', this) // window
1
2
3
4
5
6
7
8
9
10
11
12
13
// 3.this的查找规则
var obj = {
name: 'obj',
foo: () => {
var bar = () => {
console.log('bar222:', this)
}
return bar
},
}

var fn = obj.foo()
fn.apply('bbb') // window ,如果 foo 是一个普通函数,this 就是指向 obj 对象

箭头函数中 this 的应用

  • 案例一:调用 obj 对象中的一个方法发起网络请求获取数据,并存放在 obj 的 name 属性中
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
// 网络请求的工具函数
function request(url, callbackFn) {
var results = ['abc', 'cba', 'nba']
callbackFn(results)
}

// 实际操作的位置(业务)
var obj = {
names: [],
network: function () {
// 普通方法中的this指向 obj 对象
// 1.早期的时候
// var _this = this
// request("/names", function(res) {
// _this.names = [].concat(res)
// })

// 2.箭头函数写法
request('/names', (res) => {
this.names = [].concat(res)
})
},
}

obj.network()
console.log(obj)
  • 案例二:定时器中的 this 的使用
    • 调用 obj 对象中的一个方法发起网络请求获取数据,并存放在 obj 的 name 属性中
1
2
3
4
5
6
7
8
9
10
11
12
13
var obj2 = {
data: [],
getData: function () {
setTimeout(() => {
var res = ['abc', 'cba', 'nba']
this.data.push(...res)
console.log(this) // this 的指向为 obj2 对象
}, 1000)
},
}

obj2.getData()
console.log(obj2)

常见 this 指向的面试题

面试题一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = 'window'

var person = {
name: 'person',
sayName: function () {
console.log(this.name)
},
}

function sayName() {
var sss = person.sayName
sss() // 绑定: 默认绑定, window -> window
person.sayName() // 绑定: 隐式绑定, person -> person
person.sayName() // 绑定: 隐式绑定, person -> person
;(b = person.sayName)() // 术语: 间接函数引用, window -> window
}
sayName()

面试题二

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
var name = 'window'

var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
},
}
var person2 = { name: 'person2' }

// 开始题目:
person1.foo1() // 隐式绑定: person1
person1.foo1.call(person2) // 显式绑定: person2

person1.foo2() // 上层作用域: window
person1.foo2.call(person2) // 上层作用域: window

person1.foo3()() // 默认绑定: window
person1.foo3.call(person2)() // 默认绑定: window
person1.foo3().call(person2) // 显式绑定: person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

面试题三

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
var name = 'window'
/*
1.创建一个空的对象
2.将这个空的对象赋值给this
3.执行函数体中代码
4.将这个新的对象默认返回
*/
function Person(name) {
this.name = name
;(this.foo1 = function () {
console.log(this.name)
}),
(this.foo2 = () => console.log(this.name)),
(this.foo3 = function () {
return function () {
console.log(this.name)
}
}),
(this.foo4 = function () {
return () => {
console.log(this.name)
}
})
}

// person1/person2都是对象(实例instance)
var person1 = new Person('person1')
var person2 = new Person('person2')

// 面试题目:
person1.foo1() // 隐式绑定: person1
person1.foo1.call(person2) // 显式绑定: person2

person1.foo2() // 上层作用域查找: person1
person1.foo2.call(person2) // 上层作用域查找: person1

person1.foo3()() // 默认绑定: window
person1.foo3.call(person2)() // 默认绑定: window
person1.foo3().call(person2) // 显式绑定: person2

person1.foo4()() // 上层作用域查找: person1(隐式绑定)
person1.foo4.call(person2)() // 上层作用域查找: person2(显式绑定)
person1.foo4().call(person2) // 上层作用域查找: person1(隐式绑定)

面试题四

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
var name = 'window'

function Person(name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
},
}
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()() // 默认绑定: window
person1.obj.foo1.call(person2)() // 默认绑定: window
person1.obj.foo1().call(person2) // 显式绑定: person2

person1.obj.foo2()() // 上层作用域查找: obj(隐式绑定)
person1.obj.foo2.call(person2)() // 上层作用域查找: person2(显式绑定)
person1.obj.foo2().call(person2) // 上层作用域查找: obj(隐式绑定)

函数的参数

默认参数

  • 在ES6之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:

    • 传入了参数,那么使用传入的参数;
    • 没有传入参数,那么使用一个默认值;
    • 而在ES6中,我们允许给函数一个默认值:
  • 不严谨的默认值写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(arg1) {
// 1.1 这种写法不严谨, 像 0 , "" , false , null , undefined 等值会被判为 false ,得到默认值,而不是传递的参数
// 默认值写法一:
arg1 = arg1 ? arg1 : "我是默认值"
console.log(arg1)
}
foo(123, 321) // 123
foo() // 我是默认值
foo(0) // 我是默认值
foo("") // 我是默认值
foo(false) // 我是默认值
foo(null) // 我是默认值
foo(undefined) // 我是默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(arg1) {
// 1.2 这种写法不严谨, 像 0 , "" , false , null , undefined 等值会得到默认值,而不是传递的参数
// 默认值写法二:
arg1 = arg1 || "我是默认值"
console.log(arg1)
}
foo(123, 321) // 123
foo() // 我是默认值
foo(0) // 我是默认值
foo("") // 我是默认值
foo(false) // 我是默认值
foo(null) // 我是默认值
foo(undefined) // 我是默认值
  • 严谨的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(arg1) {
// 2.1 严谨的写法
// 三元运算符
arg1 = (arg1 === undefined || arg1 === null) ? "我是默认值" : arg1
console.log(arg1)
}
foo(123, 321) // 123
foo() // 我是默认值
foo(0) // 0
foo("") // "" 空字符串
foo(false) // false
foo(null) // 我是默认值
foo(undefined) // 我是默认值
1
2
3
4
5
6
7
8
9
10
11
12
function foo(arg1) {
// 2.2 ES6之后新增语法: ??
arg1 = arg1 ?? "我是默认值"
console.log(arg1)
}
foo(123, 321) // 123
foo() // 我是默认值 // 不传参就会使用默认值
foo(0) // 0
foo("") // "" 空字符串
foo(false) // false
foo(null) // 我是默认值 // null 会被判为空
foo(undefined) // 我是默认值 // undefined 会被判为空
1
2
3
4
5
6
7
8
9
10
11
function foo(arg1 = "我是默认值", arg2 = "我也是默认值") {
// 3.简便的写法: 默认参数,在形参使用默认值, 注意: 默认参数是不会对null进行处理的
console.log(arg1)
}
foo(123, 321) // 123
foo() // 我是默认值 , 没有传实参而使用默认值
foo(0) // 0
foo("") // "" 空字符串
foo(false) // false
foo(null) // null
foo(undefined) // 我是默认值 , undefined 会被判为没有传实参而使用默认值
  • 默认值也可以和解构一起来使用:
  • 另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的):
  • 但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配;
  • 另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了。
1
2
3
4
// 1.解构的回顾
const obj = { name: "why" }
const { name = "kobe", age = 18 } = obj // 对对象解构的同时赋予默认值
console.log(name, age); // why 18
1
2
3
4
5
6
// 2.1 函数的默认值是一个对象
function foo(obj = { name: "why", age: 18 }) {
console.log(obj.name, obj.age)
}
foo() // why 18 函数调用,不传参
foo({ name: "qwe", age: 20, height: 1.88 }) // qwe 20 函数调用,并传递实参
1
2
3
4
5
6
// 2.2 函数的默认值是一个对象,同时进行解构
function foo({ name, age } = { name: "why", age: 18 }) {
console.log(name, age)
}
foo() // why 18 函数调用,不传参
foo({ name: "abc", age: 19, height: 1.88 }) // abc 19 函数调用,并传递参数
1
2
3
4
5
6
// 2.3 函数的默认值是一个空对象,同时进行解构
function foo({ name = "why", age = 18 } = {}) {
console.log(name, age)
}
foo() // why 18 函数调用,不传参
foo({ name: "qwe", age: 20, height: 1.88 }) // qwe 20 函数调用,并传递参数

剩余参数

  • ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
    • 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
  • 那么剩余参数和arguments有什么区别呢?
    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
    • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
    • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;
    • 注意:剩余参数必须放到最后一个位置,否则会报错。
1
2
3
4
// 1.function定义的函数是有两个原型的:
function foo() {}
console.log(foo.prototype) // new foo() -> f.__proto__ = foo.prototype
console.log(foo.__proto__) // -> Function.prototype
1
2
3
4
5
6
7
// 2.箭头函数是没有显式原型
// 在ES6之后, 定义一个类要使用class定义
var bar = () => {}
console.log(bar.__proto__ === Function.prototype) // true
// 没有显式原型
console.log(bar.prototype) // undefined
// var b = new bar() // 报错 bar is not a constructor

展开语法

展开语法

  • 展开语法(Spread syntax):
    • 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开;
    • 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开;
  • 展开语法的场景:
    • 在函数调用时使用;
    • 在数组构造时使用;
    • 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性;
  • 注意:展开运算符其实是一种浅拷贝;

数组展开

1
2
3
4
5
6
// ES6
// 1.1 将一个数组展开
const names = ["abc", "cba", "nba", "mba"]
const str = "Hello"
const newNames = [...names, "aaa", "bbb"]
console.log(newNames) //  ['abc', 'cba', 'nba', 'mba', 'aaa', 'bbb']
1
2
3
4
5
6
7
8
// 1.2 函数调用时,将数组或字符串展开作为实参使用
const names = ["abc", "cba", "nba", "mba"]
const str = "Hello"
function foo(name1, name2, ...args) {
console.log(name1, name2, args)
}
foo(...names) // abc cba  ['nba', 'mba']
foo(...str) // H e ['l', 'l', 'o']

对象展开

1
2
3
4
5
6
7
8
9
10
11
// ES9(ES2018)  在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性;
// 2.1 在一个函数调用时,对一个对象进行展开作为形参使用,会报错,不可以这样来使用
const obj = {
name: "why",
age: 18
}
function foo(name1, name2, ...args) {
console.log(name1, name2, args)
}
// 在函数的调用时, 用展开运算符, 将对应的展开数据, 进行迭代 可迭代对象: 数组/string/arguments
foo(...obj) // 报错 Found non-callable @@iterator
1
2
3
4
5
6
7
8
9
10
11
// 2.2 在构建一个对象字面量时,可以将已有的对象展开作为另一个对象的属性和属性值
const obj = {
name: "why",
age: 18
}
const info = {
...obj,
height: 1.88,
address: "广州市"
}
console.log(info) // {name: 'why', age: 18, height: 1.88, address: '广州市'}

深拷贝与浅拷贝

浅拷贝

1
2
3
4
5
6
7
8
9
10
11
// 1.引用赋值 , 赋值的是引用的内存地址
const obj = {
name: "why",
age: 18,
height: 1.88,
friend: {
name: "curry"
}
}
const info1 = obj // info1 和 obj 是同一个对象
console.log(info1) // {name: 'why', age: 18, height: 1.88, friend: {…}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 2.浅拷贝  
const obj = {
name: "why",
age: 18,
height: 1.88,
friend: {
name: "curry"
}
}
const info2 = {
// 展开运算符其实是一种浅拷贝;info2 对象会开辟新的内存空间,将 obj 对象的属性和属性值拷贝到 info2 中作为属性使用
// 若原对象 obj 中还有对象,则拷贝的是引用的内存地址,,此时obj和info2对象的 firend 对象是同一个对象. (相当于赋值引用的内存地址)
...obj,
}
info2.name = "kobe" // 修改 info2 的属性
console.log(info2.name) // kobe
console.log(obj.name) // why 原对象 obj 的属性并没有被更改
info2.friend.name = "james" // 修改 info2 对象中 friend 对象的属性
console.log(obj.friend.name) // james 原对象 obj 的属性会被更改,因为拷贝时拷贝的是引用的内存地址

深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 3.深拷贝
// 方式一: 第三方库
// 方式二: 自己实现
// function deepCopy(obj) {}
// 方式三: 利用先有的js机制, 实现深拷贝JSON
const obj = {
name: "why",
age: 18,
height: 1.88,
friend: {
name: "curry"
}
}
const info3 = JSON.parse(JSON.stringify(obj)) // 将一个对象转为字符串型,再转回对象,可以实现深拷贝(在转回对象时就会开辟新的内存空间)
console.log(info3.friend.name) // curry
info3.friend.name = "james" // 修改新对象 info3 中的 friend 对象中的属性
console.log(info3.friend.name) // james
console.log(obj.friend.name) // curry 相当于从obj拷贝出一新的对象,新旧两个对象之间互相不干扰

进制与数值的表示

进制的表示

在ES6中规范了二进制和八进制的写法:

1
2
3
4
5
// 1. 进制
console.log(100) // 100 十进制
console.log(0b100) // 4 二进制, 0b开头
console.log(0o100) // 64 八进制, 0o开头
console.log(0x100) // 256 十六进制, 0x开头

数值的表示

另外在ES2021新增特性:数字过长时,可以使用_作为连接符

1
2
3
4
5
6
// 2.长数字的表示,下划线的位置不能放到开头,仅可用于数字中,便于阅读,对多少个0就分隔一次没有限制
const money1 = 100_00_00_0000_00_00
console.log(money1) // 100000000000000

const money2 = 100_0000_0000_0000
console.log(money2) // 100000000000000

Symbol的使用

Symbol的基本使用

  • Symbol是什么呢?Symbol是ES6中新增的一个基本数据类型,翻译为符号。
  • 那么为什么需要Symbol呢?
    • 在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
    • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
    • 比如我们前面在讲apply、call、bind实现时,我们有给其中添加一个fn属性,那么如果它内部原来已经有了fn属性了呢?
    • 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;
  • Symbol就是为了解决上面的问题,用来生成一个独一无二的值。
    • Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
    • 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;
  • Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的;
  • 我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;
1
2
3
4
5
6
7
8
9
10
11
12
13
// ES6之前存在的问题: (命名冲突问题)
// 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
const obj = {
name: "why",
fn: "aaa"
}
// 给obj对象添加一个新的属性 name
function foo(obj) {
obj.fn = function () { } // 不确定 obj 对象中是否已经存在 fn 属性,如已经存在,则会被覆盖. (命名冲突问题)
}
foo(obj)
console.log(obj) // {name: 'why', fn: ƒ}
console.log(obj.fn) // ƒ () { }

Symbol作为属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ES6之后可以使用Symbol生成一个独一无二的值
const s1 = Symbol(); // 调用 Symbol 函数生成一个独一无二的值
// const info = { name: "why" } // 不能将对象作为另一个对象的 key. 若使用,则会将对象转为字符串作为 key 使用,结果例如: {[object Object]: 'aaa'}
const obj = {
[s1]: "aaa",
}
console.log(obj) // {Symbol(): 'aaa'}


const s2 = Symbol() // 调用 Symbol 函数生成一个独一无二的值
obj[s2] = "bbb" // 修改对象中的属性
console.log(obj) // {Symbol(): 'aaa', Symbol(): 'bbb'}


function foo(obj) {
const sKey = Symbol() // 调用 Symbol 函数生成一个独一无二的值
obj[sKey] = function () { } // 修改对象中的属性
console.log(obj) // {Symbol(): 'aaa', Symbol(): 'bbb', Symbol(): ƒ}
delete obj[sKey] // 删除对象中的属性
console.log(obj) // {Symbol(): 'aaa', Symbol(): 'bbb'}
}
foo(obj)
1
2
3
4
5
6
7
8
9
10
// 1.1 将生成的唯一值加入对象中,作为属性使用
const s1 = Symbol()
const s2 = Symbol()
const obj = {
name: "why",
age: 18,
[s1]: "aaa",
[s2]: "bbb"
}
console.log(obj) // {name: 'why', age: 18, Symbol(): 'aaa', Symbol(): 'bbb'}
1
2
3
4
5
6
7
8
9
10
11
12
const s1 = Symbol()
const s2 = Symbol()
const obj1 = {}
obj1[s1] = "aaa"
obj1[s2] = "bbb"
console.log(obj1) // {Symbol(): 'aaa', Symbol(): 'bbb'}

const obj2 = {}
Object.defineProperty(obj2, s1, {
value: "aaa"
})
console.log(obj2) // {Symbol(): 'aaa'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 2.获取symbol对应的key
const s1 = Symbol()
const s2 = Symbol()
const obj = {
name: "why",
age: 18,
[s1]: "aaa",
[s2]: "bbb"
}
console.log(Object.keys(obj)) // ['name', 'age'] 获取到指定对象的 key ,但是获取不到 symbol 类型的 key
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(), Symbol()] 获取指定对象中 symbol 类型的 key,获取不到普通的 key
// 根据 symbol 类型的 key 获取到对应的属性值:
const symbolKeys = Object.getOwnPropertySymbols(obj)
for (const key of symbolKeys) {
console.log(obj[key]) // aaa / bbb
}

相同值的Symbol

  • 前面我们讲Symbol的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的Symbol应该怎么来做呢?
    • 我们可以使用Symbol.for方法来做到这一点;
    • 并且我们可以通过Symbol.keyFor方法来获取对应的key;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 3.description
// 3.1. Symbol函数直接生成的值, 都是独一无二
const s3 = Symbol("ccc")
console.log(s3.description) // ccc
const s4 = Symbol(s3.description)
console.log(s3 === s4) // false

// 3.2. 通过 Symbol.for 可以生成相同的 Symbol 值
const s5 = Symbol.for("ddd")
const s6 = Symbol.for("ddd")
console.log(s5 === s6) // true

// 3.3 获取传入的key
console.log(Symbol.keyFor(s5)) // ddd

Set的使用

Set的基本使用

  • 在ES6之前,我们存储数据的结构主要有两种:数组、对象。

    • 在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。
  • Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。

    • 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式):
    • 我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重。
1
2
3
// 1.创建Set
const set = new Set()
console.log(set) // Set(0) {size: 0}
1
2
3
4
5
6
7
8
9
10
11
12
13
  // 2.添加元素   
const set = new Set()
set.add(10)
set.add(22)
set.add(35)
set.add(22)
console.log(set) // Set(3) {10, 22, 35}
const info = {}
const obj = {name: "obj"}
set.add(info)
set.add(obj)
set.add(obj)
console.log(set) // Set(5) {10, 22, 35, {…}, {…}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 3.应用场景: 数组的去重
// 数组去重方法一:
const names = ["abc", "cba", "nba", "cba", "nba"]
const newNames = []
for (const item of names) {
if (!newNames.includes(item)) {
newNames.push(item)
}
}
console.log(newNames)

// 数组去重方法二:
const names = ["abc", "cba", "nba", "cba", "nba"]
const newNamesSet = new Set(names)
const newNames = Array.from(newNamesSet) // 将 set 对象转为数组
console.log(newNames) // ['abc', 'cba', 'nba']

Set的常见方法

  • Set常见的属性:
    • size:返回Set中元素的个数;
  • Set常用的方法:
    • add(value):添加某个元素,返回Set对象本身;
    • delete(value):从set中删除和这个值相等的元素,返回boolean类型;
    • has(value):判断set中是否存在某个元素,返回boolean类型;
    • clear():清空set中所有的元素,没有返回值;
    • forEach(callback, [, thisArg]):通过forEach遍历set;
  • 另外Set是支持for of的遍历的。
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
// 4.Set的其他属性和方法
const set = new Set()
// 4.0 size 属性: 对象的长度
console.log(set.size) // 0

// 方法
// 4.1. add方法 往 set 对象中添加属性
const obj = { name: "obj" }
const info = {}
set.add(obj)
set.add(info)
set.add(100)
console.log(set) // Set(3) {{…}, {…}, 100}

// 4.2. delete方法 删除 set 对象中的某个属性
set.delete(obj)
console.log(set) // Set(2) {{…}, 100}

// 4.3. has方法 判断 set 对象中是否包含有指定的属性
console.log(set.has(info)) // true

// 4.4. clear方法 清空 set 对象
// set.clear()
// console.log(set) // Set(0) {size: 0}

// 4.5. forEach 遍历 set 对象
set.forEach(item => console.log(item)) // {} / 100

// 5. set 支持 for...of
for (const item of set) {
console.log(item) // {} / 100
}

WeakSet的使用

WeakSet的使用

  • 和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
  • 那么和Set有什么区别呢?
    • 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
    • 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
  • WeakSet常见的方法:
    • add(value):添加某个元素,返回WeakSet对象本身;
    • delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型;
    • has(value):判断WeakSet中是否存在某个元素,返回boolean类型;
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.Weak Reference(弱引用) 和 Strong Reference(强引用)
let obj1 = { name: "why" }
let obj2 = { name: "kobe" }
let obj3 = { name: "jame" }

let arr = [obj1, obj2, obj3]
obj1 = null // 此处将 obj1 赋值为 null,但是 arr 还在引用该对象的内存地址,因此不会被回收
obj2 = null // 同上
obj3 = null // 同上

const set = new Set(arr)
console.log(set); // Set(3) {{…}, {…}, {…}}
arr = null // 此时将 arr 赋值 null , obj1,obj2,obj3 的内存地址不再被其他对象引用,就会被回收
1
2
3
4
5
6
7
8
9
10
11
// 2.WeakSet的用法
// 2.1 和Set的区别一: 只能存放对象类型,放其他类型的数据会报错
// 2.2 和Set的区别二: 对对象的引用都是弱引用,添加的对象将会被回收
let obj1 = { name: "why" }
let obj2 = { name: "kobe" }
let obj3 = { name: "jame" }
const weakSet = new WeakSet()
weakSet.add(obj1) // obj1 将会被回收
weakSet.add(obj2) // 同上
weakSet.add(obj3) // 同上
console.log(weakSet); // WeakSet {{…}, {…}, {…}}

WeakSet的应用

  • 注意:WeakSet不能遍历
    • 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。
    • 所以存储到WeakSet中的对象是没办法获取的;
  • 那么这个东西有什么用呢?
    • 事实上这个问题并不好回答,我们来使用一个Stack Overflow上的答案;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 3.WeakSet的应用
const pWeakSet = new WeakSet()
class Person {
constructor() {
pWeakSet.add(this)
}
running() {
if (!pWeakSet.has(this)) {
console.log("Type error: 调用的方式不对")
return
}
console.log("running~")
}
}

let p = new Person()
// p = null
p.running()
const runFn = p.running
runFn()
const obj = { run: runFn }
obj.run()

Map的使用

Map的基本使用

  • 另外一个新增的数据结构是Map,用于存储映射关系。
  • 但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?
  • 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);
  • 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;
  • 那么我们就可以使用Map:
1
2
3
4
5
6
7
8
9
// 1.对象类型的局限性: 不可以使用复杂类型作为key
const info = { name: "why" }
const info2 = { age: 18 }
const obj = {
address: "北京市",
[info]: "哈哈哈",
[info2]: "呵呵呵"
}
console.log(obj) // {address: '北京市', [object Object]: '呵呵呵'} ,若写多个这样的 key,会使用后面的那个 key 和 值
1
2
3
4
5
6
7
// 2.Map映射类型
const info = { name: "why" }
const info2 = { age: 18 }
const map = new Map()
map.set(info, "aaaa")
map.set(info2, "bbbb")
console.log(map) // Map(2) {{…} => 'aaaa', {…} => 'bbbb'}

Map的常用方法

  • Map常见的属性:
    • size:返回Map中元素的个数;
  • Map常见的方法:
    • set(key, value):在Map中添加key、value,并且返回整个Map对象;
    • get(key):根据key获取Map中的value;
    • has(key):判断是否包括某一个key,返回Boolean类型;
    • delete(key):根据key删除一个键值对,返回Boolean类型;
    • clear():清空所有的元素;
    • forEach(callback, [, thisArg]):通过forEach遍历Map;
  • Map也可以通过for of进行遍历。
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
// 3.Map的常见属性和方法
const info = { name: "why" }
const info2 = { age: 18 }
const map = new Map()

// 3.0 map 对象的长度
console.log(map.size) // 0

// 3.1. set方法, 设置内容
map.set(info, "cccc")
map.set(info2, "bbbb")
console.log(map) // Map(2) {{…} => 'cccc', {…} => 'bbbb'}

// 3.2. get方法, 获取内容
console.log(map.get(info)) // cccc

// 3.3. delete方法, 删除内容
map.delete(info)
console.log(map) // Map(1) {{…} => 'bbbb'}

// 3.4. has方法, 判断内容
console.log(map.has(info2)) // true

// 3.5. clear方法, 清空内容
map.clear()
console.log(map) // Map(0) {size: 0}

// 3.6. forEach方法
map.forEach(item => console.log(item))

// 4.for...of遍历
for (const item of map) {
const [key, value] = item
console.log(key, value)
}

WeakMap的使用

WeakMap的使用

  • 和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。
  • 那么和Map有什么区别呢?
    • 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
    • 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
  • WeakMap常见的方法有四个:
    • set(key, value):在Map中添加key、value,并且返回整个Map对象;
    • get(key):根据key获取Map中的value;
    • has(key):判断是否包括某一个key,返回Boolean类型;
    • delete(key):根据key删除一个键值对,返回Boolean类型;
1
2
3
4
5
6
7
8
9
10
11
let obj1 = { name: "why" }
let obj2 = { name: "kobe" }

// 1.WeakMap的基本使用
const weakMap = new WeakMap()
// weakMap.set(123, "aaa") // 报错,WeakMap的key只能使用对象,不接受其他的类型作为key
weakMap.set(obj1, "aaa")
weakMap.set(obj2, "bbb")

obj1 = null // 回收obj1
obj2 = null // 回收obj2

WeakMap的应用

  • 注意:WeakMap也是不能遍历的
  • 没有forEach方法,也不支持通过for of的方式进行遍历;
  • 那么我们的WeakMap有什么作用呢?(后续专门讲解)
1

ES6 的其他说明

  • 事实上ES6(ES2015)是一次非常大的版本更新,所以里面重要的特性非常多:
  • 除了前面讲到的特性外还有很多其他特性;
    • Proxy、Reflect,我们会在后续专门进行学习。
    • 并且会利用Proxy、Reflect来讲解Vue3的响应式原理;
  • Promise,用于处理异步的解决方案
    • 后续会详细学习;
    • 并且会学习如何手写Promise;
  • ES Module模块化开发:
    • 从ES6开发,JavaScript可以进行原生的模块化开发;
    • 这部分内容会在工程化部分学习;
    • 包括其他模块化方案:CommonJS、AMD、CMD等方案;

ES7-ES13

ES7

Array Includes

  • 在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。
  • 在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false。
1

指数运算符

  • 在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。
  • 在ES7中,增加了 ** 运算符,可以对数字来计算乘方。
1

ES8

Object values

  • 之前我们可以通过 Object.keys 获取一个对象所有的key
  • 在ES8中提供了 Object.values 来获取所有的value值:
1

Object entries

  • 通过 Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组。
  • 可以针对对象、数组、字符串进行操作;
1

String Padding

  • 某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。
1

  • 我们简单具一个应用场景:比如需要对身份证、银行卡的前面位数进行隐藏:
1

Trailing Commas

  • 在ES8中,我们允许在函数定义和调用时多加一个逗号:
1

Object Descriptors

  • Object.getOwnPropertyDescriptors :
    • 这个在之前已经讲过了,这里不再重复。
  • Async Function:async、await
    • 后续讲完Promise讲解
1

ES9

  • Async iterators:后续迭代器讲解
  • Object spread operators:前面讲过了
  • Promise finally:后续讲Promise讲解

ES10

flat flatMap

  • flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
1

  • flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
    • 注意一:flatMap是先进行map操作,再做flat的操作;
    • 注意二:flatMap中的flat相当于深度为1;
1

Object fromEntries

  • 在前面,我们可以通过 Object.entries 将一个对象转换成 entries
  • 那么如果我们有一个entries了,如何将其转换成对象呢?
  • ES10提供了 Object.formEntries来完成转换:
  • 那么这个方法有什么应用场景呢?
1

rimStart trimEnd

  • 去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?
  • ES10中给我们提供了trimStart和trimEnd;
1

其他内容

  • Symbol description:已经讲过了
  • Optional catch binding:后面讲解try cach讲解

ES11

BigInt

  • 在早期的JavaScript中,我们不能正确的表示过大的数字:
    • 大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的。
1

  • 那么ES11中,引入了新的数据类型BigInt,用于表示大的整数:
    • BitInt的表示方法是在数值的后面加上n
1

空值合并操作符

  • ES11,Nullish Coalescing Operator增加了空值合并操作符:
1

可选链

  • 可选链也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:
1

Global This

  • 在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的
  • 比如在浏览器中可以通过this、window来获取;
  • 比如在Node中我们需要通过global来获取;
1

  • 在ES11中对获取全局对象进行了统一的规范:globalThis
1

for..in标准化

  • 在ES11之前,虽然很多浏览器支持for…in来遍历对象类型,但是并没有被ECMA标准化。
  • 在ES11中,对其进行了标准化,for…in是用于遍历对象的key的:
1

其他内容

  • Dynamic Import:后续ES Module模块化中讲解。
  • Promise.allSettled:后续讲Promise的时候讲解。
  • import meta:后续ES Module模块化中讲解。

ES12

FinalizationRegistry

  • FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。
  • FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer );
  • 你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;
1

WeakRefs

  • 如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:
  • 如果我们希望是一个弱引用的话,可以使用WeakRef;
1

其他内容

  • Numeric Separator:讲过了;
  • String.replaceAll:字符串替换;

ES13

method.at()

  • 前面我们有学过字符串、数组的at方法,它们是作为ES13中的新特性加入的:
1

Object.hasOwn()

  • Object中新增了一个静态方法(类方法): hasOwn(obj, propKey)
    • 该方法用于判断一个对象中是否有某个自己的属性;
    • 那么和之前学习的Object.prototype.hasOwnProperty有什么区别呢?
      • 区别一:防止对象内部有重写hasOwnProperty
      • 区别二:对于隐式原型指向null的对象, hasOwnProperty无法进行判断
1

class类成员字段

  • 在ES13中,新增了定义class类中成员字段(field)的其他方式:
    • Instance public fields
    • Static public fields
    • Instance private fields
    • static private fields
    • static block
1

Proxy使用详解

监听对象的操作

  • 我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程

    • 通过我们前面所学的知识,能不能做到这一点呢?
    • 其实是可以的,我们可以通过之前的属性描述符中的存储属性描述符来做到;
  • 左边这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对属性的操作进行监听。
    但是这样做有什么缺点呢?

    • 首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。
      • 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
    • 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty是无能为力的。
  • 所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

1

Proxy基本使用

  • 在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
  • 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
  • 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
  • 我们可以将上面的案例用Proxy来实现一次:
    • 首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;
      • const p = new Proxy(target, handler)
  • 其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;
1

Proxy的set和get捕获器

  • 如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):

  • set和get分别对应的是函数类型;

    • set函数有四个参数:
      • target:目标对象(侦听的对象);
      • property:将被设置的属性key;
      • value:新属性值;
      • receiver:调用的代理对象;
    • get函数有三个参数:
      • target:目标对象(侦听的对象);
      • property:被获取的属性key;
      • receiver:调用的代理对象;
1

Proxy的所有捕获器

1674720558176

construct和apply

  • 当然,我们还会看到捕捉器中还有construct和apply,它们是应用于函数对象的:
1

Reflect使用详解

Reflect的作用

  • Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
  • 那么这个Reflect有什么用呢?
    • 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
    • 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
    • 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
  • 如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
    • 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
    • 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
    • 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
    • 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
    • 另外在使用Proxy时,可以做到不操作原对象;
  • 那么Object和Reflect对象之间的API关系,可以参考MDN文档:

Reflect的常见方法

1674720719657

Reflect的使用

  • 那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
1

Receiver的作用

  • 我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
    • 如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;
1

Reflect的construct

1

迭代器与生成器详解

什么是迭代器

  • 迭代器(iterator),使用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
    • 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
    • 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
  • 从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
  • 在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):
    • 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;
    • 在JavaScript中这个标准就是一个特定的next方法;
  • next方法有如下的要求:
    • 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
    • done(boolean)
      • 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
      • 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
    • value
      • 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
1

可迭代对象

  • 但是上面的代码整体来说看起来是有点奇怪的:
    • 我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
    • 事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
  • 什么又是可迭代对象呢?
    • 它和迭代器是不同的概念;
    • 当一个对象实现了iterable protocol协议时,它就是一个可迭代对象;
    • 这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性;
  • 当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?
    • 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作;
    • 比如 for…of 操作时,其实就会调用它的 @@iterator 方法;
1

原生迭代器对象

  • 事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:
    • String、Array、Map、Set、arguments对象、NodeList集合;
1

可迭代对象的应用

  • 那么这些东西可以被用在哪里呢?
    • JavaScript中语法:for …of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment);
    • 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);
    • 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
1

自定义类的迭代

  • 在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象:

    • 在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:
    • 如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候我们就可以添加上 @@iterator 方法;
  • 案例:创建一个classroom的类

    • 教室中有自己的位置、名称、当前教室的学生;
    • 这个教室可以进来新学生(push);
    • 创建的教室对象是可迭代对象;
1

迭代器的中断

  • 迭代器在某些情况下会在没有完全迭代的情况下中断:
    • 比如遍历的过程中通过break、return、throw中断了循环操作;
    • 比如在解构的时候,没有解构所有的值;
  • 那么这个时候我们想要监听中断的话,可以添加return方法:
1

什么是生成器

  • 生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。

    • 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
  • 生成器函数也是一个函数,但是和普通的函数有一些区别:

    • 首先,生成器函数需要在function的后面加一个符号:*
    • 其次,生成器函数可以通过yield关键字来控制函数的执行流程:
    • 最后,生成器函数的返回值是一个Generator(生成器):
      • 生成器事实上是一种特殊的迭代器;
      • MDN:Instead, they return a special type of iterator, called a Generator

生成器函数执行

  • 我们发现下面的生成器函数foo的执行体压根没有执行,它只是返回了一个生成器对象。
    • 那么我们如何可以让它执行函数中的东西呢?调用next即可;
    • 我们之前学习迭代器时,知道迭代器的next是会有返回值的;
    • 但是我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果;
1

next函数

  • 函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
    • 答案是可以的;
    • 我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;
    • 注意:也就是说我们是为本次的函数代码块执行提供了一个值;
1

return函数

  • 还有一个可以给生成器函数传递参数的方法是通过return函数:
    • return传值后这个生成器函数就会结束,之后调用next不会继续生成值了;
1

throw函数

  • 除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:
    • 抛出异常后我们可以在生成器函数中捕获异常;
    • 但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;
1

生成器替代迭代器

  • 我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
1

  • 事实上我们还可以使用yield*来生产一个可迭代对象:
    • 这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;
1

生成器实现

  • 在之前的自定义类迭代中,我们也可以换成生成器:
1

  • 既然生成器是一个迭代器,那么我们可以对其进行如下的操作:
1

异步处理方案

  • 学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
  • 案例需求:
    • 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
    • 第二次的请求url依赖于第一次的结果;
    • 第三次的请求url依赖于第二次的结果;
    • 依次类推;
1

Generator方案

  • 但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?
1

  • 目前我们的写法有两个问题:
    • 第一,我们不能确定到底需要调用几层的Promise关系;
    • 第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?
  • 所以,我们可以封装一个工具函数execGenerator自动执行生成器函数:
1

async/await

异步函数 async function

  • async关键字用于声明一个异步函数:
  • async是asynchronous单词的缩写,异步、非同步;
  • sync是synchronous单词的缩写,同步、同时;
  • async异步函数可以有很多中写法:
1

异步函数的执行流程

  • 异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。

  • 异步函数有返回值时,和普通函数会有区别:

    • 情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中;
    • 情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定;
    • 情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;
  • 如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;

await关键字

  • async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。

  • await关键字有什么特点呢?

    • 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise;
    • 那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;
  • 如果await后面是一个普通的值,那么会直接返回这个值;

  • 如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;

  • 如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值;

进程和线程

进程和线程

  • 线程和进程是操作系统中的两个概念:
    • 进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式;
    • 线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中;
  • 听起来很抽象,这里还是给出我的解释:
    • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);
    • 线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;
    • 所以我们也可以说进程是线程的容器;
  • 再用一个形象的例子解释:
    • 操作系统类似于一个大工厂;
    • 工厂中里有很多车间,这个车间就是进程;
    • 每个车间可能有一个以上的工人在工厂,这个工人就是线程;

1674722890059

操作系统的工作方式

  • 操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?
  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
  • 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;
  • 对于用户来说是感受不到这种快速的切换的;
  • 你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程:

1674722932898

浏览器中的JS线程

  • 我们经常会说JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。
  • 浏览器是一个进程吗,它里面只有一个线程吗?
  • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;
  • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;
  • JavaScript的代码执行是在一个单独的线程中执行的:
  • 这就意味着JavaScript的代码,在同一个时刻只能做一件事;
  • 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;
  • 所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:
  • 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;
  • 比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可;

浏览器的事件循环

浏览器的事件循环

  • 如果在执行JavaScript代码的过程中,有异步操作呢?
  • 中间我们插入了一个setTimeout的函数调用;
  • 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行;

1674723346457

宏任务和微任务

  • 但是事件循环中并非只维护着一个队列,事实上是有两个队列:
  • 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
  • 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
  • 那么事件循环对于两个队列的优先级是怎么样的呢?
    • 1.main script中的代码优先执行(编写的顶层script代码);
    • 2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
      •  也就是宏任务执行之前,必须保证微任务队列是空的;
      •  如果不为空,那么就优先执行微任务队列中的任务(回调);

常见面试题

1

Node的事件循环

Node的事件循环

  • 浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。
  • 这里我们来给出一个Node的架构图:
  • 我们会发现libuv中主要维护了一个EventLoop和worker threads(线程池);
  • EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等
  • libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方;

Node事件循环的阶段

  • 我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:
  • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
  • 但是一次完整的事件循环Tick分成很多个阶段:
  • 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。
  • idle, prepare:仅系统内部使用。
  • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;
  • 检测(check):setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)。

1674723568788

宏任务和微任务

  • 我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:
  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
  • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;
  • 但是,Node中的事件循环不只是 微任务队列和 宏任务队列:
  • 微任务队列:
    •  next tick queue:process.nextTick;
    •  other queue:Promise的then回调、queueMicrotask;
  • 宏任务队列:
    •  timer queue:setTimeout、setInterval;
    •  poll queue:IO事件;
    •  check queue:setImmediate;
    •  close queue:close事件;

Node事件循环顺序

  • 所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:
  • next tick microtask queue;
  • other microtask queue;
  • timer queue;
  • poll queue;
  • check queue;
  • close queue;

常见面试题

1

错误处理方案

错误处理方案

  • 开发中我们会封装一些工具函数,封装之后给别人使用:
    • 在其他人使用的过程中,可能会传递一些参数;
    • 对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果;
  • 很多时候我们可能验证到不是希望得到的参数时,就会直接return:
    • 但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined;
    • 事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道函数内部报错了;
  • 如何可以让一个函数告知外界自己内部出现了错误呢?
    • 通过throw关键字,抛出一个异常;
  • throw语句:
    • throw语句用于抛出一个用户自定义的异常;
    • 当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行);
  • 如果我们执行代码,就会报错,拿到错误信息的时候我们可以及时的去修正代码。

throw关键字

  • throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息:

  • throw关键字可以跟上哪些类型呢?

  • 基本数据类型:比如number、string、Boolean

  • 对象类型:对象类型可以包含更多的信息

  • 但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类

1

Error类型

  • 事实上,JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象:
1

  • Error包含三个属性:
    • messsage:创建Error对象时传入的message;
    • name:Error的名称,通常和类的名称一致;
    • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;
  • Error有一些自己的子类:
    • RangeError:下标值越界时使用的错误类型;
    • SyntaxError:解析语法错误时使用的错误类型;
    • TypeError:出现类型错误时,使用的错误类型;

异常的处理

  • 我们会发现在之前的代码中,一个函数抛出了异常,调用它的时候程序会被强制终止:
  • 这是因为如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传递到上一个函数调用中;
  • 而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行;
  • 我们先来看一下这段代码的异常传递过程:
  • foo函数在被执行时会抛出异常,也就是我们的bar函数会拿到这个异常;
  • 但是bar函数并没有对这个异常进行处理,那么这个异常就会被继续传递到调用bar函数的函数,也就是test函数;
  • 但是test函数依然没有处理,就会继续传递到我们的全局代码逻辑中;
  • 依然没有被处理,这个时候程序会终止执行,后续代码都不会再执行了;
1

异常的捕获

  • 但是很多情况下当出现异常时,我们并不希望程序直接推出,而是希望可以正确的处理异常:
  • 这个时候我们就可以使用try catch

1674723910194

  • 在ES10(ES2019)中,catch后面绑定的error可以省略。
  • 当然,如果有一些必须要执行的代码,我们可以使用finally来执行:
  • finally表示最终一定会被执行的代码结构;
  • 注意:如果try和finally中都有返回值,那么会使用finally当中的返回值;

本地存储Storage

认识Storage

  • WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式:
  • localStorage:本地存储,提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留;
  • sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除;

1674723980844

  • 我们会发现localStorage和sessionStorage看起来非常的相似。
  • 那么它们有什么区别呢?
    • 验证一:关闭网页后重新打开,localStorage会保留,而sessionStorage会被删除;
    • 验证二:在页面内实现跳转,localStorage会保留,sessionStorage也会保留;
    • 验证三:在页面外实现跳转(打开新的网页),localStorage会保留,sessionStorage不会被保留;

Storage常见方法和属性

  • Storage有如下的属性和方法:
    • 属性:
      • Storage.length:只读属性
      •  返回一个整数,表示存储在Storage对象中的数据项数量;
    • 方法:
      • Storage.key(index):该方法接受一个数值n作为参数,返回存储中的第n个key名称;
      • Storage.getItem():该方法接受一个key作为参数,并且返回key对应的value;
      • Storage.setItem():该方法接受一个key和value,并且将会把key和value添加到存储中。
        •  如果key存储,则更新其对应的值;
      • Storage.removeItem():该方法接受一个key作为参数,并把该key从存储中删除;
      • Storage.clear():该方法的作用是清空存储中的所有key;

正则表达式(空)

防抖与节流

认识防抖和节流函数

  • 防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中

    • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
    • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
  • 防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。

  • 但是很多前端开发者面对这两个功能,有点摸不着头脑:

    • 某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);
    • 某些开发者可以区分,但是不知道如何应用;
    • 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;
  • 接下来我们会一起来学习防抖和节流函数:

    • 我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;
    • 并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;

认识防抖debounce函数

  • 我们用一副图来理解一下它的过程:
    • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
    • 当事件密集触发时,函数的触发会被频繁的推迟;
    • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

1674724232907

  • 防抖的应用场景很多:
    • 输入框中频繁的输入内容,搜索或者提交信息;
    • 频繁的点击按钮,触发某个事件;
    • 监听浏览器滚动事件,完成某些特定操作;
    • 用户缩放浏览器的resize事件;

防抖函数的案例

  • 我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容:

1674724643150

  • 比如想要搜索一个MacBook:
    • 当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
    • 当继续输入ma时,再次发送网络请求;
    • 那么macbook一共需要发送7次网络请求;
    • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;
  • 但是我们需要这么多次的网络请求吗?
    • 不需要,正确的做法应该是在合适的情况下再发送网络请求;
    • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;
    • 比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;
    • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;
  • 这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;

案例准备

  • 我们通过一个搜索框来延迟防抖函数的实现过程:
    • 监听input的输入,通过打印模拟网络请求
  • 测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:

Underscore库的介绍

  • 事实上我们可以通过一些第三方库来实现防抖操作:
    • lodash
    • underscore
  • 这里使用underscore
    • 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
    • 但是目前我看到underscore还在维护,lodash已经很久没有更新了;
  • Underscore的官网: https://underscorejs.org/
  • Underscore的安装有很多种方式:
    • 下载Underscore,本地引入;
    • 通过CDN直接引入;
    • 通过包管理工具(npm)管理安装;
  • 这里我们直接通过CDN:
1
<scriptsrc="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
1

认识节流throttle函数

  • 我们用一副图来理解一下节流的过程
    • 当事件触发时,会执行这个事件的响应函数;
    • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
    • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;

1674724371890

  • 节流的应用场景:
    • 监听页面的滚动事件;
    • 鼠标移动事件;
    • 用户频繁点击按钮操作;游戏中的一些设计;

节流函数的应用场景

  • 很多人都玩过类似于飞机大战的游戏

  • 在飞机大战的游戏中,我们按下空格会发射一个子弹:

    • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;
    • 比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;
    • 但是事件是触发了10次的,响应的函数只触发了一次;
  • 生活中防抖的例子:

    • 比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
    • 如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
      • 在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
      • 如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);
  • 生活中节流的例子:

    • 比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;
    • 如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;

自定义防抖节流函数

  • 我们按照如下思路来实现:
    • 防抖基本功能实现:可以实现防抖效果
    • 优化一:优化参数和this指向
    • 优化二:优化取消操作(增加取消功能)
    • 优化三:优化立即执行效果(第一次立即执行)
    • 优化四:优化返回值
  • 我们按照如下思路来实现:
    • 节流函数的基本实现:可以实现节流效果
    • 优化一:节流最后一次也可以执行
    • 优化二:优化添加取消功能
    • 优化三:优化返回值问题
1

自定义深拷贝函数

  • 前面我们已经学习了对象相互赋值的一些关系,分别包括:
    • 引入的赋值:指向同一个对象,相互之间会影响;
    • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
    • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;
  • 前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse
    • 这种深拷贝的方式其实对于函数、Symbol等是无法处理的;
    • 并且如果存在对象的循环引用,也会报错的;
  • 自定义深拷贝函数:
    • 1.自定义深拷贝的基本功能;
    • 2.对Symbol的key进行处理;
    • 3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;
    • 4.对循环引用的处理;
1

自定义事件总线

  • 自定义事件总线属于一种观察者模式,其中包括三个角色:
    • 发布者(Publisher):发出事件(Event);
    • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
    • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;
  • 当然我们可以选择一些第三方的库:
    • Vue2默认是带有事件总线的功能;
    • Vue3中推荐一些第三方库,比如mitt;
  • 当然我们也可以实现自己的事件总线:
    • 事件的监听方法on;
    • 事件的发射方法emit;
    • 事件的取消监听off;
1