专栏首页李才哥前端面试拔高题

前端面试拔高题

如何深拷贝一个对象
function copyFn(obj) {
  if (obj == null) {
    return null
  }
  var result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {// 如果是对象,再次调用该方法自身result[key] = copyFn(obj[key]);      } else {
        result[key] = obj[key];
      }
    }
  }
  return result;
}
Object.prototype.clone = function() {
  var copy = (this instanceof Array) ? [] : {};
  for (var attr in this) {
    if (this.hasOwnProperty(attr)){
copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];}
  }
  return copy;
};

对象是 JS 中基本类型之一,而且和原型链、数组等知识息息相关。不管是面试中,还是实际开发中我们都会碰见深拷贝对象的问题。

顾名思义,深拷贝就是完完整整的将一个对象从内存中拷贝一份出来。所以无论用什么办法,必然绕不开开辟一块新的内存空间。

通常有下面两种方法实现深拷贝:

  1. 迭代递归法
  2. 序列化反序列化法

我们会基于一个测试用例对常用的实现方法进行测试并对比优劣:

let test = {
    num: 0,
    str: '',
    boolean: true,
    unf: undefined,
    nul: null,
    obj: {
        name: '我是一个对象',
        id: 1
    },
    arr: [0, 1, 2],
    func: function() {
        console.log('我是一个函数')
    },
    date: new Date(0),
    reg: new RegExp('/我是一个正则/ig'),
    err: new Error('我是一个错误')
}

let result = deepClone(test)

console.log(result)
for (let key in result) {
    if (isObject(result[key]))
        console.log(`${key}相同吗? `, result[key] === test[key])
}

// 判断是否为对象
function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
}

迭代递归法

这是最常规的方法,思想很简单:就是对对象进行迭代操作,对它的每个值进行递归深拷贝。

for...in 法

// 迭代递归法:深拷贝对象与数组
function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }

    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [] : {}
    for (let key in obj) {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    }

    return cloneObj
}

结果:

迭代递归法结果.png

我们发现,arr 和 obj 都深拷贝成功了,它们的内存引用已经不同了,但 func、date、reg 和 err 并没有复制成功,因为它们有特殊的构造函数。

Reflect 法

// 代理法
function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }

    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [...obj] : { ...obj }
    Reflect.ownKeys(cloneObj).forEach(key => {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    })

    return cloneObj
}

结果:

代理法结果

我们发现,结果和使用 for...in 一样。那么它有什么优点呢?读者可以先猜一猜,答案我们会在下文揭晓。

lodash中的深拷贝实现

著名的 lodash 中的 cloneDeep 方法同样是使用这种方法实现的,只不过它支持的对象种类更多,具体的实现过程读者可以参考 lodash 的 baseClone 方法。

我们把测试用例用到的深拷贝函数换成lodash的:

let result = _.cloneDeep(test)

结果:

lodash深拷贝结果.png

我们发现,arr、obj、date、reg深拷贝成功了,但 func 和 err 内存引用仍然不变。

为什么不变呢?这个问题留给读者自己去探寻,嘿嘿~不过可以提示下,这跟 lodash 中的 cloneableTags 有关。

由于前端中的对象种类太多了,所以 lodash 也给用户准备了自定义深拷贝的方法 cloneDeepWith,比如自定义深拷贝 DOM 对象:

function customizer(value) {
  if (_.isElement(value)) {
    return value.cloneNode(true);
  }
}
 
var el = _.cloneDeepWith(document.body, customizer);
 
console.log(el === document.body);
// => false
console.log(el.nodeName);
// => 'BODY'
console.log(el.childNodes.length);
// => 20

序列化反序列化法

这个方法非常有趣,它先把代码序列化成数据,再反序列化回对象:

// 序列化反序列化法
function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj))
}

结果:

序列化反序列化法结果.png

我们发现,它也只能深拷贝对象和数组,对于其他种类的对象,会失真。这种方法比较适合平常开发中使用,因为通常不需要考虑对象和数组之外的类型。

进阶

  1. 对象成环怎么办? 我们给 test 加一个 loopObj 键,值指向自身:
test.loopObj = test

这时我们使用第一种方法中的 for..in 实现和 Reflect 实现都会栈溢出:

环对象深拷贝报错

而使用第二种方法也会报错:

但 lodash 却可以得到正确结果:

lodash 深拷贝环对象.png

为什么呢?我们去 lodash 源码看看:

lodash 应对环对象办法.png

因为 lodash 使用的是栈把对象存储起来了,如果有环对象,就会从栈里检测到,从而直接返回结果,悬崖勒马。这种算法思想来源于 HTML5 规范定义的结构化克隆算法,它同时也解释了为什么 lodash 不对 Error 和 Function 类型进行拷贝。

当然,设置一个哈希表存储已拷贝过的对象同样可以达到同样的目的:

function deepClone(obj, hash = new WeakMap()) {
    if (!isObject(obj)) {
        return obj
    }
    // 查表
    if (hash.has(obj)) return hash.get(obj)

    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [] : {}
    // 哈希表设值
    hash.set(obj, cloneObj)

    let result = Object.keys(obj).map(key => {
        return {
            [key]: deepClone(obj[key], hash)
        }
    })
    return Object.assign(cloneObj, ...result)
}

这里我们使用 WeakMap 作为哈希表,因为它的键是弱引用的,而我们这个场景里键恰好是对象,需要弱引用。

  1. 键值不是字符串而是 Symbol

我们修改一下测试用例:

var test = {}
let sym = Symbol('我是一个Symbol')
test[sym] = 'symbol'

let result = deepClone(test)
console.log(result)
console.log(result[sym] === test[sym])

运行 for...in 实现的深拷贝我们会发现:

拷贝失败了,为什么?

因为 Symbol 是一种特殊的数据类型,它最大的特点便是独一无二,所以它的深拷贝就是浅拷贝。

但如果这时我们使用 Reflect 实现的版本:

成功了,因为 for...in 无法获得 Symbol 类型的键,而 Reflect 是可以获取的。

当然,我们改造一下 for...in 实现也可以:

function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }

    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [] : {}
    let symKeys = Object.getOwnPropertySymbols(obj)
    // console.log(symKey)
    if (symKeys.length > 0) {
        symKeys.forEach(symKey => {
            cloneObj[symKey] =  isObject(obj[symKey]) ? deepClone(obj[symKey]) : obj[symKey]
        })
    }
    for (let key in obj) {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    }

    return cloneObj
}
  1. 拷贝原型上的属性

众所周知,JS 对象是基于原型链设计的,所以当一个对象的属性查找不到时会沿着它的原型链向上查找,也就是一个非构造函数对象的 __proto__ 属性。

我们创建一个 childTest 变量,让 result 为它的深拷贝结果,其他不变:

let childTest = Object.create(test)
let result = deepClone(childTest)

这时,我们最初提供的四种实现只有 for...in 的实现能正确拷贝,为什么呢?原因还是在结构化克隆算法里:原形链上的属性也不会被追踪以及复制。

落在具体实现上就是:for...in 会追踪原型链上的属性,而其它三种方法(Object.keys、Reflect.ownKeys 和 JSON 方法)都不会追踪原型链上的属性:

  1. 需要拷贝不可枚举的属性 第四种情况,就是我们需要拷贝类似属性描述符,setters 以及 getters 这样不可枚举的属性,一般来说,这就需要一个额外的不可枚举的属性集合来存储它们。类似在第二种情况使用 for...in 拷贝 Symbol 类型键时: 我们给 test 变量里的 obj 和 arr 属性定义一下属性描述符:
Object.defineProperties(test, {
    'obj': {
        writable: false,
        enumerable: false,
        configurable: false
    },
    'arr': {
        get() {
            console.log('调用了get')
            return [1,2,3]
        },
        set(val) {
            console.log('调用了set')
        }
    }
})

然后实现我们的拷贝不可枚举属性的版本:

function deepClone(obj, hash = new WeakMap()) {
    if (!isObject(obj)) {
        return obj
    }
    // 查表,防止循环拷贝
    if (hash.has(obj)) return hash.get(obj)

    let isArray = Array.isArray(obj)
    // 初始化拷贝对象
    let cloneObj = isArray ? [] : {}
    // 哈希表设值
    hash.set(obj, cloneObj)
    // 获取源对象所有属性描述符
    let allDesc = Object.getOwnPropertyDescriptors(obj)
    // 获取源对象所有的 Symbol 类型键
    let symKeys = Object.getOwnPropertySymbols(obj)
    // 拷贝 Symbol 类型键对应的属性
    if (symKeys.length > 0) {
        symKeys.forEach(symKey => {
            cloneObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey], hash) : obj[symKey]
        })
    }

    // 拷贝不可枚举属性,因为 allDesc 的 value 是浅拷贝,所以要放在前面
    cloneObj = Object.create(
        Object.getPrototypeOf(cloneObj),
        allDesc
    )
    // 拷贝可枚举属性(包括原型链上的)
    for (let key in obj) {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];
    }

    return cloneObj
}

结果:

结语

  1. 日常深拷贝,建议序列化反序列化方法。
  2. 面试时遇见面试官搞事情,写一个能拷贝自身可枚举、自身不可枚举、自身 Symbol 类型键、原型上可枚举、原型上不可枚举、原型上的 Symol 类型键,循环引用也可以拷的深拷贝函数:
// 将之前写的 deepClone 函数封装一下
function cloneDeep(obj) {
    let family = {}
    let parent = Object.getPrototypeOf(obj)

    while (parent != null) {
        family = completeAssign(deepClone(family), parent)
        parent = Object.getPrototypeOf(parent)
    }

    // 下面这个函数会拷贝所有自有属性的属性描述符,来自于 MDN
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
    function completeAssign(target, ...sources) {
        sources.forEach(source => {
            let descriptors = Object.keys(source).reduce((descriptors, key) => {
                descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
                return descriptors
            }, {})

            // Object.assign 默认也会拷贝可枚举的Symbols
            Object.getOwnPropertySymbols(source).forEach(sym => {
                let descriptor = Object.getOwnPropertyDescriptor(source, sym)
                if (descriptor.enumerable) {
                    descriptors[sym] = descriptor
                }
            })
            Object.defineProperties(target, descriptors)
        })
        return target
    }

    return completeAssign(deepClone(obj), family)
}

  1. 有特殊需求的深拷贝,建议使用 lodash 的 copyDeep 或 copyDeepWith 方法。

更多资料

https://www.hangge.com/blog/cache/detail_1802.html

(1)我们定义一个 clone 方法实现深度复制功能(Deep Copy),其内部实现原理就是将对象、数组的属性遍历一遍,赋给一个新的对象。

//自定义的复制方法
function clone(obj) {
  var copy = {};
  for (var attr in obj) {
    copy[attr] = typeof(obj[attr])==='object' ? clone(obj[attr]) : obj[attr];
  }
  return copy;
}
 
//测试样例
var a = {v1:1, v2:2};
var b = clone(a);
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);

(2)也可以直接给 Object 增加个 clone 方法,其内部实现原来同上面是一样的。

//自定义的复制方法
Object.prototype.clone = function() {
  var copy = (this instanceof Array) ? [] : {};
  for (var attr in this) {
    if (this.hasOwnProperty(attr)){
      copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];
    }
  }
  return copy;
};
 
//测试样例
var a = {v1:1, v2:2};
var b = a.clone();
b.v1 = 3;
console.log("对象a:",a);
console.log("对象b:",b);
简单阐述一下es6中新增的原始数据类型和其作用
symbol

symbol是基本数据类型,每一个symbol值都是一个全局唯一的字符串,你永远不会知道它里面存的什么,symbol值可以作为对象的属性的键。

Symbol的特点

  • 独一无二
  • 不能隐式转换
  • 不能与其它数据类型做运算
  • 不能使用点运算符进行操作

总结:

  1. window.Symbol()
  2. Symbol()前面不能加new
  3. Symbol() !== Symbol()
  4. object[Symbol()]='xxx' 可以作为对象的键,创造私有属性

symbol作用: 可以造一个隐藏属性 使用方法:在一个块级作用域里使用symbol

{
    let a = Symbol()
    let object = {
        name: 'ooo',
        age: 10,
        [a]: '隐藏属性'
    }
    window.object = object
}

上面的代码我们可以访问到它里面的name和age但是我们不能访问到a,包括我们直接通过object[Symbol]也访问不到它

Set

set类型是Object里面的一种 Set对象里面只要有重复的值他都会只保留一个,无论是原始值还是对象引用。

作用:实现数组去重

  • 不使用Set实现数组去重
var a = [1,2,3,1,5,6,2]
function uniq(array){
    var result = []
    var hash = {}
    for(var i =0;i<array.length;i++){
        hash[array[i]]=true
    }
    for(key in hash){
        result.push(key)
    }
    return result
}
uniq(a)
//["1", "2", "3", "5", "6"]

问题: 1.他无法区分数字和字符串 2.如果数组里面有对象我们的去重方法就会失败,因为对象里面的下标(键)只能是字符串

  • 使用Set实现数组去重

其中Array.from是将任意类型转化成一个数组

Map

可以允许任何类型作为对象的键,弥补了object只能使用字符串作为键的问题

  • 通过for...of进行遍历 .keys()拿到所有的key
for(let key of map.keys()){
    console.log(key)
}

.values()拿到所有的value

for(let key of map.values()){
    console.log(key)
}

.entries()拿到所有的key和value

for(let key of map.entries()){
    console.log(key)
}

weakSet和WeakMap是弱引用,放在它们的key里面的东西会自然消失,它无法知道自己有哪些值

更多资料

https://blog.csdn.net/m0_38134431/article/details/83897315

在任何时代,

教育说起来都是一件高大上的事,

但却没有什么真正有价值的东西是教得会的,

没有任何一种文化模因

可以说清楚一个个体的全部问题。

在任何时代,

想要抓住人性的弱点来赚钱都非常容易,

没有一点高级。

相反,想要建设一种文化,

耐心地拆除信息壁垒,

并且能够坚持下来,

那真不是一般的不易。

在任何时代,

在一秒钟内看到本质的人,

和花半辈子看不清的人,

自然是不一样的命运。

每一天,你将受到才哥的理论,结合历史、政治、文化、艺术、商业故事,令人防不胜防的高纯度无死角知识轰炸。以及,不间断的私藏书籍、电影推荐。

有时候,某件事虽记不清楚,但总感觉这样的事在很久很久以前发生过...

有时候,某个人虽从未见过,但总感觉面前的人在另一个时空里曾遇见......

那时候相忘于江湖的事,或许穿越了时光,有了新的世界;

那时候不远万里追寻的梦,也许穿行过人海,也有了新的意义;

而对于我们来说,那个惦念的江湖,那个执着的梦,其核心都是在偌大的世界里,寻找到更真实更好的自己!

一个人在比较了自己与别人的力量和弱点之后,如果仍然看不出差别的话, 那么他将很容易被敌人打败。人们常常抱怨,人生中有太多的敌人,包括在学习 中遇到的沟壑,事业上难爬的高山,生活中隐藏的陷阱,稍一不慎,就将前功尽 弃或跌入沟壑。可是,人们哪里知道,真正的敌人不在眼前,也不在背后,就在 自己面前。只有战胜各种艰难困苦,才能赢得成功。当然,要战胜艰难险阻,首 先要战胜自己,如果连自己都不能战胜,何谈战胜别人和各种险阻。

世界著名游泳健将佛洛伦丝·查德威克,在一次从卡得林那岛游向加利福尼 亚海湾时,已经在海水中浸泡了16小时的她,看见眼前大雾茫茫、一望无际的 海面时,顿时精疲力竭、浑身困乏,没有继续坚持下去,失去了一次创造世界纪 录的机会。后来,她才得知自己已经快要到达成功的彼岸了,阻碍她成功的不是 茫茫无际的大海,而是自己的疑惑、自我放弃,最终被自己的潜意识所打倒。之后过了两个多月,佛洛伦丝·查德威克又一次重游了加利福尼亚海湾,每一刻她 都不停的对自己说:“离彼岸越来越近了”,潜意识激发了她“我一定能到达彼 岸”的信号,经过不懈的努力和坚持,她终于实现目标,成功到达了彼岸。

我们无法避免在追求成功的道路中所遇到的荆棘与挫折,但是,当你将挫折当成痛苦去对待的时候,疲惫和痛苦将纠缠着你,失望和放弃将一直笼罩着你。如果我们把每天经历过的种种痛苦、烦恼仔细分析一下,会发现,这痛苦、这困 扰的来源很大一部分是战胜不了自己。很多时候,我们在困难和挫折面前,否定 了自己,以至于让自己打败了你。所以,很多人失败,通常是输给了自己。

本文分享自微信公众号 - 李才哥(liqi13695515224),作者:中奋boss

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JS实现clone()方法,对五种主要数据类型进行值复制

    apply方法: 语法:apply([thisObj[,argArray]]) 定义:应用某一对象的一个方法,用另一个对象替换当前对象。 说明: 如果 ...

    李才哥
  • 前端面试系列(4)

    第二步:true < 1,这时true会转为1,false会转为0进行比较,因此这一步结果为false

    李才哥
  • 关于对象莫名其妙的面试题

    李才哥
  • JS深拷贝方法

    薛定喵君
  • Django 实现对已存在的model进行更改

    3.python manage.py makemigrations yourappname

    砸漏
  • JS深拷贝几种实现方法

    javascript.shop
  • ES6之对象的扩展

    对象有一个描述对象,通过Object.getOwnPropertyDescriptor方法可以获取:

    wade
  • 12种不宜使用的Javascript语法

    这几天,我在读《Javascript语言精粹》。 这本书很薄,100多页,正好假日里翻翻。 该书的作者是Douglas Crockford,他是目前世界上最精通...

    ruanyf
  • How to Share Git Tag

    Now I have two tags under my git repository. Let’s take a look at how to push th...

    技术小黑屋
  • SAP CDS view源代码行数统计工具

    Part1 – how to test odata service generated by CDS view Part2 – what objects ar...

    Jerry Wang

扫码关注云+社区

领取腾讯云代金券