深拷贝的面试题网上随便一搜,都一大堆,相信每个前端同学都看过,但还是有很多面试官喜欢让人手写实现深拷贝函数,这到底是为什么呢,背后面试到底想考察你什么知识点呢 首先每个面试前的同学估计都会刷一波高频面试题,手写深拷贝也不难,我们先来一个最简单的深拷贝函数
简单的深拷贝函数
function deepClone(obj) {
if(obj === null || obj === undefined){
return
}
let cloneObj = Array.isArray(obj) ? [] : {}
if(typeof obj === 'object'){
for(let key in obj) {
if(typeof obj[key] === 'object'){
cloneObj[key] = deepClone(obj[key])
} else {
cloneObj[key] = obj[key]
}
}
}
return cloneObj
}
我们可以看到这个克隆函数最重的一点就是利用了递归,那为什么利用递归就可以实现深拷贝了呢
我们看到里面有两个条件判断都是判断值的类型是否是 object
typeof 数据类型的检测
typeof 操作符返回一个字符串,表示未经计算的操作数的类型
描述
下表总结了 typeof 可能的返回值。有关类型和原始值的更多信息,可查看 JavaScript 数据结构 页面。
| 类型 | 结果 |
|---|---|
| Undefined | "undefined" |
| Null | "object" (见下文) |
| Boolean | "boolean" |
| Number | "number" |
| BigInt(ECMAScript 2020 新增) | "bigint" |
| String | "string" |
| Symbol (ECMAScript 2015 新增) | "symbol" |
| 宿主对象(由 JS 环境提供) | 取决于具体实现 |
| Function 对象 (按照 ECMA-262 规范实现 [[Call]]) | "function" |
| 其他任何对象 | "object" |
typeof一般用于检测基本数据类型,因为它检测引用数据类型都返回Objcet
console.log(typeof a); //'undefined'
console.log(typeof(true)); //'boolean'
console.log(typeof '123'); //'string'
console.log(typeof 123); //'number'
console.log(typeof NaN); //'number'
console.log(typeof null); //'object'
const obj = new String();
console.log(typeof(obj)); //'object'
const fn = function(){};
console.log(typeof(fn)); //'function'
console.log(typeof(class c{})); //'function'
typeof运算符用于判断对象的类型,但是对于一些创建的对象,它们都会返回 object,有时我们需要判断该实例是否为某个对象的实例,那么这个时候需要用到instanceof运算符,后面记录instanceof运算符的相关用法
注意:typeof检测null也会返回Object,这是js一直以来遗留的BUG。用typeof检测function返回的是function。
变量复制
接下来,我们从内存角度来看下变量复制。
基本数据类型的复制
我们通过一个例子来看下基本类型的复制,代码如下所示:
let desc = "像写诗一样地写代码";
let content = desc;
content = "努力,奋斗 -- 星爷";
复制代码
上述代码中:
desc、content都是基本类型,它们的值存储在栈内存。- 它们分别有各自独立的栈空间
- 因此,修改
content的值,desc不受影响
引用数据类型的复制
接下来,我们通过一个例子来看下引用类型的复制,代码如下所示:
let movie = {name:"国产凌凌漆", id: 10}
let data = movie;
data.name = "大内密探零零发";
console.log(movie.name); // 大内密探零零发
复制代码
上述代码中:
movie、data都是引用类型,它们的引用存在栈内存,值存在堆内存- 它们的值指向同一块堆内存,栈内存中会复制一份相同的引用
javascript中的堆和栈
一.栈和堆
栈(stack):栈会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。(基本类型:String,Number,Boolean,Null,Undefined,BigInt,Symbol) 堆(heap):动态分配的内存,大小不定也不会自动释放,存放引用类型,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。(引用类型:Function,Array,Object)
二.区别
栈:所有在方法中定义的变量都是放在栈内存中,随着方法的执行结束,这个方法的内存栈也自然销毁。
优点:存取速度比堆快,仅次于直接位于CPU中的寄存器,数据可以共享;
缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
堆:堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(参数传递)。创建对象是为了反复利用,这个对象将被保存到运行时数据区。
复杂的深拷贝
function deepClone(target, map = new Map()) {
if(target instanceof RegExp) return new RegExp(target)
if(target instanceof Date) return new Date(target)
if(typeof target === 'object' && target !== null && target !== undefined){
let cache = map.get(target)
if(cache) return cache
const result = new target.constructor
map.set(target, result)
if(Array.isArray(target)){
target.forEach((item, key) =>{
result[index] = deepClone(item, map)
})
} else {
Object.keys(target).forEach(key => {
result[key] = deepClone(target[key], map)
})
}
return result
} else {
return target
}
}
instanceof 检测引用数据类型
它的主要作用是用来判断一个实例是否属于某种类型,用于判断对象很合适
// 语法:object instanceof constructor
// object 某个实例对象 constructor 某个构造函数
'coboy' instanceof String //false 检查原型链会返回undefined
new String('coboy') instanceof String //true
new Boolean(true) instanceof Boolean // true
new Number(1) instanceof Number // true
这个方法主要是用来准备的检测引用数据类型的(不能用来检测基本数据类型),用来检测构造函数的prototype属性是否出现在对象原型链中的任意位置。
let fun = function(){ }
fun instanceof Function //true
let obj ={ }
obj instanceof Object //true
let arr = [ ]
arr instanceof Array //true
不能用来检测基本数据类型
1 instanceof Number //false
null instanceof Object // false
instanceof运算符直接访问的变量的原始值,不会自动建立包装类。因此不能用来判断基本数据类型。
instanceof 本意是用来判断 A 是否为 B 的实例对象,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。 在这里需要特别注意的是:instanceof检测的是原型,那它是怎么检测的呢,我们用一段伪代码来模拟其内部执行过程
instanceof (A,B) = {
const L = A.__proto__;
const R = B.prototype;
if(L === R) {
// A的内部属性__proto__指向B的原型对象,不是简单的指向
return true;
}
return false;
}
// instanceof检测左侧的L的__proto__原型链上,是否存在右侧的R的prototype原型
这里面又涉及到了原型和原型链的知识了
原型和原型链
上述过程可以看出,当A的__proto__ 指向B的prototype时,就认为A就是B的实例对象,我们再来看几个例子:
[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true
function Person(){};
new Person() instanceof Person;
[] instanceof Object; //true
new Date() instanceof Object;//true
new Person instanceof Object;//true
从上面的例子中,我们发现虽然instanceof能够正确判断[] 是Array的实例对象,但不能辨别 [] 不是Object的实例对象,为什么呢,这还需要从javascript的原型链说起,我们首先来分析一下[]、Array、Object 三者之间的关系,从instanceof判断能够得出:[].proto ->Array.prototype, 而Array.prototype.__proto__指向了Object.prototype,Object.prototype.proto 指向了null,标志着原型链的结束。 因此,[]、Array、Object就形成了一条原型链 。
从原型链可以看出,[]的__proto__最终指向了Object.prototype,类似的new Date()、new Person() 也会形成这样一条原型链,因此,我们用 instanceof 也不能完全精确的判断object类的具体数据类型
prototype和__proto__的区别
prototype是函数才有的属性,切记,切记
具体原因,可以看看阮一峰大神的这篇文章Javascript继承机制的设计思想,里面介绍了prototype的设计由来。
__proto__是每个对象都有的属性
它不是一个规范属性,该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它。__proto__属性已在ECMAScript 6语言规范中标准化,用于确保Web浏览器的兼容性,因此它未来将被支持。它已被不推荐使用, 现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf和Object.setPrototypeOf/Reflect.setPrototypeOf。
注意:大多数情况下,proto__可以理解为“构造器的原型”,即__proto===constructor.prototype,但是通过 Object.create()创建的对象有可能不是, Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,下面会有实例解释
let obj ={};
console.log("obj:", obj);
console.log("obj.prototype:", obj.prototype);
console.log("obj.__proto__:", obj.__proto__);
console.log("====================================");
function myFunc() {}
console.log("myFunc.prototype:",myFunc.prototype);
console.log("myFunc.__proto__:",myFunc.__proto__);
Object.prototype.toString()可以用来准确地检测所有数据类型
Object.prototype.toString.call([])
// "object Array"
Object.prototype.toString.call(1)
// "object Number"
Object.prototype.toString.call(null)
// "object Null"
Object.prototype.toString.call(undefined)
// "object Undefined"
Object.prototype.toString.call({})
// "object Object"
Object.prototype.toString.call(function add(){})
// "object Function"
....
constructor
根据数据类型的构造函数返回类型,但是由于null和undefined没有构造函数故无法判断
''.constructor == String //true
new Number(1).constructor == Number //true
new Function().constructor == Function //true
true.constructor == Boolean //true
new Date().constructor == Date //true
let arr =[]
arr.constructor==Array
// true
let fun = function(){}
fun.constructor==Function
//true
借用JSON对象的 parse 和 stringify
function deepClone(obj){
let newObj = JSON.stringify(obj);
let objClone = JSON.parse(newObj);
return objClone;
}
缺点,如果对象中有函数,这种方法就无法实现深拷贝了,函数变成字符串,就无法通过parse转回去了
总结:
虽然只是考你一个深拷贝的函数,但背后涉及到的知识是非常庞大的,面试官会根据你写的深拷贝函数的复杂程度判断你的Javascript基础知识掌握程度。
原理:基本类型拷贝是直接在栈内存新开空间,直接复制一份名-值,两者互不影响。 而引用数据类型,比如对象,变量名在栈内存,值在堆内存,拷贝只是拷贝了堆内存提供的指向值的地址