这个问题涉及JS的数据类型、数据存储、内存管理。还涉及很多边界条件的考虑,很具有代表性。很好查漏补缺
内存管理
每一个数据存储都都有一个内存空间,内存空间又被分为两种:栈内存(stock)、堆内存(heap)。js会根据数据类型存储在对应内存上
基础数据类型和栈内存
基础数据类型:Number String Null Undefined Boolean Symbol
基础数据类型储存在栈内存,可以直接操作保存在栈内存的值,这个值通过变量来访问。
但是不能修改已经储存的值,比如可以往栈内存中存储了一个数字2,但是这个2不能改为3。
引用数据类型与堆内存
引用数据类型:Array Object Function
引用类型储存在堆内存,栈内存中会存储指向这个堆内存的引用。js引用类型的值大小不固定,可以在不声明长度的情况下静态补充。
js不允许直接访问堆内存的位置,因此不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作栈内存的引用而不是实际的对象。
拷贝
js中基础类型和引用类型的特点不同,所以拷贝时使用的方式也不同
浅拷贝
只拷贝一层数据,是实现深拷贝的基础
基础数据类型
利用栈内存的值不能修改,基础数据类型可以通过直接赋值的方式实现拷贝,不必担心原数据改变引起拷贝后的值发生变化
数组
- let arr2 = [...arr1]
- let arr2 = arr1.slice(0)
- ...
骚操作-拷贝对象
一般拷贝对象会先声明一个变量等于一个空对象let newObj = {},这么做可能会丢失原型链。使用let newObj = new obj.constructor可以在一定程度上保留原型链,还可以使用在拷贝数组上
- 可以使用的对象
- 数组
- 对象
- 类的实例
let newObj = new obj.constructor // 找出实例
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 忽略继承属性
newObj[key] = obj[key]
}
}
函数拷贝
核心的两个方法就是 Function.prototype.toString 与 new Function()
Function.prototype.toString 可以返回整个方法字符串,如 'function fn(a, b) { console.log(a, b) }'。
new Function可以创建新函数,例如创建上面的函数 new Function('a, b', 'console.log(a, b)')。
链接这两个方法就需要一个正则分别匹配参数名和函数内容
// 例子
function fn(a, b){ console.log(a, b) }
const reg = /^function \S*\s*\(([\s\S]*)\)\s*{([\s\S]*)}$/
// 第一步:将函数转为字符串
const str = fn.toString() // 'function fn(a, b){ console.log(a, b) }'
// 第二步:拿到参数名与函数内容
const m = str.match(reg) // ["function fn(a, b){ console.log(a, b) }", "a, b", " console.log(a, b) ", index: 0, input: "function fn(a, b){ console.log(a, b) }", groups: undefined]
// 第三步:创建函数
const newFn = new Function(m[1], m[2])
注:正则在这个例子中只是演示,不保证实际工作使用是否会出问题 =。=
拷贝正则、日期对象
构造函数可以直接接受一个正则、日期对象来创建一个新的正则、日期对象
let reg = /^n+$/
let date = new Date()
let newReg = new RegExp(reg)
let newDate = new Date(date)
正则对象中存有 lastIndex,考虑 lastIndex 的话需要
newReg.lastInde === reg.lastInde
Set、Map
构造函数本身就可以接受set、map数据类型,返回新的set、map
var set = new Set([1, 2, 3, 4])
var map = new map(['a', 1], ['b', 2])
var newSet = new Set(set)
var newMap = new Map(map)
WeakSet 、 WeakMap 都不接受自身的数据类型,本身也不支持遍历,暂没有找到拷贝的方法。
symbol
Symbol 只接受字符串,需要取到创建 Symbol 的字符串
let symbol = Symbol(1)
let str = Symbol.prototype.toString.call(symbol) // "Symbol(1)"
str = str.replace(/^Symbol\((\S+)\)$/, '$1') // "1"
let newSymbol = Symbol(str)
深拷贝
多层拷贝
JSON
let obj = {a: 1, b: {c: 2}}
JSON.parse(JSON.stringify(obj))
缺点:不能含有 undefined、function、正则、日期类型...
深度拷贝函数
使用 WeakMap 解决循环引用的问题
// 类型检测
function _type(value) {
return Object.prototype.toString.call(value)
}
// 深拷贝
function _deepClone(obj, hash = new WeakMap) {
if (obj === null) return null
if (typeof obj !== 'function') return obj // 函数深拷贝没有意义
if (typeof obj !== 'object') return obj
if (_type(obj) === '[object RegExp]' || _type(obj) === '[object Date]') {
return new obj.constructoe(obj)
}
let v = hash.get(obj);
if (v) return v // 如果映射表中有,就是循环引用。直接返回拷贝后的结果
let newObj = new obj.constructoe
hash.set(obj, newObj); // 将拷贝前的和拷贝后的做一个映射表
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = _deepClone(obj[key])
}
}
return newObj
}
修改一下支持 Set、Map
// 类型检测
function _type(value) {
return Object.prototype.toString.call(value).replace(/^\[object (\S+)\]$/g, '$1')
}
// 深拷贝
function _deepClone(obj, hash = new WeakMap) {
if (obj === null) return null
if (typeof obj !== 'function') return obj // 函数深拷贝没有意义
if (typeof obj !== 'object') return obj
if (['RegExp', 'Date', 'Set', 'Map'].includes(_type(obj))) {
return new obj.constructoe(obj)
}
let v = hash.get(obj);
if (v) return v // 如果映射表中有,就是循环引用。直接返回拷贝后的结果
let newObj = new obj.constructoe
hash.set(obj, newObj); // 将拷贝前的和拷贝后的做一个映射表
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = _deepClone(obj[key])
}
}
return newObj
}