乐趣区

关于前端:前端阿里面试-实现一个深拷贝

前言

深拷贝这个性能在开发中常常应用到,特地在对援用类型的数据进行操作时,个别会先深拷贝一份赋值给一个变量,而后在对其操作,避免影响到其它应用该数据的中央。

如何实现一个深拷贝,在面试中呈现频率始终居高不下。因为在实现一个深拷贝过程中,能够看出应聘者很多方面的能力。

本专栏将从青铜到王者来介绍怎么实现一个深拷贝,以及每个段位对应的能力。

青铜段位

JSON.parse(JSON.stringify(data))
复制代码

这种写法非常简单,而且能够应答大部分的利用场景,然而它有很大缺点的。如果你不晓得它有那些缺点,而且这种实现办法体现不出你任何能力,所以这种实现办法处于青铜段位。

  • 如果对象中存在循环援用的状况也无奈正确实现深拷贝。
const a = {b: 1,}
a.c = a;
JSON.parse(JSON.stringify(a));

  • 如果 data 外面有工夫对象,则 JSON.stringify 后再 JSON.parse 的后果,工夫将只是字符串的模式。而不是工夫对象。
const a = {b: new Date(1536627600000),
}
console.log(JSON.parse(JSON.stringify(a)))

  • 如果 data 里有 RegExp、Error 对象,则序列化的后果将只失去空对象;
const a = {b: new RegExp(/\d/),
    c: new Error('谬误')
}
console.log(JSON.parse(JSON.stringify(a)))

  • 如果 data 里有函数,undefined,则序列化的后果会把函数置为 undefined 或失落;
const a = {b: function (){console.log(1)
    },
    c:1,
    d:undefined
}
console.log(JSON.parse(JSON.stringify(a)))

  • 如果 data 里有 NaN、Infinity 和 -Infinity,则序列化的后果会变成 null
const a = {
    b: NaN,
    c: 1.7976931348623157E+10308,
    d: -1.7976931348623157E+10308,
}
console.log(JSON.parse(JSON.stringify(a)))

白银段位

深拷贝的外围就是对援用类型的数据的拷贝解决。

function deepClone(target){if(target !== null && typeof target === 'object'){let result = {}
        for (let k in target){if (target.hasOwnProperty(k)) {result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{return target;}
}

以上代码中,deepClone函数的参数 target 是要深拷贝的数据。

执行 target !== null && typeof target === 'object' 判断 target 是不是援用类型。

若不是,间接返回 target

若是,创立一个变量 result 作为深拷贝的后果,遍历 target,执行 deepClone(target[k]) 把 target 每个属性的值深拷贝后赋值到深拷贝的后果对应的属性 result[k] 上,遍历结束后返回 result

在执行 deepClone(target[k]) 中,又会对 target[k] 进行类型判断,反复上述流程,造成了一个递归调用 deepClone 函数的过程。就能够层层遍历要拷贝的数据,不论要拷贝的数据有多少子属性,只有子属性的值的类型是援用类型,就会调用 deepClone 函数将其深拷贝后赋值到深拷贝的后果对应的属性上。

另外应用 for...in 循环遍历对象的属性时,其原型链上的所有属性都将被拜访,如果只有只遍历对象本身的属性,而不遍历继承于原型链上的属性,要应用 hasOwnProperty 办法过滤一下。

在这里能够向面试官展现你的三个编程能力。

  • 对原始类型和援用类型数据的判断能力。
  • 对递归思维的利用的能力。
  • 深刻了解 for...in 的用法。

黄金段位

白银段位的代码中只思考到了援用类型的数据是对象的状况,漏了对援用类型的数据是数组的状况。

function deepClone(target){if(target !== null && typeof target === 'object'){let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
        for (let k in target){if (target.hasOwnProperty(k)) {result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{return target;}
}

以上代码中,只是额定减少对参数 target 是否是数组的判断。执行 Object.prototype.toString.call(target) === "[object Array]" 判断 target 是不是数组,若是数组,变量result 为 [],若不是数组,变量result 为 {}

在这里能够向面试官展现你的两个编程能力。

  • 正确理解援用类型概念的能力。
  • 准确判断数据类型的能力。

铂金段位

假如要深拷贝以下数据 data

let data = {a: 1};
data.f=data

执行 deepClone(data),会发现控制台报错,错误信息如下所示。

这是因为递归进入死循环导致栈内存溢出了。根本原因是 data 数据存在循环援用,即对象的属性间接或间接的援用了本身。

function deepClone(target) {function clone(target, map) {if (target !== null && typeof target === 'object') {let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
            if (map[target]) {return map[target];
            }
            map[target] = result;
            for (let k in target) {if (target.hasOwnProperty(k)) {result[k] = deepClone(target[k])
                }
            }
            return result;
        } else {return target;}
    }
    let map = {}
    const result = clone(target, map);
    map = null;
    return result
}

以上代码中利用额定的变量 map 来存储以后对象和拷贝对象的对应关系,当须要拷贝以后对象时,先去 map 中找,有没有拷贝过这个对象,如果有的话间接返回,如果没有的话持续拷贝,这样就奇妙化解的循环援用的问题。最初须要把变量 map 置为 null,开释内存,避免内存泄露。

在这里能够向面试官展现你的两个编程能力。

  • 对循环援用的了解,如何解决循环援用引起的问题的能力。
  • 对内存泄露的意识和防止泄露的能力。

砖石段位

该段位要思考性能问题了。在下面的代码中,咱们遍历数组和对象都应用了 for...in 这种形式,实际上 for...in 在遍历时效率是非常低的,故用效率比拟高的 while 来遍历。

function deepClone(target) {
    /**
     * 遍历数据处理函数
     * @array 要解决的数据
     * @callback 回调函数,接管两个参数 value 每一项的值 index 每一项的下标或者 key。*/
    function handleWhile(array, callback) {
        const length = array.length;
        let index = -1;
        while (++index < length) {callback(array[index], index)
        }
    }
    function clone(target, map) {if (target !== null && typeof target === 'object') {let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
            if (map[target]) {return map[target];
            }
            map[target] = result;

            const keys = Object.prototype.toString.call(target) === "[object Array]" ? undefined : Object.keys(target);

            function callback(value, key) {if (keys) {
                    // 如果 keys 存在则阐明 value 是一个对象的 key,不存在则阐明 key 就是数组的下标。key = value;
                }
                result[key] = clone(target[key], map)
            }
            handleWhile(keys || target, callback)
            return result;
        } else {return target;}
    }
    let map = {}
    const result = clone(target, map);
    map = null;
    return result
}

用 while 遍历的深拷贝记为 deepClone,把用 for ... in 遍历的深拷贝记为 deepClone1。利用 console.time() 和 console.timeEnd() 来计算执行工夫。

let arr = [];
for (let i = 0; i < 1000000; i++) {arr.push(i)
}
let data = {a: arr};
console.time();
const result = deepClone(data);
console.timeEnd();
console.time();
const result1 = deepClone1(data);
console.timeEnd();

从上图显著能够看到用 while 遍历的深拷贝的性能远优于用 for ... in 遍历的深拷贝。

在这里能够向面试官展现你的四个编程能力。

  • 具备优化代码运行性能的能力。
  • 理解遍历的效率的能力。
  • 理解 ++i 和 i++ 的区别。
  • 代码形象的能力。

星耀段位

在这个阶段应该思考代码逻辑的严谨性。在下面段位的代码尽管曾经满足平时开发的需要,然而还是有几处逻辑不谨严的中央。

  • 判断数据不是援用类型时就间接返回 target,然而原始类型中还有 Symbol 这一非凡类型的数据,因为其每个 Symbol 都是举世无双,须要额定拷贝解决,不能间接返回。
  • 判断数据是不是援用类型时不谨严,漏了 typeof target === function' 的判断。
  • 只思考了 Array、Object 两种援用类型数据的解决,援用类型的数据还有 Function 函数、Date 日期、RegExp 正则、Map 数据结构、Set 数据机构,其中 Map、Set 属于 ES6 的。

废话不多说,间接贴上全副代码,代码中有正文。

function deepClone(target) {
    // 获取数据类型
    function getType(target) {return Object.prototype.toString.call(target)
    }
    // 判断数据是不是援用类型
    function isObject(target) {return target !== null && (typeof target === 'object' || typeof target === 'function');
    }
    // 解决不须要遍历的应援用类型数据
    function handleOherData(target) {const type = getType(target);
        switch (type) {case "[object Date]":
                return new Date(target)
            case "[object RegExp]":
                return cloneReg(target)
            case "[object Function]":
                return cloneFunction(target)

        }
    }
    // 拷贝 Symbol 类型数据
    function cloneSymbol(targe) {const a = String(targe); // 把 Symbol 字符串化
        const b = a.substring(7, a.length - 1); // 取出 Symbol()的参数
        return Symbol(b); // 用原先的 Symbol()的参数创立一个新的 Symbol}
    // 拷贝正则类型数据
    function cloneReg(target) {
        const reFlags = /\w*$/;
        const result = new target.constructor(target.source, reFlags.exec(target));
        result.lastIndex = target.lastIndex;
        return result;
    }
    // 拷贝函数
    function cloneFunction(targe) {
        // 匹配函数体的正则
        const bodyReg = /(?<={)(.|\n)+(?=})/m;
        // 匹配函数参数的正则
        const paramReg = /(?<=\().+(?=\)\s+{)/;
        const targeString = targe.toString();
        // 利用 prototype 来辨别下箭头函数和一般函数,箭头函数是没有 prototype 的
        if (targe.prototype) { // 一般函数
            const param = paramReg.exec(targeString);
            const body = bodyReg.exec(targeString);
            if (body) {if (param) {const paramArr = param[0].split(',');
                    // 应用 new Function 从新结构一个新的函数
                    return new Function(...paramArr, body[0]);
                } else {return new Function(body[0]);
                }
            } else {return null;}
        } else { // 箭头函数
            //eval 和函数字符串来从新生成一个箭头函数
            return eval(targeString);
        }
    }
    /**
     * 遍历数据处理函数
     * @array 要解决的数据
     * @callback 回调函数,接管两个参数 value 每一项的值 index 每一项的下标或者 key。*/
    function handleWhile(array, callback) {
        let index = -1;
        const length = array.length;
        while (++index < length) {callback(array[index], index);
        }
    }
    function clone(target, map) {if (isObject(target)) {
            let result = null;
            if (getType(target) === "[object Array]") {result = []
            } else if (getType(target) === "[object Object]") {result = {}
            } else if (getType(target) === "[object Map]") {result = new Map();
            } else if (getType(target) === "[object Set]") {result = new Set();
            }

            // 解决循环援用
            if (map[target]) {return map[target];
            }
            map[target] = result;

            if (getType(target) === "[object Map]") {target.forEach((value, key) => {result.set(key, clone(value, map));
                });
                return result;
            } else if (getType(target) === "[object Set]") {
                target.forEach(value => {result.add(clone(value, map));
                });
                return result;
            } else if (getType(target) === "[object Object]" || getType(target) === "[object Array]") {const keys = getType(target) === "[object Array]" ? undefined : Object.keys(target);

                function callback(value, key) {if (keys) {
                        // 如果 keys 存在则阐明 value 是一个对象的 key,不存在则阐明 key 就是数组的下标。key = value
                    }
                    result[key] = clone(target[key], map)
                }
                handleWhile(keys || target, callback)
            } else {result = handleOherData(target)
            }
            return result;
        } else {if (getType(target) === "[object Symbol]") {return cloneSymbol(target)
            } else {return target;}
        }
    }
    let map = {}
    const result = clone(target, map);
    map = null;
    return result
}

在这里能够向面试官展现你的六个编程能力。

  • 代码逻辑的严谨性。
  • 深刻理解数据类型的能力。
  • JS Api 的纯熟应用的能力。
  • 理解箭头函数和一般函数的区别。
  • 纯熟应用正则表达式的能力。
  • 模块化开发的能力

王者段位

以上代码中还有很多数据类型的拷贝,没有实现,有趣味的话能够在评论中实现一下,王者属于你哦!

总结

综上所述,面试官叫你实现一个深拷贝,其实是要考查你各方面的能力。例如

  • 白银段位

    • 对原始类型和援用类型数据的判断能力。
    • 对递归思维的利用的能力。
  • 黄金段位

    • 正确理解援用类型概念的能力。
    • 准确判断数据类型的能力。
  • 铂金段位

    • 对循环援用的了解,如何解决循环援用引起的问题的能力。
    • 对内存泄露的意识和防止泄露的能力。
  • 砖石段位

    • 具备优化代码运行性能的能力。
    • 理解遍历的效率的能力。
    • 理解 ++i 和 i++ 的区别。
    • 代码形象的能力。
  • 星耀段位

    • 代码逻辑的严谨性。
    • 深刻理解数据类型的能力。
    • JS Api 的纯熟应用的能力。
    • 理解箭头函数和一般函数的区别。
    • 纯熟应用正则表达式的能力。
    • 模块化开发的能力

所以不要去死记硬背一些手写代码的面试题,最好本人入手写一下,看看本人达到那个段位了。

最初

对于大厂面试,我最初想要强调的一点就是心态真的很重要,是决定你在面试过程中施展的要害,若不能失常施展,很可能就因为一个小失误与 offer 失之交臂,所以肯定要器重起来。另外揭示一点,充沛温习,是打消你缓和的心理状态的要害,但你温习充沛了,天然面试过程中就要有底气得多。

我平时始终有整顿面试题的习惯,有随时跳出舒服圈的筹备,人不知; 鬼不觉整顿了 229 页了,在这里分享给大家,有须要的点击这里收费支付题目 + 解析 PDF

篇幅无限,仅展现局部内容

如果你须要这份完整版的面试题 + 解析,【点击我】就能够了。

心愿大家明年的金三银四面试顺利,拿下本人心仪的 offer!

退出移动版