阅读 395

面试官让你手写深拷贝函数的背后是想考察什么知识点

深拷贝的面试题网上随便一搜,都一大堆,相信每个前端同学都看过,但还是有很多面试官喜欢让人手写实现深拷贝函数,这到底是为什么呢,背后面试到底想考察你什么知识点呢 首先每个面试前的同学估计都会刷一波高频面试题,手写深拷贝也不难,我们先来一个最简单的深拷贝函数

简单的深拷贝函数

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 = "努力,奋斗 -- 星爷";
复制代码
复制代码

上述代码中:

  • desccontent都是基本类型,它们的值存储在栈内存。
  • 它们分别有各自独立的栈空间
  • 因此,修改content的值,desc不受影响

引用数据类型的复制

接下来,我们通过一个例子来看下引用类型的复制,代码如下所示:

let movie = {name:"国产凌凌漆", id: 10}
let data = movie;
data.name = "大内密探零零发";
console.log(movie.name); // 大内密探零零发
复制代码
复制代码

上述代码中:

  • moviedata都是引用类型,它们的引用存在栈内存,值存在堆内存
  • 它们的值指向同一块堆内存,栈内存中会复制一份相同的引用

javascript中的堆和栈

一.栈和堆

栈(stack):栈会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。(基本类型:StringNumberBooleanNullUndefinedBigIntSymbol) 堆(heap):动态分配的内存,大小不定也不会自动释放,存放引用类型,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。(引用类型:FunctionArrayObject

二.区别

栈:所有在方法中定义的变量都是放在栈内存中,随着方法的执行结束,这个方法的内存栈也自然销毁。

优点:存取速度比堆快,仅次于直接位于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.getPrototypeOfObject.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

根据数据类型的构造函数返回类型,但是由于nullundefined没有构造函数故无法判断

''.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基础知识掌握程度。

原理:基本类型拷贝是直接在栈内存新开空间,直接复制一份名-值,两者互不影响。 而引用数据类型,比如对象,变量名在栈内存,值在堆内存,拷贝只是拷贝了堆内存提供的指向值的地址

文章分类
前端
文章标签