前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端面试拔高题

前端面试拔高题

作者头像
李才哥
发布2019-09-04 14:45:13
8770
发布2019-09-04 14:45:13
举报
文章被收录于专栏:李才哥
代码语言:javascript
复制
如何深拷贝一个对象
代码语言:javascript
复制
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;
}
代码语言:javascript
复制
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. 序列化反序列化法

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

代码语言:javascript
复制
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 法
代码语言:javascript
复制
// 迭代递归法:深拷贝对象与数组
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 法
代码语言:javascript
复制
// 代理法
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的:

代码语言:javascript
复制
let result = _.cloneDeep(test)

结果:

lodash深拷贝结果.png

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

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

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

代码语言:javascript
复制
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

序列化反序列化法

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

代码语言:javascript
复制
// 序列化反序列化法
function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj))
}

结果:

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

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

进阶

  1. 对象成环怎么办? 我们给 test 加一个 loopObj 键,值指向自身:
代码语言:javascript
复制
test.loopObj = test

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

环对象深拷贝报错

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

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

lodash 深拷贝环对象.png

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

lodash 应对环对象办法.png

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

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

代码语言:javascript
复制
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

我们修改一下测试用例:

代码语言:javascript
复制
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 实现也可以:

代码语言:javascript
复制
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 为它的深拷贝结果,其他不变:

代码语言:javascript
复制
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 属性定义一下属性描述符:
代码语言:javascript
复制
Object.defineProperties(test, {
    'obj': {
        writable: false,
        enumerable: false,
        configurable: false
    },
    'arr': {
        get() {
            console.log('调用了get')
            return [1,2,3]
        },
        set(val) {
            console.log('调用了set')
        }
    }
})

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

代码语言:javascript
复制
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 类型键,循环引用也可以拷的深拷贝函数:
代码语言:javascript
复制
// 将之前写的 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),其内部实现原理就是将对象、数组的属性遍历一遍,赋给一个新的对象。

代码语言:javascript
复制
//自定义的复制方法
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 方法,其内部实现原来同上面是一样的。

代码语言:javascript
复制
//自定义的复制方法
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);
代码语言:javascript
复制
简单阐述一下es6中新增的原始数据类型和其作用
symbol

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

Symbol的特点

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

总结:

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

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

代码语言:javascript
复制
{
    let a = Symbol()
    let object = {
        name: 'ooo',
        age: 10,
        [a]: '隐藏属性'
    }
    window.object = object
}

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

Set

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

作用:实现数组去重

  • 不使用Set实现数组去重
代码语言:javascript
复制
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
代码语言:javascript
复制
for(let key of map.keys()){
    console.log(key)
}

.values()拿到所有的value

代码语言:javascript
复制
for(let key of map.values()){
    console.log(key)
}

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

代码语言:javascript
复制
for(let key of map.entries()){
    console.log(key)
}

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

更多资料

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

在任何时代,

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

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

没有任何一种文化模因

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

在任何时代,

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

没有一点高级。

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

耐心地拆除信息壁垒,

并且能够坚持下来,

那真不是一般的不易。

在任何时代,

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

和花半辈子看不清的人,

自然是不一样的命运。

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

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

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

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

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

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

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 李才哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 迭代递归法
    • for...in 法
      • Reflect 法
        • lodash中的深拷贝实现
          • symbol
          • Set
          • Map
      • 序列化反序列化法
      • 进阶
      • 结语
      相关产品与服务
      文件存储
      文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档