大家好,我是晚天。

手写代码是前端面试中必不可少的环节,常见的手写代码题也根本是可枚举的。网上也有很多面经文章会讲到手写代码,然而大都不够全面、不够深度,有些低质量文章甚至会误导初学者。因而,打算出一个【前端手写代码】系列文章,全面深度的解说下常见的前端面试手写代码题,并通过手写代码题,延长解说下背地面试官想要考查的根底能力。为了保障尽量全面深度,本系列文章会参考其余优良文章,参考文章会附到参考资料中。

明天从深克隆开始讲起,深克隆是前端面试中十分高频的手写代码题,置信有肯定前端面试教训的前端同学都手写过,然而在持续看下文之前,请先问下本人这三个问题:

  • 你真的了解深克隆吗?
  • 在面试官眼里,怎么的深克隆办法才是合格的?
  • 怎么可能写一个更好的深克隆办法?

了解深克隆 & 浅克隆

要了解深克隆和浅克隆的不同,须要先了解 JavaScript 中的根底类型和援用类型的区别。

知识点1:JavaScript 数据类型。


在 JavaScript 中有两种不同的数据类型:根底类型和援用类型。

根底类型有 Number、Boolean、String、Symbol、Null 和 Undefined;
援用类型有 Object、Array、Function 等。

根底类型的传递形式均为按值传递,援用类型的传递形式均为按援用传递。


知识点2:按值传递和按援用传递


按值传递(Passing by value)意味着每次赋值给一个变量,都会将值的拷贝赋值给这个变量;

以下例子,能够形象的解释按值传递的过程。

let a = 1;let b = a;b = b + 2;console.log(a); // 1console.log(b); // 3

将 a 赋值给 b,会将 a 的值的拷贝赋值给 b。当对 b 进行从新赋值时,不会对 a 产生任何影响。所以最终 a !== b。

按援用传递(Passing by reference),则只会传递对对象的援用,而不是对象的拷贝。援用雷同的两个对象,批改任何一个对象,都会影响对方。

如下,x 是一个数组 Array 类型,Array 类型是一个援用类型,Array 类型实质上是 Object 类型的子类型,遵循按援用传递。变量 x 赋值给变量 y,只会将 x 的援用传递给 y。此时,x 和 y 领有对同一个数组独特的援用。

const x = [1];typeof x; // objectx instanceof Array; // truex instanceof Object; // trueArray.prototype.__proto__ === Object.prototype; // true,Array 的原型指向 Object 的原型

如上面例子,批改 x 和 y 任意一个变量,均会影响另一个,因为 x 和 y 领有对同一个数组的援用:

let x = [1];let y = x;y.push(2);console.log(x); // [1, 2]console.log(y); // [1, 2]

知识点3:深克隆和浅克隆的区别。


深克隆和浅克隆的区别在于,浅克隆在复制援用类型对象时,会将同一个援用传递给新的克隆对象;深克隆在复制援用类型对象时,则会将援用类型的值的拷贝传递给新的克隆对象。因为浅克隆无需对对象类型的值进行拷贝,因而相较深克隆性能更好。

两张图来解释浅克隆和深克隆的不同:

浅克隆:

深克隆:


了解了深克隆和浅克隆的概念以及不同,咱们当初来正式手写代码实现深克隆。

丐版实现

实现深克隆,最简略的形式是应用 JSON.parse(JSON.stringify(target)) 的形式,然而这种形式有显著的缺点,比方:

  • 无奈克隆函数;
  • 无奈克隆存在死循环的对象;
  • 等等;

    JSON.parse(JSON.stringify(target));

因而,咱们须要手动实现一个深克隆办法。

根底实现

首先,咱们来实现一个浅克隆,通过遍历的形式实现对指标对象的克隆。

function shadowClone(target) {    let cloneTarget = {};    for (const key in target) {        cloneTarget[key] = target[key];    }    return cloneTarget;};

如果要实现一个深克隆,咱们须要基于以上实现新增两个逻辑:

  • 如果是根底类型,能够间接返回根底类型值;
  • 如果是援用类型,则须要通过递归的形式根底反对 clone 办法,返回新的拷贝对象;
function deepClone(target){    if(typeof target !== 'object'){    return target;  }    const cloneTarget = {};    for(const key in target){    cloneTarget[key] = deepClone(target[key]);  }    return cloneTarget;  }

上述实现,在面对对象类型的援用类型时,根本是能够满足需要的。然而如果是数组类型呢?上述实现会将数组也转化为对象。因而,咱们接下来思考兼容数组。

思考数组

当咱们思考数组时,就会波及到一个新的知识点,如何判断数组类型。

知识点4:数组类型的判断。


JavaScript 的类型判断,有以下几种形式:

  • Array.isArray()

    const arr = [1,2,3,4,5];Array.isArray(arr); // trueconst obj = {id: 1, name: “Josh”};Array.isArray(obj); // false
  • instanceof

    const arr = [1,2,3,4,5];data instanceof Array; // trueconst obj = {id: 1, name: “Josh”};data instanceof Array; // false
  • Object.prototype.toString.call

    const data = [1,2,3,4,5];Object.prototype.toString.call(data) === '[object Array]'; // trueconst data = {id: 1, name: “Josh”};Object.prototype.toString.call(data) === '[object Array]'; // false

这里,可能有初学者会应用 typeof 来判断类型,然而 typeof 是无奈判断数组类型的。

知识点5:typeof。


typeof 的返回类型如下:

  • string
  • number
  • undefined
  • object
  • function
  • boolean
  • symbol
  • bigint

数组的 typeof 的返回值也是 object。

typeof []; // object

接下来,咱们在代码实现中思考数组类型的状况。

function deepClone(target){  if(typeof target !== 'object'){    return target;  }    const cloneTarget = Array.isArray(target) ? [] : {};    for(let key in target){    cloneTarget[key] = deepClone(target[key]);  }    return cloneTarget;  }

思考循环援用

如果当一个对象存在循环援用时,应用上述实现,会怎么呢?

const obj = {a: {b: 1}, c: 'hello'}obj.a.c = obj;cloneDeep(obj);

执行上述代码,咱们将失去如下后果。

因为 obj 中存在循环援用,所以对 cloneDeep 的调用将有限循环上来,最终导致内存溢出。

要解决上述问题,咱们须要一个存储空间来存储以后对象和拷贝对象的关系,如果以后对象曾经被拷贝过,则间接返回存储的拷贝对象,如果没有被拷贝过,则进行拷贝,并将拷贝对象存储到存储空间中。这个存储空间须要是 key-value 模式的,因而咱们抉择 Map 进行存储。

应用 Map 的实现形式如下:

function deepClone(target, map = new Map()) {  if (typeof target !== 'object') {    return target;  }    let cloneTarget = Array.isArray(target) ? [] : {};  if (map.get(target)) {    return map.get(target);  }    map.set(target, cloneTarget);  for (const key in target) {    cloneTarget[key] = deepClone(target[key], map);  }    return cloneTarget;   };

从新拷贝上述存在循环援用的对象 obj,失去如下后果:

存在循环援用的对象已能够失常深克隆。

相熟 ES6 的同学必定曾经想到了 Map 的孪生姊妹 WeakMap。此处,咱们能够应用 WeakMap 对咱们的实现持续进行优化。

知识点6:Map 和 WeakMap 的区别


WeakMap 是弱援用的键值对汇合,键值必须是对象,值能够是任意类型。和 Map 的区别就在于 Map 是强援用,WeakMap 是弱援用。
与Map对象不同的是,WeakMap的键是不可枚举的。不提供列出其键的办法

什么是弱援用呢?弱援用意味着在没有其余援用的状况下,弱援用的对象能够被垃圾回收机制回收。如果一个对象只有弱援用存在,则该对象会在下一次垃圾回收机制执行时被回收。

当咱们在深克隆一个很大的对象时,应用 Map 将造成很大的性能损耗,咱们必须手动分明 Map 的键值来开释内存。WeakMap 则不会有这个问题。


思考原型链

上述实现中,咱们获取对象的 key 应用的办法是 for...in,然而 for...in 有个问题,它会获取到原型链上所有除 Symbol 以外的可枚举属性。对于深克隆来说,只须要克隆对象自身的属性即可,不须要克隆原型链上的非本身属性。那如何获取对象自身属性呢?

知识点7:如何获取对象本身属性


获取对象本身属性有以下几种形式:

  • Object.getOwnPropertyNames() 获取对象本身所有属性,包含不可枚举属性;
  • Object.keys() 获取对象的本身可枚举属性;
  • for...in + Object.hasOwnProperty() + Object.getOwnPropertySymbols()

    • for...in 以任意程序迭代一个对象的除Symbol以外的可枚举属性,包含继承的可枚举属性;
    • Object.hasOwnProperty() 判断属性是否是对象本身属性;
    • Object.getOwnPropertySymbols() 获取对象 Symbol 类型的属性;

可见,如果咱们的指标是获取对象本身的所有属性,Object.getOwnPropertyNames() 是一个适合的办法。


基于上述剖析,对咱们的实现持续进行优化。

function cloneDeep(target, map = new WeakMap()) {    if (typeof target !== 'object') {        return target;    }    if (map.has(target)) {        return map.get(target);    }    const cloneTarget = Array.isArray(target) ? [] : {};    map.set(target, cloneTarget);    Object.getOwnPropertyNames(target).forEach((key) => {        cloneTarget[key] = cloneDeep(target[key], map);    });    return cloneTarget;}const obj = {a: {value: 1}, b: {value: 2}}Object.defineProperty(obj, 'a', {enumerable: true});Object.defineProperty(obj, 'b', {enumerable: false});const newObj = cloneDeep(obj);console.log(newObj); // { a: { value: 1 }, b: { value: 2 } }

其余更多类型

在上述实现中,援用类型咱们只思考了一般对象和数组。事实上,援用类型远远不止这两个,比方 String、Number、Boolean、RegExp、Date、Map、Set、Error 等。

咱们如何获取到实在的援用类型呢?通过 Object.prototype.toString 办法,咱们能够察看到各种不同对象实例的 toString 都会遵循雷同的格局输入。

知识点8:获取援用对象实在类型的办法


因而,能够通过以下办法:

function getType(obj){  return Object.prototype.toString.call(obj).slice(8, -1);}

其中 Symbol 类型须要特地留神一下:

知识点9:Symbol 类型的创立办法

symbol 是一种根本数据类型(primitive data type)。Symbol() 函数会返回 symbol 类型的值,该类型具备动态属性和静态方法。它的动态属性会裸露几个内建的成员对象;它的静态方法会裸露全局的 symbol 注册,且相似于内建对象类,但作为构造函数来说它并不残缺,因为它不反对语法:"new Symbol()"。

能够通过如下形式创立 Symbol 类型的数据:

const sym = Symbol(1); // Symbol(1)typeof sym; // 'symbol'

不能通过 new Symbol() 的形式创立 Symbol 类型,因为 Symbol 不是构造函数。
如果真的想创立 Symbol 对象,能够通过如下形式:

const sym = Object(Symbol(1)); // Symbol {Symbol(1), description: '1'}typeof sym; // 'object'Object.prototype.toString.call(sym); // '[object Symbol]'

接下来,咱们来反对 Map、Set、RegExp、Number、String、Boolean、Date、Error、Null类型和 Symbol 对象类型。其中 Symbol 对象类型,指的是通过 Object(Symbol()) 形式创立的 Symbol 类型。

最终实现

基于上述优化,咱们最终实现了尽可能兼容各种状况的深克隆办法,并通过测试验证性能正确性:

function cloneDeep(target, map = new WeakMap()) {    if (typeof target !== 'object') {        return target;    }    if (map.has(target)) {        return map.get(target);    }    const type = Object.prototype.toString.call(target).slice(8, -1);    let cloneTarget;    switch (type) {        case 'Object':        case 'Array':            cloneTarget = type === 'Array' ? [] : {};            map.set(target, cloneTarget);            Object.getOwnPropertyNames(target).forEach(key => {                cloneTarget[key] = cloneDeep(target[key], map);            });            break;        case 'Map':            cloneTarget = new Map();            map.set(target, cloneTarget);            target.forEach((value, key) => {                cloneTarget.set(cloneDeep(key, map), cloneDeep(value, map));            });            break;        case 'Set':            cloneTarget = new Set();            map.set(target, cloneTarget);            target.forEach((value) => {                cloneTarget.add(cloneDeep(value, map));            });            break;        case 'RegExp':        case 'Number':        case 'String':        case 'Boolean':        case 'Date':        case 'Error':            cloneTarget = new target.constructor(target);            break;        case 'Symbol':            cloneTarget = Object(Object.prototype.valueOf.call(target));            break;        case 'Null':            cloneTarget = null;            break;    }    return cloneTarget;}// testconst map = new Map();map.set('key', 'value');const set = new Set();set.add('value1');set.add('value2');const obj = {    field1: 1,    field2: undefined,    field3: {        child: 'child'    },    field4: [2, 4, 8],    empty: null,    map,    set,    bool: new Boolean(true),    num: new Number(2),    str: new String(2),    symbol: Object(Symbol(1)),    date: new Date(),    reg: /\d+/,    error: new Error(),    func1: () => {        console.log('hello friend!');    },    func2: function (a, b) {        return a + b;    }};console.log(obj);const copy = cloneDeep(obj);console.log(copy);

知识点回顾

前文中,咱们通过对深克隆办法的一直优化,延长学习了以下知识点:

  1. JavaScript 数据类型
  2. 值传递 & 对象传递
  3. 深克隆和浅克隆的区别
  4. 数组类型的判断
  5. typeof
  6. Map 和 WeakMap 的区别
  7. 如何获取对象本身属性
  8. 如何获取援用对象的实在类型
  9. Symbol 类型的创立办法

    总结

    通过上文,咱们一直对深克隆的办法进行优化,逐渐反对了所有根底类型(number/boolean/string/undefined/bigint/function/symbol)、援用类型(Object/Array/Map/Set/Symbol/Error/Regex/Number/String/Boolean/Date/Null)的反对,并且思考到对原型链、循环援用等状况的兼容。

敬请其余《前端手写代码系列文章》,其余更多内容欢送拜访晚天的个人主页。

参考资料

  • Pass by Value and Pass by Reference in Javascript
  • The Difference Between Values and References in JavaScript
  • [[JavaScript] Check if a variable is a type of an Object or Array?](https://amandeepkochhar.mediu...)
  • WeakMap
  • Write a Better Deep Clone Function in JavaScript
  • 一篇搞定对象的深克隆
  • Shallow Copy vs Deep Copy in JavaScript
  • Create cloneDeep - BFT
  • Javascript经典面试之深拷贝VS浅拷贝
  • Difference between Shallow and Deep copy of a class