Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

根据前端技能树补缺JS知识

作用域

作用域(scope)规定了变量能够被访问的范围,离开范围后变量即不能使用

作用域分为:

  • 全局作用域
  • 局部作用域

局部作用域

局部作用域可分为两类:

  • 函数作用域
  • 块作用域

函数作用域

在函数内部声明的变量只有函数内可以访问

1
2
3
4
function fun() {
const num = 10
}
console.log(num) // 报错
  • 函数内部声明的变量,外部无法访问
  • 函数参数也是函数内部的局部变量
  • 不同函数内部声明的变量无法访问
  • 函数执行完毕后,内部变量实际被清空了

块作用域

JS中被{}包裹的部分称为代码块,块外的代码有可能无法访问

1
2
3
4
for (let i = 1; i <= 3; i++) {
console.log(i)
}
console.log(i) // 报错
  • let声明的变量回产生块作用域,var则不会产生块作用域
  • const声明的变量也会产生块作用域
  • 不同代码间的变量无法互相访问
  • 推荐使用let和const
  • 块作用域是ES6后新加入的

全局作用域

<script>标签和.js文件的最外层就是所谓全局作用域,全局作用域中声明的变量在任何地方都可以被访问

1
2
3
4
5
6
<script>
const num = 10
function fn() {
console.log(num) // 10
}
</script>
  • 为window对象动态添加的属性默认也为全局作用域(不推荐
  • 函数中未使用任何关键字声明的变量为全局作用域(不推荐
  • 尽可能少使用全局变量,防止变量污染

作用域链

作用域链的本质是变量查找机制

  • 在函数执行时,会优先查找当前作用域中的变量
  • 如果当前作用域查找不到,则会依次逐级查找父级作用域直到全局作用域

即:

  • 嵌套关系的作用域串联行程了作用域链
  • 相同作用域链中按从小到大的规则查找变量
  • 子作用域能够访问父作用域,父作用域无法访问子作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
let a = 1
let b = 2
function f() {
let a = 1
function g() {
a = 2
console.log(a)
}
g()
}
f() // 输出2
</script>

垃圾回收机制

垃圾回收机制即(Garbage Collection,GC)

JS中内存的分配和回收是自动完成的,内存在不使用的时候会被垃圾回收器自动回收

内存生命周期

JS分配内存的过程一般经过以下三个生命周期:

  1. 内存分配:声明变量、函数、对象时,系统会自动分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存销毁:使用完毕,由垃圾回收器自动回收不再使用的内存

注意:

  • 全局变量一般不会回收(关闭页面才回收
  • 一般情况下局部变量的值,不用了会被自动回收

内存泄露:程序中分配的内存由于某种原因程序未释放无法释放叫作内存泄漏

算法说明

堆栈空间分配区别:

  1. 栈(操作系统):由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型都存放在栈中
  2. 堆(操作系统):一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放在堆中

目前浏览器中常用的垃圾回收算法有:

  • 引用计数法
  • 标记清除法

引用计数法

IE采用的算法,定义内存不再使用,就是看一个对象是否有指向它的引用,没有引用了就回收对象:

算法
  1. 跟踪记录被引用的次数
  2. 如果被引用了一次,那么记录次数1,多次引用会累加++
  3. 如果减少一个引用就减1
  4. 如果引用为0,则释放内存
问题

引用计数存在一个致命问题:嵌套引用(循环引用)

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器也不会进行回收,则会导致内存泄漏

1
2
3
4
5
6
7
8
function fn() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return '引用计数无法回收'
}
fn()

此时两个对象的引用计数一直为1,这样的相互引用则会造成大量的内存泄露

标记清除法

现代浏览器中已经不再使用引用计数法了。

标记清除法是对其的改进

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

由于查找对象必须从根部出发,因此对于相互引用的情况,由于其无法从根部出发找到,因此均会被回收

image-20230714162423585

闭包

闭包Closure

概念:一个函数堆周围状态的引用捆绑在一起,内层函数中访问到其他外层函数的作用域

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

最简单的函数闭包写法如下:

1
2
3
4
5
6
7
8
function outer() {
const a = 1;
function f() {
console.log(a)
}
f()
}
outer()

其中函数f和变量a构成了闭包

需要注意的是:

  • 简单的外层函数中定义里层函数是无法形成闭包的
  • 只有当里层函数访问了外层函数变量时,才会产生闭包

此时,在函数f内设置断点,在浏览器开发者页面的Sources标签中 -> Scope标签中会显示名为Local、Closure、Global的作用域

闭包作用与写法

闭包的作用:

  • 封闭数据,提供操作,外部可以访问函数内部的变量
  • 它允许将函数与其所操作的某些数据(环境)关联起来

更多情况下闭包的基本格式如下:

1
2
3
4
5
6
7
8
9
10
function outer() {
let i = 1
function fn() {
console.log(i)
}
return fn
}

const fun = outer()
fun()

闭包应用

闭包可以实现数据私有

假设现在有一个需求要记录一个函数调用的次数:

1
2
3
4
5
let i = 0
function fn() {
i++
console.log(`函数被调用了${i}次`)
}

此时虽然可以实现这样的效果,但是由于i的作用域是全局,则外部可以通过i = 1000来修改函数调用的次数。

因此我们可以使用闭包让变量i称为私有

1
2
3
4
5
6
7
8
9
10
function count() {
let i = 0
function fn() {
i++
console.log(i)
}
return fn
}
const fun = count()
fun()

此时外部已经无法修改i的值了

注意:

  • 此时调用count()()的结果始终是1,这是因为直接调用时,count()返回fn这个函数,因此相当于在全局作用域中直接访问作用域fn,这就意味着位于作用域count的变量i没有被添加到作用域链,无法被标记清除法到达,调用一次后即被销毁
  • 调用fun()的结果才会正常累加,因为执行const fun = count()时,位于全局作用域的fun访问了局部作用域count,而count访问了局部作用域fn,因此其中i是可以被标记清除算法到达的,则不会被回收,发生了内存泄露

闭包存在的问题

会引发内存泄露(详见上例

变量提升

变量提升是JS的缺陷,仅存在于var声明变量

被var声明的变量,其声明的过程(注意仅提升声明过程,初始化过程不会提升)会被提升到当前作用域的开头

即如下代码:

1
2
console.log(num) // undefined
var num = 10

相当于如下代码:

1
2
3
var num
console.log(num) // undefined
num = 10

注意事项:

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

函数进阶

函数提升

函数允许在声明之前调用:

1
2
3
4
fn()
function fn() {
console.log("函数提升")
}

解释器会把函数声明提升到当前作用域的最前面

但是使用函数表达式的形式声明函数时,函数不会被提升:

1
2
3
4
fun() // 此处报错,fun is not a function
var fun = function() {
console.log('函数表达式')
}

因为变量也具有变量提升,而此时的函数定义包含在赋值语句中,因此对于变量的提升,只提升其声明部分var fun,而对其赋值部分(即函数定义)不进行提升,因此无法提前调用

函数提升的特点:

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

函数参数

函数动态参数

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

1
2
3
4
5
6
7
8
9
10
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i]
}
console.log(sum)
}

getSum(1, 2, 3) // 6
getSum(2, 3, 4) // 9
  1. arguments是一个伪数组,只存在于函数中
  2. 1arguments的作用是动态获取函数的实参
  3. 可以通过for循环一次得到传递过来的实参

剩余参数

允许将不定数量的参数作为一个数组

1
2
3
4
5
6
7
8
9
10
function getSum(..arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
console.log(sum)
}

getSum(1, 2, 3) // 6
getSum(2, 3, 4) // 9
  1. ...是语法符号,置于函数形参之前,用于获取多余的实参
  2. 借助...的剩余实参,是个真数组

因此开发中还是提成使用剩余参数

展开运算符

展开运算符可以将一个数组进行展开

1
2
3
4
5
const a = [1, 2, 3, 4]
console.log(...a)
console.log(Math.max(...a))
const b = [5, 6, 7]
const c = [...a, ...b]
  1. 不会修改原数组
  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
const fn = (x) => {
console.log(x)
}
fn(1)

// 只有一个形参的时候可以省略小括号
const fn1 = x => {
console.log(x)
}
fn1(1)

// 函数体只有一行代码的时候可以省略大括号
const fn2 = x => console.log(x)
fn2(1)

// 函数体只有一行代码作为返回值的时候可以省略大括号和returen
const fn3 = x => x * x
fn3(2)

// 箭头函数可以直接返回一个对象,由于对象的{}与函数体的{}冲突了,因此需要用()包裹
const fn4 = (name) => ({uname: name})
fn("Ender")
  1. 箭头函数属于表达式函数,因此不存在函数提升
  2. 箭头函数只有一个参数时可以省略圆括号()
  3. 箭头函数函数体只有一行代码时可以省略花括号{},并自动做为返回值被返回
  4. 加括号的函数体返回对象字面量表达式

箭头函数的参数

箭头函数中没有动态参数arguments,但是有剩余参数...args

1
2
3
4
5
6
7
8
9
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)

箭头函数的this

箭头函数不会自己创建this,之后从自己的作用域链的上一层沿用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
console.log(this) // window
const fn = () => {
console.log(this) // window
}
fn()

// 对象方法
const obj = {
username: "Ender",
sayHi: () => console.log(this) // window
}
obj.sayHi()

// 多层嵌套
const obj2 = {
uname: "Ender",
sayHi: function() {
let i = 10
const count = () => {
console.log(this) // obj2
}
count()
}
}
obj2.sayHi()

因此在开发中需要注意,事件回调函数使用this时,this未全局的window,因此DOM事件回调函数为了简便,不推荐使用箭头函数

解构赋值

数组解构

数组解构是将数组的单元值快速批量给一系列变量的简洁语法

1
2
const arr = [100, 60, 80]
const [a, b, c] = arr

典型的应用是在变量交换:

1
2
3
4
let a = 1
let b = 3; // 注意此处必须加分号
[b, a] = [a, b]
console.log(a, b)

这里需要注意两个特殊的必须加分号的地方,详见后文

单元值少变量多

1
const [a,b,c,d] = [1,2,3] // 此时d为undefined

单元值多变量少

1
const [a,b,c] = [1,2,3,4] // a = 1, b = 2, c = 3

还可以结合剩余参数来利用这一特性:

1
const [a,b,...c] = [1,2,3,4] // a = 1, b = 2, c = [3,4]真数组

防止undefined传递

可以使用类似参数默认值的方式设置默认值

1
const [a = 0, b = 0] = [] // a = 0, b = 0

按需导入

1
2
const [a, b, , d] = [1, 2, 3, 4]
// a=1, b=2, d=4

多维数组

1
const [a, b, [c, d] = [1, 2, [3, 4]] // a=1,b=2,c=3,d=4

对象解构

解构基本解构基本语法:

1
const { uname, age } = { uname: "teacher", age: 18} // 解构时按名解构,属性名与变量名需要相同

但解构时使用的变量可以通过以下方式改名:

1
const { uname: username, age} = { uname: "teacher", age: 18} // 注意更改后的新变量名在冒号后

数组对象的混合解构:

1
const [{ uname, age }] = [{ uname: "佩奇", age: 6 }]

多层对象解构:

1
const { uname, family: { mother, father, sister} } = { uname: "佩奇", family: { father: '猪爸爸', mother: '猪妈妈', sister: '乔治'}, age: 18} // 多级解构需要指定第次级属性名

解构方法可以直接出现在函数的参数中:

1
2
3
4
5
function render({ data: myData }) {
console.log(myData)
}
const msg = {"data":[{"id": 1, "count":20},{"id": 2, "count":30}], "code": 200}
render(msg)

必须加分号的地方

  1. 理解执行函数前必须有分号:
1
2
(function(){})();
(function(){})();
  1. 数组解构的上一句为赋值语句时必须加分号:
1
2
3
4
const str = "pink"
;[1, 2, 3].map(function(item) {
console.log(item)
})

forEach

forEach用于遍历某个对象,与map的区别在于只遍历不返回

1
2
3
4
5
6
const arr = ['red', 'green', 'blue']
arr.forEach(function(item, index){
console.log(item) // 元素
console.log(index) // 索引号
})
// 索引号index为可选参数

forEach适合于遍历数组对象,在复杂的数组遍历过程中更方便

filter

根据筛选条件返回新数组

1
2
3
4
5
const arr = [10, 20, 30]
arr.filter(function (item, index) {
return item >= 20
})
// 返回值为bool类型

reduce

累计器,返回累计处理的结果,经常用于求和

1
2
3
// arr.reduce(function(上一次的值, 当前值){}, 起始值)
const arr = [1, 5, 8]
const total = arr.reduce((prev, current) => parev + current, 10) // 24

reduce的执行过程:

  1. 如果没有初始值,则上一次值以数组的第一个数组元素的值作为自己的值
  2. 每一次循环,把返回值给作为下一次循环的上一次值
  3. 如果有起始值,则起始值作为上一次的值
  4. 即如果没有起始值,则长度为n的数组,只需要循环n-1次

需要注意处理对象累加的情况:

1
2
3
4
5
6
7
8
9
10
11
12
const Obj = [{
name: "a",
salary: 10000
},{
name: "b",
salary: 10000
},{
name: "c",
salary: 10000
}]

const total = Obj.reduce((prev, current) => prev + current.salary, 0) // 可以利用第二个参数来使得prev和返回值的数据类型相同

数组方法总结

方法 作用 说明
forEach 遍历数组 不返回数组,经常用于遍历数组元素
map 迭代数组 返回新数组,返回处理之后的数组元素构成的新数组
filter 过滤数组 返回新数组,返回满足筛选条件的数组元素构成的新数组
reduce 累计器 返回累计处理的结果,常用于求和操作

对象

创建对象的三种方式

  1. 利用对象字面量创建对象
1
2
3
const obj = {
name: "ender"
}
  1. 利用new Object创建对象
1
2
const obj = new Object()
obj.uname = "ender"
  1. 构造函数创建

构造函数

一种特殊的函数,用于初始化对象

可以通过构造函数来创建多个类似的对象

1
2
3
4
5
6
7
function Pig(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}

const pepa = new Pig("佩奇", 6, "girl")

构造函数用如下两个约定来约束:

  1. 命名以大写字母开头
  2. 只能由new操作符来执行

注意事项:

  1. 使用new关键字调用函数的行为被称为实例化
  2. 实例化构造函数时没有参数可以省略
  3. 构造函数内部无需写return,返回值即为新创建的对象
  4. 构造函数内部的return返回值无效

new的执行过程

  1. 创建新对象
  2. 构造函数this指向新对象
  3. 执行构造函数代码,修改this,添加新属性
  4. 返回新对象

实例成员与静态成员

  1. 实列成员即实例化的对象具有的方法或属性
  2. 静态成员即构造函数具有的方法或属性
    • 静态成员只能通过构造函数访问
    • 静态方法中的this指向构造函数
1
2
3
4
5
6
7
8
9
function Pig(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}
Pig.eyes = 2
Pig.sayHi = function() {
console.log("Hi")
}

内置构造函数

引用数据类型

  • Object
    • Object.keys(obj)获取对象中所有属性名
    • Object.values(obj)获取对象中所有属性值
    • Object.assign(newObj, oldObj)对象拷贝
  • Array
  • RegExp
  • Date

包装数据类型

  • String
  • Number
  • Boolean

数组常见方法

方法 作用 说明
join 元素拼接 将数组元素拼接为字符串,连接符作为参数传递,返回字符串
find 查找元素 返回符合条件的第一个数组元素,如果没有则返回undefined
every 全量检测 检测数组中是否所有元素都满足条件,通过返回true,否则返回false
some 存量检测 检测数组中是否存在一个元素满足条件,通过返回true,否则返回false
concat 数组合并 合并两个数组,返回合并后的新数组
sort 数组排序 原数组元素排序
splice 删除替换 删除或替换原数组元素
reverse 反转数组 反转原数组
findIndex 查找索引 查找元素索引值
fill 数组填充 从某位置开始(参数2)到某位置结束(参数3)填充为某值(参数1),返回修改后的数组
Array.from() 伪数变真 将伪数组转换为真数组,返回真数组

字符串常见方法

方法 作用 说明
length 字符串长度 与join相反
splt('分隔符') 字符串拆分
substring(开始索引号[, 结束索引号]) 字符串截取 substr已被弃用,省略结束索引时,从开始索引一直截取到结束,注意结束索引为开区间
startsWith(子字符串[, 开搜索位置]) 是否以某字符开头,区分大小写 开搜位置从0开始,从开始位置匹配,以子字符串开头返回true,否则返回false
includes(搜索字符串[, 检测位置索引号]) 是否包含,区分大小写 位置索引默认为0,从位置索引开始匹配,匹配到了返回true,否则返回false
toUpperCase() 字母转换大写
toLowerCase() 字母转换小写
indexOf() 检查是否包含某字符
endsWith() 是否以某字符串结尾 startsWith相同
replace() 替换字符串 支持正则表达式
match() 查找字符串 支持正则表达式

数字常见方法

方法 作用 说明
toFixed(n) 保留n位小数 以四舍五入的保留,不够位数则以0填充,返回值为字符串

注意在JS小数运算会出现精度问题,尽量转换为整数后计算

例如0.1 + 0.2计算会出现很长的小数位

因此使用(0.1 * 10 + 0.2 * 10) / 10

面向对象

变成思想

  1. 面向过程
    • 分析出解决问题需要的步骤
    • 实现每一个步骤
    • 依次调用步骤
    • 优点
      • 性能更高
      • 适合于根硬件很紧密的东西
    • 缺点
      • 不易维护
      • 不易复用
      • 不易扩展
  2. 面向对象
    • 把事务分解为对象
    • 对象之间分工合作
    • 优点:
      • 灵活
      • 代码可复用
      • 容易维护和开发
    • 缺点
      • 性能低
    • 特性:
      • 抽象
      • 封装性
      • 继承性
      • 多态性

构造函数

JS通过狗咱函数来实现封装

因此:

  1. 构造函数体现了面向对象的封装特性
  2. 构造函数创建的对象彼此独立互不影响
1
2
3
4
5
6
7
8
9
10
function ClassName(attr1, attr2) {
this.attr1 = attr1
this.attr2 = attr2
this.method = function(){
console.log("Hello")
}
}

const obj1 = new ClassName('obj1', '1')
const obj2 = new ClassName('obj2', '2')

但是使用这种方式创建对象是,存在内存浪费

原因是虽然obj1和obj2中的method完全一致

但是当我们使用构造函数new两个不同的对象时,依然会为这两个对象中的method分别分配内存

1
console.log(obj1.method === obj2 === method) // false

原型对象prototype

因此原型则用于解决这样的浪费内存的问题

构造函数通过原型来分配的函数是所有对象共享的

  1. 所有构造函数都包含prototype属性,指向一个对象,称为原型对象
  2. 改对象可以挂载函数,对象实例化不会多次创建原型上函数,以节约内存
  3. 将不变的方法定义在prototype对象上,以实现共享
  4. 构造函数和原型对象中的this都指向实例化对象
1
2
3
4
5
6
7
8
9
10
11
12
function ClassName(attr1, attr2) {
this.attr1 = attr1
this.attr2 = attr2
}
ClassName.prototype.method = function(){
console.log("Hello")
}

const obj1 = new ClassName('obj1', '1')
const obj2 = new ClassName('obj2', '2')

console.log(obj1.method === obj2 === method) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let that
function ClassName(attr1) {
that = hits
this.attr1 = attr1
}
const obj = new ClassName("obj")
console.log(that === obj) // true

ClassName.prototype.method = function(){
that = this
console.log("Hello")
}
const obj2 = new ClassName("obj2")
console.log(that === obj2) // true

例如我们可以使用原型对象为数组添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Array.prototype.max = function() {
return Math.max(...this)
}

Array.prototype.min = function() {
return Math.min(...this)
}

Array.prototype.sum = function() {
return this.reduce((prev, current) => prev + current, 0)
}

console.log([2, 5, 9].max())
const arr = [1, 2, 3]
console.log(arr.max())
console.log(arr.sum())

constructor属性

  1. 每个原型对象中都有一个constructor属性
  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
console.log(Array.constructor) // Array包含Constructor
Array.prototype = {
max: function() {
return Math.max(...this)
},
min: function() {
return Math.min(...this)
},
sum: function() {
return this.reduce((prev, current) => prev + current, 0)
}
}
console.log(Array.constructor)
// 此时Array的原型对象将对视构造函数,因为直接进行了对象值的覆盖
// 因此这样赋值时需要将原来的Construct加上
Array.prototype = {
constructor: Array // 加上当前构造函数的指向
max: function() {
return Math.max(...this)
},
min: function() {
return Math.min(...this)
},
sum: function() {
return this.reduce((prev, current) => prev + current, 0)
}
}
console.log(Array.constructor)

对象原型__proto__

对象原型即对象的原型

每个实例对象中都有一个属性__proto__指向原型对象prototype

之所以实例化对象可以访问构造函数的prototype原型对象中的方法

就是因为对象具有__proto__的存在

image-20230727160555859

__proto__是JS非标准属性,即不同浏览器的名称可能不同

例如谷歌内核的浏览器使用[[prototype]]来表示__proto__

该属性是只读的

查看时依然使用obj.__proto__查看

其中也有一个constructor,指向创建该实例对象的构造函数

对象原型VS原型对象

名称 表示 指向 construct this 说明
原型对象 prototype 指向原型对象 指向原型对象的构造函数 指向实例化对象 可以挂载函数,节约内存
对象原型 __proto__ 指向prototype(同上 同上 只读

原型继承

JS中利用原型对象来实现继承,因此称为原型继承

深浅拷贝

进行拷贝时,由于对象类型的数据是引用数据,因此进行简单的赋值无法为新变量开辟新的内存空间,因此对新对象的修改将会导致原对象也发生改变。

1
2
3
4
5
6
7
8
9
const obj = {
uname: 'red',
age: 18
}
const o = obj
console.log(o)
o.age = 20
console.log(o) // age = 20
console.log(obj) // age = 20

因此针对引用数据类型出现了浅拷贝和深拷贝

浅拷贝

浅拷贝:只拷贝地址

常见的浅拷贝方法:

  1. 拷贝对象:Object.assgin() 或者 展开运算符{…obj}
  2. 拷贝数组:Array.prototyope.concat() 或者 […arr]
1
2
3
4
5
6
7
8
9
10
const obj = {
uname: 'red',
age: 18
}
const o = { ...obj }
// Object.assign(o, obj) 与使用assign等效
console.log(o)
o.age = 20
console.log(o) // age = 20
console.log(obj) // age = 18

以这种方式则是使用{}先创建了新的对象,因此进行的是将对旧象内的简单数据类型赋值给新对象的同名属性。因此不会出现同步修改的问题。

但如果其中遇到的对象中的某个属性是复杂数据类型时,由于直接将地址赋值给新对象中的同名属性。

因此其中的对象内容仍然会同步修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
uname: 'red',
age: 18,
family: {
uname: "baby1"
}
}
const o = { ...obj }
// Object.assign(o, obj) 与使用assign等效
console.log(o)
o.age = 20
o.famuly.baby = 'baby2'
console.log(o) // age = 20 family.baby = 'baby2'
console.log(obj) // age = 18 family.baby = 'baby2'

因此浅拷贝只适合拷贝单层复杂引用类型

深拷贝

深拷贝拷贝对象

常见的实现方法有:

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.strngify()实现

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 处理数组
if(oldObj[k] instanceof Array) {
newObj[k] = []
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
} else {
// k 属性名 oldObj[k]属性值
newObj[k] = oldObj[k]
}
}
}

做过深拷贝吗,说一下深拷贝是怎么实现的

  1. 深拷贝需要做到拷贝得到的新对象的修改不会影响旧对象
  2. 要想实现深拷贝需要用到函数递归
  3. 当进行普通数据的函数,拷贝是直接赋值即可
  4. 但遇到数组、对象这类引用数据类型时,需要再次调用递归函数
  5. 需要注意的是要先处理数组,再处理对象

lodash

使用lodash中的clonecloneDeep函数

1
2
3
4
5
6
7
let obj = [
{'a': 1},
{'b': 2}
];

let deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]) // false

利用JSON实现深拷贝

1
2
3
4
5
6
7
8
let obj = [
{'a': 1},
{'b': 2}
];


// 即先将对象序列化为字符串,然后从字符串解析并创建一个新对象
const o = JSON.parse(JSON.stringify(obj))

异常处理

throw

预估代码中可能出现的异常以方便调试

1
2
3
4
5
6
7
8
function fn(x, y) {
if(!x || !y) {
throw new Error('没有传递参数')
}

return x + y
}
console.log(fn())
  1. 抛出异常时程序也会终止
  2. throw后面跟的时错误提示信息
  3. Error对象配合throw使用,能设置更详细的错误信息

try/catch/finally

try用于捕获浏览器提供的错误信息

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
function fn(x, y) {
try {
// 可能出现错误的代码
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (err) {
// err中保存了浏览器提供的错误信息

// 拦截错误,提示浏览器提供的错误信息
// 不会中断程序的执行
console.log(err.message)

// 也可以配合throw使用
throw new Error('报错啦!')

// 如果要中断这个函数,需要使用return
return
} final {
// 不论有没有异常,finally中的块一定会执行
alert('弹出对话框')
}

return x + y
}
console.log(fn())
  1. try…catch用于捕获错误信息
  2. 将预估可能发生错误的代码写在try代码块中
  3. 如果try代码块中出现错误,就会执行catch代码块,并截获错误信息
  4. finally代码块不管是否有错误都会执行

debugger

debugger是JS中的一个关键字,即相当于在这个位置下了一个断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deepCopy(newObj, oldObj) {
// 运行至此浏览器会自动暂停显示断点信息
debugger
for (let k in oldObj) {
// 处理数组
if(oldObj[k] intanceof Array) {
newObj[k] = []
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] intanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
} else {
// k 属性名 oldObj[k]属性值
newObj[k] = oldObj[k]
}
}
}

this指向

普通函数this

谁调用函数,this就指向谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 普通函数
function fun() {
console.log(this)
}

fun() // this指向window,普通函数都由window调用

// 对象方法
const user = {
name: 'ender',
walk: function () {
console.log(this)
}
}
user.walk() // this指向user

// 严格模式
'use strict'
function fn() {
console.log(this)
}
fn() // undefined 严格模式下没有调用者时值为this

需要注意的是,在严格模式下,没有调用者时,this的值为undefined

箭头函数this

  1. 箭头函数不具备自己的this
  2. 箭头函数默认将this绑定为外层的this,即最近作用域的this
  3. 向外层作用域一层一层查找this,直到this有定义
1
2
3
4
5
6
7
8
const user = {
name: 'ender',
walk: () => {
console.log(this)
}
}

user.walk() // this指向window,因为外层user没有this,最外层this为widnow

不推荐使用箭头函数的情况

绑定DOM事件

在操作DOM时,使用箭头函数作为回调函数绑定事件,将会导致this无指向错误的问题:

1
2
3
4
5
6
7
8
9
const btn = document.querySelect('.btn')

btn.addeventListener('click', () => {
console.log(this) // window
})

btn.addeventListener('click', () => {
console.log(this) // .btn的DOM对象
})
基于原型的面向对象

基于原型的面向对象不推荐使用箭头函数

1
2
3
4
5
6
7
8
function Person() {}

Person.prototype.walk() = () => {
console.log(this + '正在走路')
}

const p1 = new Person()
p1.walk() // this指向window

因为构造函数和原型对象中的this均指向实例

改变this

call

调用函数的同时为函数指定this

1
fun.call(thisArg, arg1, arg2, ...)
  • thisArg:fun函数中的this的指
  • arg1, arg2:fun函数的参数
  • 返回值就是函数的返回值

apply

调用函数的同时为函数指定this

1
fun.apply(thisArg, [argsArray])
  • thisArg:fun函数中的this的指
  • argsArray:可选参数,fun函数参数,传递的值必须包含在数组中
  • 返回值就是函数的返回值
  • 因此apply主要和数组有关,比如利用Math.max()求数组最大值
1
const max = Math.max.apply(Math, [1, 2, 3])

bind

不调用函数,改变函数this的指向

1
fun.bind(thisArg, arg1, arg2, ...)
  • thisArg:fun函数中的this的指
  • arg1, arg2:fun函数的参数
  • 返回由指定的this值和初始化参数改造的原函数拷贝(新函数
  • 因此当想要改变this指向,却不想调用函数时使用,比如改变定时器内部的this指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 需求,一个按钮,点击则禁用,2秒后开启
const btn = document.querySelect('.btn')

btn.addeventListener('click', function() {
// 禁用按钮
this.disabled = true
// 计时器
setTimeout(function(){
this.disabled = false
}.bind(this), 2000)
})

btn.addeventListener('click', () => {
console.log(this) // .btn的DOM对象
})

此处使用箭头函数也可以达到效果,因为箭头函数的this指向与上一层相同,此处的function是放在setTimeout中的普通函数,因此默认由window调用

方法 参数形式 是否调用 使用场景
call arg1,arg2,... 需要调用函数改变this且传递参数
apply [arg1,arg2,...] 需要调用函数改变this且与数组相关
bind arg1,arg2,... 不调用函数改变this,例如定时器内部函数

防抖(debounce)

单位时间内,频繁出发事件,只执行最后一次

使用场景:

  • 搜索框搜索输入,只需要用户最后一次输入完,在发送请求
  • 手机号、邮箱验证输入检测
1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div class="box"></div>
</body>
<script>
const box = document.querySelector('box')
let i = 1
function mouseMove() {
// 不进行防抖时,这一代码会在鼠标移动1像素后立即执行,如果是非常复杂的操作,那将造成页面卡顿
box.innterHTML = i++
}
box.addEventListener('mousemove', mouseMove)
</script>

因此有两种方案处理这一问题:

  1. lodash提供的防抖函数
  2. 手写防抖函数

lodash

1
_.debounce(func, [wait = 0], [options=])

该函数会从上一次被调用后,延迟wait毫秒后调用func方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<div class="box"></div>
<script src='./js/lodash.min.js'></script>
<script>
const box = document.querySelector('box')
let i = 1
function mouseMove() {
// 不进行防抖时,这一代码会在鼠标移动1像素后立即执行,如果是非常复杂的操作,那将造成页面卡顿
box.innterHTML = i++
}
// 实现鼠标移动500毫秒之后+1
box.addEventListener('mousemove', _.debounce(mouseMove, 500))
</script>
</body>

手写防抖函数

  1. 声明一个定时器变量
  2. 当事件发生时先判断是否存在定时器,如果存在则清空定时器
  3. 如果没有定时器则开启定时器,保存到变量中
  4. 在定时器中调用要执行的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<div class="box"></div>
<script>
const box = document.querySelector('box')
let i = 1
function mouseMove() {
// 不进行防抖时,这一代码会在鼠标移动1像素后立即执行,如果是非常复杂的操作,那将造成页面卡顿
box.innterHTML = i++
}
// 实现鼠标移动500毫秒之后+1
box.addEventListener('mousemove', debounce(mouseMove, 500))

function debounce(fn, t) {
let timer = null
return function() {
if(timer) clearTimeout(timer)
timer = setTimeout(function(){
fn()
}, t)
}
}
</script>
</body>

节流(throttle)

单位时间内频繁出发事件,只执行一次

使用场景:高频事件

  • 鼠标移动mousemove
  • 页面尺寸缩放resize
  • 滚动条滚动scroll

要求不管鼠标移动多少次,每隔500ms才 + 1

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div class="box"></div>
</body>
<script>
const box = document.querySelector('box')
let i = 1
function mouseMove() {
// 不进行防抖时,这一代码会在鼠标移动1像素后立即执行,如果是非常复杂的操作,那将造成页面卡顿
box.innterHTML = i++
}
box.addEventListener('mousemove', mouseMove)
</script>

实现方法:

  1. lodash节流函数
  2. 手写节流函数

lodash.throttle

1
_.throttle(func, [wait=0], [option=])
  • 在wait秒内最多执行func一次
  • 提供一个cancel()方法取消延迟调用
  • 提供一个flush()方法立即调用
  • options.leading=true指定调用在节流开始前
  • options.leading=true指定调用在节流开始后
1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div class="box"></div>
</body>
<script>
const box = document.querySelector('box')
let i = 1
function mouseMove() {
// 不进行防抖时,这一代码会在鼠标移动1像素后立即执行,如果是非常复杂的操作,那将造成页面卡顿
box.innterHTML = i++
}
// 每500ms只执行一次
box.addEventListener('mousemove', _.throttle(mouseMove, 500))
</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
<body>
<div class="box"></div>
<script>
const box = document.querySelector('box')
let i = 1
function mouseMove() {
// 不进行防抖时,这一代码会在鼠标移动1像素后立即执行,如果是非常复杂的操作,那将造成页面卡顿
box.innterHTML = i++
}
// 实现鼠标移动500毫秒之后+1
box.addEventListener('mousemove', throttle(mouseMove, 500))

function throttle(fn, t) {
let timer = null
return function() {
if(!timer) setTimeout(function() {
// 调用函数
fn()
//清空定时器, 此处不能使用clearTimeout
timer = null
}, t)
}
}
</script>
</body>

注意事项:

setTimeout中无法删除定时器,因为定时器仍然在运作,因此使用timer = null,而不是clearTimeout(timer)

防抖与节流

性能优化 说明 使用场景
防抖 单位时间内频繁触发事件,只执行最后一次 搜索框输入、手机号、邮箱验证
节流 单位时间内频繁触发事件,只执行一次 高频事件:鼠标移动,页面缩放,滚动条滚动

Proxy

Proxy是ES6种新增的语法,用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种元编程(对编程语言进行编程)

可以理解为在目标对象之前设置了一层拦截器:

  • 外界对该对象的访问都必须先通过这层拦截
  • 即可以对外界的访问进行了过滤和改写

下面这段代码使用Proxy修改了一个空对象的访问与设置操作:

1
2
3
4
5
6
7
8
9
10
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});

因此进行如下操作时会得到额外的结果:

1
2
3
4
5
6
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2

直观上俩看Proxy重载了.运算符

其语法模板为:

1
let proxy = new Proxy(target, handler);
  • target接收一个对象,即需要拦截的具体对象
  • handler接收一个对象作为拦截器,用于定义拦截行为

需要注意的是如果希望proxy起作用就必须对proxy对象进行操作:

1
2
3
4
5
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

一个实用技巧就是将proxy对象设置为原对象的一个属性:

1
var object = { proxy: new Proxy(target, handler) };

proxy对象也能作为其他对象的原型对象,且其中的拦截器会被继承:

1
2
3
4
5
6
7
8
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});

let obj = Object.create(proxy);
obj.time // 35

拦截器一共支持13种操作的拦截:

  • get(targte, propKey, receiver)

    • 拦截对象属性的读取proxy.attrproxy['attr']

    • target为拦截对象

    • propKey接收访问的参数Key值

    • receiver指向原始的读操作所在的那个对象即原本的target.propKey所指向的值,如果没有则会向target的原型中查找

    • 除了使用上面的创建方式外,还可以使用get:function(targte, propKey, receiver)的形式来让get变为执行一个函数

    • 此处展示一个使用该方法创建的函数链式调用栈的方法:

    • let pipe = function (value) {
          // funcStack与Proxy构成闭包
          // 即进行链式调用时访问的作用域始终为pipe作用域
          let funcStack = [];
          let oproxy = new Proxy({} , {
              get : function (pipeObject, fnName) {
                  // 调用get方法则进行计算
                  if (fnName === 'get') {
                      // 使用reduce函数,将value设为初始值则第一次调用时val=value
                      // 之后val=fn(val)返回的结果
                      return funcStack.reduce(function (val, fn) {
                          return fn(val);
                      },value);
                  }
                  // 在调用get之前不进行计算,只将调用的函数保存
                  // 函数通过变量名从window对象种查找
                  funcStack.push(window[fnName]);
                  // 每次返回一个代理对象,保证代理对象均包含get拦截器
                  return oproxy;
              }
          });
          return oproxy;
      }
      
      // 此处函数的作用域需要时全局作用域才能使用window[fnName]访问到
      var double = n => n * 2;
      var pow    = n => n * n;
      var reverseInt = n => n.toString().split("").reverse().join("") | 0;
      
      pipe(3).double.pow.reverseInt.get; // 63
      
  • set(target, propKey, value, receiver)

    • 阻拦对象的属性设置行为例如proxy.propKey=v,proxy['propKey'] = v
    • 返回一个bool值
  • has(target, propKey)

    • 拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey)

    • 拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target)

    • 拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环
    • 返回一个数组
    • 该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey)

    • 拦截Object.getOwnPropertyDescriptor(proxy, propKey)
    • 返回属性的描述对象。
  • defineProperty(target, propKey, propDesc)

    • 拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs)
    • 返回一个布尔值。
  • preventExtensions(target)

    • 拦截Object.preventExtensions(proxy)
    • 返回一个布尔值。
  • getPrototypeOf(target)

    • 拦截Object.getPrototypeOf(proxy)
    • 返回一个对象。
  • isExtensible(target)

    • 拦截Object.isExtensible(proxy)
    • 返回一个布尔值。
  • setPrototypeOf(target, proto)

    • 拦截Object.setPrototypeOf(proxy, proto)
    • 返回一个布尔值。
    • 如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args)

    • 拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args)

    • 拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API

Reflect对象的设计目的如下:

  1. Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上(目前Object和Reflect均包含这些方法
  2. 修改某些Object方法的返回结果,让其返回值更合理
  3. Object操作都变成函数行为。例如delete obj[name]等价于Reflect.deleteProperty(obj, name)
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

目前Reflect提供的方法有如下13中:

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

评论