引言
嘿! 欢迎来到我整理的 JS
面试题集!! 无论你是正在寻找前端开发的机会, 还是想巩固自己的 JS
知识, 相信这些问题会给你带来些帮助或启发!!!
考虑到 JS
相关面试内容会比较多, 所以这里会将其拆分多个篇幅进行讲解!!! 没篇固定十道题目!!!
现在, 让我们一起开始吧!!
一、防抖与节流
1.1 防抖
当触发高频函数事件,
n
秒内又再次触发事件, 则取消上一次事件的执行; 也就是说一段时间内如果高频次触发事件只会执行最后一个, 之前触发的事件都认为是"抖动"
引起的、是误操作、是做不得数的
- 闭包: 对事件进行包裹
- 内部使用定时器, 事件函数延迟
n
秒执行 - 当新的事件触发则取消上一次计算器, 重新一轮定时器
js
// 防抖
function debounce (func, wait) {
let timeout;
return functio ( ){
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func.apply(this, arguments);
}, wait)
}
}
1.2 节流
当触发高频函数事件,
n
秒内又再次触发事件, 则不做任何处理, 节流会稀释
函数的执行频率
; 也就是说假设我们的n
设置为1s
那么在高频操作下10s
内最多只会触发10
次事件
- 闭包: 对事件进行包裹
- 内部使用定时器, 事件函数延迟
n
秒执行 - 当新的事件触发, 如果上一个事件还未执行(定时器还在), 则不执行新的事件, 否则开始新的定时器
js
// 节流
function throttle (func, wait) {
let timeout;
return function () {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(this, arguments);
}, wait);
}
}
}
二、本地存储
主要介绍下面四种: cookie
、localStorage
、sessionStorage
、indexedDB
cookie
-
存储类型: 小型文本字符串
-
作用: 指某些网站为了辨别用户身份而储存在用户本地终端上的数据!! 是为了解决 HTTP无状态导致的问题!!!
-
大小:
4KB
(跟浏览器厂商有关系) -
补充说明: 首先要知道标准的
http
协议是无状态的, 也就是指服务端对于客户端的每次请求都认为它是一个新的请求, 上一次会话和下一次会话之间是没有任何联系, 这时当用户访问服务端并进行登录后, 客户端之后的请求服务端依然无法对客户端身份进行识别, 如果将客户端与服务器之间的多次交互当做一个整体来看, 那么服务端若想识别客户端的身份那么就需要将多次交互所涉及的数据(状态)保存下来。cookie
的作用就是对请求和响应的状态进行一个管理, 服务端通过在响应体中设置cookie
(状态), 客户端会将cookie
(状态) 存储起来, 之后客户端的每个请求都将cookie
(状态)带上, 这样服务端就能够对客户端的身份进行识别;在早期cookie
还未出现之前有个最简单的办法就是在请求的页面中插入一个token
, 然后在下次请求时将这个token
返回至服务器。这需要在页面的form
表单中插入一个包含token
的隐藏域, 或者将token
放在URL
的query
字符串中来传递。这两种方法都需要手动操作, 而且极易出错, 而cookie
则不同一般情况下客户端会自动存储服务器发来的cookie
, 并在之后的每次请求中自动带上cookie
而无需客户端进行手动处理。需要注意的是标准的http
协议指的是不包括cookies
他是不属于标准协议, 虽然各种网络应用提供商, 实现语言、web
容器等, 都默认支持它。
详细介绍参考: cookie 简介
localStorage & sessionStorage
- 相同点
- 大小:
5M
(跟浏览器厂商有关系) - 本质上是对字符串的读取, 如果存储内容多的话会消耗内存空间, 会导致页面变卡
- 当本页操作(新增、修改、删除) 了
Storage
的时候, 本页面不会触发storage
事件, 但是别的页面会触发storage
事件
- 不同点:
- 生命周期:
localStorage
持久化的本地存储, 除非主动删除数据, 否则数据是永远不会过期的;sessionStorage
一旦页面(会话)关闭, 就会删除数据 - 限制:
localStorage
受同浏览器限制、同源限制;sessionStorage
受到同浏览器限制、同源限制、同标签页限制
详细介绍参考: localStorage 和 sessionStorage 简介
2.3 indexedDB
优点:
- 储存量理论上没有上限
- 所有操作都是异步的, 相比
localStorage
那几个同步操作, 性能会更高, 尤其是数据量较大时 - 原生支持储存
JS
的对象 - 是个正经的数据库, 意味着数据库能干的事它都能干
缺点:
- 操作非常繁琐
- 使用上有一定门槛
2.3 localStorage sessionStorage Cookie 区别
相同点
- 跨域: 都存在跨域问题
- 存储数据类型: 存储的值都是字符串, 如果要存储对象则需要通过
JSON.stringify
进行格式化
不同点
- 生命周期:
cookie
可自定义时长(默认会话结束即失效)、sessionStorage
会话结束则失效、localStorage
永久存在, 除非手动清除(通过api
清除或直接清理浏览器缓存) - 大小:
cookie
4KB、sessionStorage
5MB、localStorage
5MB - 语法上:
cookie
通过直接修改window.document.cookie
来进行增删改,localStorage
和sessionStorage
则通过对应 api(setItem
getItem
clear
) 来修改数据 - 参与通信:
cookie
会参与通信, 在请求接口时会自动在请求头中添加cookie
、localStorage
和sessionStorage
则不参与通信
三、模块化
3.1 CommonJS 模块化规范
CommonJS
规范 是 NodeJS
中实现的一种模块化规范
- 使用
require
导入模块,require
是一个可执行函数 - 使用
module.exports
导出模块 - 特性:
- 同步:
require
加载文件是同步进行的 - 缓存机制:
require
第一次加载模块会对该模块进行缓存, 再次加载模块时会先比较异同, 若模块没有改变, 就不会执行require
- 每次加载都是对值的拷贝
- 运行时加载
3.2 ES6 模块化规范
E6
正式将模块定义为一种规范
- 使用
import
导入模块 - 使用
export
导出模块 - 特性:
- 异步:
import
加载文件是异步加载的 - 每次加载都是模块的一个引用地址
- 编译时加载
3.3 ES6 和 CommonJS 区别
- 本质区别:
CommonJS
是输出的是值的拷贝, 模块内部的改变, 不会影响到其他地方ES6
模块输出的是值的引用, 模块内部的改变, 会影响到其他地方
- 运行时机:(由本质决定)
commentJS
是运行时才加载模块(因为导出的是值的拷贝, 所以每次都要重新加载保证最新), 所以它就有了缓存机制ES6
则是编译时进行加载(引起模块导出的是引用)
- 语法上不同
require
是同步加载、import
是异步加载this
关键词:
- 在
ES6
模块顶层,this
指向undefined
CommonJS
模块的顶层的this
指向当前模块
- 在
ES6
模块中可以直接加载CommonJS
模块, 但是只能整体加载, 不能加载单一的输出项 - 循环引用:
CommonJS
模块遇到循环加载时, 输出的是当前已经执行那部分的值, 而不是代码全部执行后的值ES6
模块是动态引用, 如果使用import
加载一个变量, 变量不会被缓存, 真正取值的时候是能够取到最终的值
3.4 为什么不在浏览器也是用 CommonJS
-
回答这个问题之前, 我们首先要清楚一个事实,
CommonJS
的require
语法是同步的, 当我们使用require
加载一个模块的时候, 必须要等这个模块加载完后, 才会执行后面的代码 -
那我们的问题也就很容易回答了,
NodeJS
是服务端, 使用require
语法加载模块, 一般是一个文件, 只需要从本地硬盘中读取文件, 它的速度是比较快的 -
但是在浏览器端就不一样了, 文件一般存放在服务器或者
CDN
上, 如果使用同步的方式加载一个模块还需要由网络来决定快慢, 可能时间会很长, 这样浏览器很容易进入假死状态
-
所以异步加载, 是比较适合在浏览器端使用
3.5 其他模块方案
AMD
模块化规范: 定义异步模块CMD
模块化规范: 定义通用模块定义
3.6 参考
3.7 TODO
ES
模块如何被webpack
编译的
四、Promise
4.1 promise a+ 规范
promise
有三个状态:promise
只会从等待变为成功, 或者从等待变为失败
- 等待(
PENDING
) - 已解决(
RESOLVED
), 返回的结果, 被.then
捕获 - 已拒绝(
REJECTED
), 返回的原因, 被.catch
捕获
promise
接受一个函数, 该函数会被立即执行(宏任务)promise
中.then
有两个函数参数, 第一个函数参数作为已解决时的回调函数被调用(微任务), 第二参数则被作为已拒绝时的回调函数被调用(微任务)promise
中.catch
方法用于捕获失败状态.then
和.catch
会返回一个新的Promise
(所以才支持链式调用), 如果返回的是一个普通值则会使用promise.resolve
进行包裹promise
错误永远会被最近的一个捕获函数捕获到, 可以是某个最近.then
的第二参数函数, 也可能是最近的一个.catch
4.2 promise 常用方法
-
Promise.resolve()
: 静态方法返回一个已解决(RESOLVED
)的Promise
对象,.then
参数为函数传入的参数 -
Promise.reject()
: 静态方法返回一个已拒绝(REJECTED
)的Promise
对象,.catch
拒绝原因为给定的参数 -
Promise.all()
: 所以都已解决才是已解决, 否则出现一个已拒绝, 则是已拒绝;- 参数: 由多个
Promise
组成的可迭代对象 - 功能: 所有
Promise
并行执行, 当所有输入的Promise
都被兑现时, 返回的Promise
也将被兑现(即使传入的是一个空的可迭代对象), 并返回一个包含所有兑现值的数组; 如果输入的任何Promise
被拒绝, 则返回的Promise
将被拒绝, 并带有第一个被拒绝的原因
- 参数: 由多个
-
Promise.allSettled()
: 等待所有Promise
执行完成(不会像all()
一样只要有一个已拒绝, 就结束)- 参数: 由多个
Promise
组成的可迭代对象 - 功能: 和
all()
方法不同, 当所有输入的Promise
状态都已结束(包括传入空的可迭代对象时), 返回的Promise
将被兑现, 并带有描述每个Promise
结果的一个对象数组
- 参数: 由多个
-
Promise.any()
: 一个成功则成功, 全部失败则失败, 并返回所有拒绝原因- 参数: 由多个
Promise
组成的可迭代对象 - 功能: 和
all()
相反, 这里只要有一个状态改为已解决, 则结束, 并返回该值; 如果所有状态都是已拒绝, 则结束, 并返回所有拒绝的原因
- 参数: 由多个
-
Promise.race()
: 只要一个状态被修改, 则结束;- 参数: 由多个
Promise
组成的可迭代对象 - 功能: (竞速)等待第一个状态改变, 所有
promise
中只要一个状态改变即结束, 如果先成功了那就成功了, 如果先失败了那就失败了
- 参数: 由多个
-
Promise.prototype.then()
:- 参数: 两个函数, 第一个是已解决状态的回调函数, 第二个是已拒绝的回调函数
- 功能: 设置
Promise
状态修改的一个回调函数, 需要注意的是该方法返回一个新的Promise
, 从而实现链式调用
-
Promise.prototype.catch()
:- 参数: 回调函数
- 功能: 用于注册一个在
promise
被拒绝时要调用的函数, 需要注意的是该方法返回一个新的Promise
, 从而实现链式调用
-
Promise.prototype.finally()
:- 参数: 回调函数
- 功能: 用于注册一个在
promise
状态被修改时要调用的函数, 需要注意的是该方法返回一个新的Promise
, 从而实现链式调用
4.3 中断 promise
- 在执行器中则, 使用
resolve reject
中断
js
new Promise((resolve, reject) => {
// ......
// 直接调用 resolve 或 reject
resolve();
});
- 中断调用链: 如果在某个
then/catch
执行之后, 不想让后续的链式调用继续执行了, 则可以返回一个永不结束的Promise
js
new Promise((resolve, reject) => {
// ....
})
.then(() => {
// ....
// 关键代码: 返回一个永不结束的 Promise, 该 promise 状态永远是 PENDING 中, 导致后面的链式调用(.then/.catch/finally)无法被执行
return Promise.reject(() => {});
})
.then(() => console.log('then'))
.catch(() => console.log('catch'))
.finally(() => console.log('finally'));
- 使用
Promise.race
竞速, 只要一个状态改变就结束
js
const p1 = new Promise(() => {});
const p2 = new Promise(() => {});
const p3 = new Promise(() => {});
Promise.race([p1, p2, p3]).then(() => {});
4.4 手写 promise
下面是一个最简易版本的
js
const PENDING = "PENDING";
const RESOLVED = "RESOLVED";
const REJECTED = "REJECTED";
class MyPromise {
constructor(executor) {
this.status = PENDING; // 宏变量, 默认是等待态
this.onfulfilled = null; // .then 中成功的回调函数
this.onrejected = null; // .then 中失败的回调函数
// 定义 resolve
const resolve = (value) => {
if (this.status === PENDING) {
// 保证只有状态是等待态的时候才能更改状态
this.status = RESOLVED;
this.onfulfilled && this.onfulfilled(value);
}
};
// 定义 reject
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.onrejected && this.onrejected(reason);
}
};
// 执行 executor 传入我们定义的成功和失败函数
try {
executor(resolve, reject);
} catch (e) {
reject(e); // 报错
}
}
// .then 方法
then(onfulfilled, onrejected) {
this.onfulfilled = onfulfilled;
this.onrejected = onrejected;
}
}
4.5 参考
五、柯里化
5.1 柯里化
柯里化
(Currying
), 又称 部分求值
(Partial Evaluation
), 是函数编程的一种高级技巧, 通常只需要传递给函数 一部分参数
来调用它, 让它返回一个新的函数去 处理剩下的参数
- 最简单的一个
DEMO
js
// 使用「柯里化」思想编写的一个求和函数
const sumCurry = (a) => (b) => {
console.log(a + b)
}
sumCurry(1)(2) // 3
- 无限参数
js
// 使用「柯里化」思想编写的一个求和函数
const sumCurry = (...preArgs) => {
// 1. 计算当前结果
const preTotal = preArgs.reduce((total, ele) => (total + ele), 0)
// 2. 使用 bind, 基于原函数创建新函数, 并将上一次结果作为第一个参数
const _sumCurry = sumCurry.bind(this, preTotal)
// 3. 修改 toString 返回值, 用于值值
_sumCurry.toString = () => preTotal
return _sumCurry
}
// 1. 参数固定
- 普通函数转柯里化
js
/**
* 中转函数
* @param fun 待柯里化的原函数
* @param allArgs 已接收的参数列表
* @returns {Function} 返回一个接收剩余参数的函数
*/
const _curry = (fun, ...allArgs) => {
// 1. 返回一个接收剩余参数的函数
return (...currentArgs) => {
// 2. 当前接收到的所有参数
const _args = [...allArgs, ...currentArgs]
// 3. 接收到的参数大于或等于函数本身的参数时, 执行原函数
if (_args.length >= fun.length) {
return fun.call(this, ..._args)
}
// 4. 继续执行 _curry 返回一个接收剩余参数的函数
return _curry.call(this, fun, ..._args)
}
}
/**
* 将函数柯里化
* @param fun 待柯里化的原函数
* @returns {Function} 返回「柯里化」函数
*/
const curry = (fun) => _curry.call(this, fun)
// 测试
const sum = (a, b, c) => (a + b + c) // 原函数
const currySum = curry(sum) // 柯里化 函数
currySum(1)(2)(3) // 6
currySum(1)(2, 3) // 6
currySum(1, 2, 3) // 6
5.2 柯里化作用
参数复用: 因为是闭包的一种实现, 所以能复用上一个函数的参数 提前计算: 提前计算好, 返回新的函数; 如兼容性处理, 提前对浏览器进行兼容性处理, 并返回一个通用函数 延迟运行: 返回的是新函数, 而不是值, 当所以参数收集完毕, 最后统一处理
5.3 柯里化和闭包的关系
闭包
和 柯里化
是两个不同的概念, 但它们之间有一定的关联:
-
在编程中, 闭包允许函数捕获并访问其定义时的上下文中的变量, 即使在其定义环境之外被调用时也可以使用这些变量, 闭包可以通过函数返回函数的方式创建, 从而使得内部函数可以访问外部函数的变量
-
柯里化是指高阶函数使用技巧, 柯里化函数的特点是, 允许被连续调用
f(a)(b)(c)
每次调用传递若干参数, 同时返回一个接受剩余参数
的新函数
, 直到所有参数都被传递完毕, 才会执行主体逻辑 -
闭包和柯里化之间的关系在于,
柯里化函数
通常会使用闭包
来实现
, 当我们将一个函数进行柯里化时, 每次返回一个新的函数, 这个新的函数会捕获前一次调用时的参数和上下文, 这个上下文就形成了闭包, 使得新函数可以在后续调用中继续访问之前传递的参数和上下文
六、类型判断
6.1 typeof
基本数据
类型除了null
外都能够返回正确的数据类型(null
会返回object
)- 函数返回
function
typeof
可以判断基本数据类型, 但是难以判断除了函数以外的复杂数据类型
js
typeof null // 'object'
typeof 1 // 'number"'
typeof 'a' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'
typeof function(){} // 'function'
typeof NaN // 'number'
typeof ({a:1}) // 'object'
typeof [1,3] // 'object'
typeof (new Date) // 'object'
6.2 instanceof & isPrototypeOf()
- 在
JS
中我们有两种
方式可以判断原型
是否存在于某个实例
的原型链
上, 通过这种判断就可以帮助我们,确定
引用数据
的具体类型
instanceof
代码演示
js
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);
auto instanceof Car // Car.prototype 是否在 auto 原型链上, true
auto instanceof Object // Object.prototype 是否在 auto 原型链上, true
isPrototypeOf
代码演示
js
function Car() {}
const auto = new Car();
Car.prototype.isPrototypeOf(auto) // Car.prototype 是否在 auto 原型链上, true
Object.prototype.isPrototypeOf(auto) // Object.prototype 是否在 auto 原型链上, true
6.3 constructor
constructor
判断方法跟 instanceof
相似, 已知在 实例对象
的原型中存在 constructor
指向 构造函数
, 那么借用这个特性我们可以用于判断 数据
类型
js
function Car() {}
const auto = new Car();
auto.constructor === Car // true
不同于 instanceof
, 通过该方式既可以处理 引用数据
、又能够处理 基本数据
js
(123).constructor === Number // true
(true).constructor === Boolean // true
('bar').constructor === String // true
- 同于 instanceof, 不能判断 对象父类
6.4 终结办法: Object.prototype.toString()
Object.prototype.toString.call()
方法返回一个表示该对象的字符串, 该字符串格式为 "[object Type]"
, 这里的 Type
就是对象的类型
js
const toString = Object.prototype.toString;
toString.call(111); // [object Number]
toString.call(null); // [object Null]
toString.call(undefined); // [object Undefined]
toString.call(Math); // [object Math]
toString.call(new Date()); // [object Date]
toString.call(new String()); // [object String]
注意: 对于自定义构造函数实例化出来的对象, 返回的是 [object Object]
js
const toString = Object.prototype.toString;
function Bar(){}
toString.call(new Bar()); // [object Object]
默认, 如果一个对象有 Symbol.toStringTag
属性并且该属性值是个字符串, 那么这个属性值, 会被用作 Object.prototype.toString()
返回内容的 Type
值进行展示
js
const toString = Object.prototype.toString;
const obj = {
[Symbol.toStringTag]: 'Bar'
}
toString.call(obj) // [object Bar]
补充: 一个通用方法, 一行代码获取 数据的类型
js
const getType = (data) => {
return Object.prototype.toString.call(someType)
.slice(8, -1)
.toLocaleLowerCase()
}
七、作用域、作用域链、作用域链、闭包
7.1 作用域
- 作用域是在运行时代码中的变量、函数的可访问性
- 作用域决定了代码区块中变量和其他资源的可见性, 作用域最大的用处就是隔离变量, 不同作用域下同名变量不会有冲突
ES6
之前JavaScript
没有块级作用域, 只有全局作用域和函数作用域
7.2 全局作用域和函数作用域
-
全局作用域中的变量、函数在任何地方都能被访问到
-
函数作用域内的变量只有在函数内部才能被访问到(包括其子函数)
-
在
js
最外面定义的变量、函数属于全局作用域 -
在函数中定义的变量、函数属于函数作用域
-
对末定义的变量进行赋值, 将会被作为全局变量进行声明
-
window
对象的属性拥有全局作用域
7.3 块级作用域
var
声明的变量只在函数中声明才具有块级作用域特性let
const
声明的变量在任意{}
中都具有块级作用域
7.4 作用域链
作用域链是指查找变量的过程, 假如在函数中调用了某个变量 A
, 首先会查询当前作用域看是否存在该变量, 有则直接使用, 没有则寻找上一层作用域, 如果还没有则继续往上一层就行选择, 如此一层层的寻找过程被称为作用域链
7.5 作用域和执行上下文
- 首先作用域和执行上下文并不是一个相同的概念
js
属于解释型语言, 执行过程中分两个阶段: 解释和执行- 在解析阶段作用域就被确定了, 但是执行上下文则是在执行阶段才被创建, 并且通过
call
apply
bind
都可修改执行上下文 - 执行上下文在运行时确定, 随时可能改变; 作用域在定义时就确定, 并且不会改变
- 补充: 执行上下文其实就是
this
指向, 只有在执行阶段才会被创建
补充:
解释阶段: 词法分析、语法分析、作用域规则确定 执行阶段: 创建执行上下文、执行函数代码、垃圾回收
7.6 闭包
闭包: 指一个函数有权限访问另一个函数中的变量的变量, 一般发生在嵌套函数中; 在嵌套函数, 返回一个新函数, 新函数引用了外层函数的变量, 导致外层函数的执行上下文无法销毁, 从而形成了闭包
应用场: 从现实出发, 就和容易想象带它的使用场景, 在需要缓存数据、在内存中维持变量的情况下使用, 如: 缓存数据(解决循环问题)、防抖、节流、柯里化等等
js
// 解决循环问题
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(){
alert(i + 1)
}, true)
}
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(i){
return function(){
alert(i + 1)
}
}(i), true)
}
js
// 柯里化
var add = function (m) {
var temp = function (n) {
return add(m + n);
}
// 为了取值, 隐性转换等
temp.toString = function (){
console.log('[ m ]', m);
return m;
}
return temp;
}
js
// 可在内存中维持住变量 a
const fun = a => () => {};
const f = fun({ name: 'qy' });
八、「for in」和「for of」的区别
本质区别:
for...in
用于迭代对象
的可枚举属性(数组本质上也是对象, 所以也可用于数组)for...of
用于跌到可迭代对象
, 因为数组是可迭代对象所以可以被使用, 普通对象不是可迭代对象所以不可被使用
九、map 和 forEach 的区别
9.1 相同点
- 参数一致:
callbackFn: (ele, index, arr) => {}
thisArg
- 作用: 都可用于循环
- 可变性: 都能在
callback
中改变原数组, 但是map
并不推荐这么做
9.2 不同点
- 返回值不同
map
返回一个新数组, 新数组是基于源数组中每个元素生成的, 长度和源数组保持一致forEach
返回Undefined
- 是否可以链式调用
map
因为返回的是数组, 所以可以链式调用其他数组方法forEach
因为返回的是undefined
, 所以不可以链式调用
9.3 性能比较
- 性能比较:
for
>forEach
>map
性能的比较实际上与环境使用的
V8
版本相关, 这也是为什么map
方法在chrome
里比在Node
中慢10
倍, 有人测试过(在chrome 62
和Node.js
v9.1.0
环境下):for
循环比forEach
快1
倍,forEach
比map
快20%
左右
- 原因分析:
-
for
:for
循环没有额外的函数调用栈和上下文, 所以它的实现最为简单, 所以也最快 -
forEach
: 对于forEach
来说, 它的函数签名是这样的array.forEach(function(currentValue, index, arr), thisValue)
其中包含了参数和上下文, 这会影响它的性能 -
map:
map
最慢的原因是因为map
和forEach
相比还多了返回一个新的数组
这一环节, 数组的创建和赋值会导致分配内存空间, 因此会带来较大的性能开销
9.2 参考
十、浅拷贝、深拷贝
10.1 前置知识
- 数据类型分类
基本数据类型: null
undefined
number
string
boolean
symbol
bigInt
引用数据类型: Object
细分有 Object
Array
Date
Function
...
- 数据存储方式
基本数据: 基本数据类型保存在 栈内存
, 栈内存
中分别存储着变量的标识符以及变量的值, 值是直接存储在变量访问的位置 引用数据: 数据存储在 堆内存
中, 栈内存
存储着变量标识以及变量的值(该值是一个引用地址)
- 栈内存 & 堆内存
- 栈内存: 它是一种计算机内存中划分出来的一块
连续
的存储区域
, 它的主要特点是先进后出
- 堆内存: 它是一种计算机内存中划分出来的一块
非连续
的存储区域
, 它的特点是可以动态分配和释放内存空间, 但它需要手动管理内存空间
- 赋值操作, 不同类型数据, 内存变化
js
let a = 1;
let b = a; // 栈内存会新开辟一个内存, 存储变量标识和值
js
let a = { name: 'A',age: 10 };
let b = a; // 栈内存会新开辟一个内存, 存储变量标识和值(堆内存中数据的引用地址)
a.age = 20; // 修改的是堆内存中数据, a b 都指向了同一个引用地址(堆内存中的数据)
10.2 深拷贝
创建一个
新对象
, 拷贝对象的所有属性, 如果属性是基本数据
, 拷贝的就是基本数据
的值; 如果是引用数据
, 则需要重新分配一块内存, 拷贝该引用数据
的所有属性, 然后将引用地址
赋值给对应的属性, 如果该引用数据
中某个属性也是引用数据
则需要继续一层层递归拷贝......
JSON.parse(JSON.stringify())
: 利用JSON.stringify
将对象转成JSON
字符串, 再用JSON.parse
把字符串解析成对象, 如此一来一去就能够实现引用数据
的一个深拷贝, 但是该方法有缺陷如下:
NaN
Infinity
-Infinity
会被序列化为null
Symbol
undefined
function
会被忽略(对应属性会丢失)Date
将得到的是一个字符串RegExp
Error
对象, 得到的是空对象{}
- 如果被拷贝的对象中, 有多个属性复用同一个对象, 拷贝后复用的情况将不复存在
- 如果存在
循环引用
对象则会报错
-
structuredClone
: 新的API
可用于对数据进行深拷贝
, 同时还支持循环引用, 但是不支持函数
类型的属性值 -
使用第三方库:
lodash
中的cloneDeep
方法 -
手写:
js
// map 用于记录出现过的对象, 解决循环引用
const deepClone = (target, map = new WeakMap()) => {
// 1. 对于基本数据类型(string、number、boolean......), 直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 2. 函数 正则 日期 MAP Set: 执行对应构造题, 返回新的对象
const constructor = target.constructor
if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) {
return new constructor(target)
}
// 3. 解决 共同引用 循环引用等问题
// 借用 `WeakMap` 来记录每次复制过的对象, 在递归过程中, 如果遇到已经复制过的对象, 则直接使用上次拷贝的对象, 不重新拷贝
if (map.get(target)) {
return map.get(target)
}
// 4. 创建新对象
const cloneTarget = Array.isArray(target) ? [] : {}
map.set(target, cloneTarget)
// 5. 循环 + 递归处理
Object.keys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], map);
})
// 6. 返回最终结果
return cloneTarget
}
10.3 浅拷贝
会新建一个对象, 拷贝对象的所有属性值, 对于
基本数据
来说就是拷贝一份对应的值, 但是对于引用数据
则是拷贝一份引用数据
的引用地址
Object.assign()
- 展开运算符
...
- 对于数组, 可以使用一些数组方法, 比如
Array.prototype.concat()
Array.prototype.slice()
Array.from
等方法, 它们的特点都是不改变原数组、同时返回一个新的数组 - 第三方库:
lodash
中的clone
方法 - 手写:
js
const clone = (target) => {
// 1. 对于基本数据类型(string、number、boolean......), 直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 2. 创建新对象
const cloneTarget = Array.isArray(target) ? [] : {}
// 3. 循环 + 递归处理
Object.keys(target).forEach(key => {
cloneTarget[key] = target[key];
})
return cloneTarget
}
const res= clone({ name: 1, user: { age: 18 } })
补充: 个人理解, 要判断是
浅拷贝
还是深拷贝
, 只需要区分拷贝前后两个数据是否是完全独立、隔离的, 是则是深拷贝, 否则则是浅拷贝
未完待续, 敬请期待!!!!!