手撕-JS Clone

713 阅读3分钟

突然发现有两年没写过东西了,最近准备温习一下js基础以及一些比较实用的技术点。本想以小册的形式发布,不定时更新一个知识点,奈何等级不够,就先一篇一篇写吧,看之后能不能收到一个专栏里。

好了,废话不多说了,上正餐。

js Clone分为浅拷贝和深拷贝,通俗来说,浅拷贝就是数据结构首层不会指向同一个引用,深拷贝就是数据结构所有层级都互不影响。

浅拷贝

1. 万能的for循环
let a = {
    id: 1, 
    name: 'Madman', 
    say: (str) => {
        console.log(str)
    },
    other: {
        like: ['music', 'code']
    }
}

Object.prototype._shallowClone = function() {
    let tmp = {}
    for (let k in this) {
        tmp[k] = this[k]
    }
    return tmp
}

let a_copy = a._deepClone()
2. 扩展运算符 ...

可以简单理解为把对象a首层展开放到一个新对象里

let a_copy = { ...a }
3. Object.assign()

此方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。MDN

let a_copy = Object.assign({}, a)

深拷贝

1. JSON
Object.prototype._deepClone = function() {
    try {
        return JSON.parse(JSON.stringify(this))
    } catch(err) {
        // 处理一些异常
        return null
    }
}

这里说一下为什么代码里加了try...catch...

这种方式不能处理undefinedSymbol、不能序列化Function、不能解决循环引用、不能处理非JSON格式的类型。当用此方法处理了循环引用的对象的时候,还会抛出一个异常。

2. 递归处理

完美的方案肯定是递归数据结构的所有层级去处理每一个属性:

function _deepClone(source) {
    if (typeof source !== 'object') return source
    if (source === null) return null
    
    let tmp = source._isType('Array') ? [] : {} 
    // 亦可用source.constructor === Array ? [] : {} 来处理
    
    Object.keys(source).forEach(k => {
        tmp[k] = (source[k] && typeof source[k] === 'object') ? _deepClone(source[k]) : source[k]
    })
    
    return tmp
}

// 检测类型
Object.prototype._isType = function(type) {
    return Object.prototype.toString.call(this) === `[object ${type}]`
}

以上只是简单处理,一个完美的deepClone除了要处理以上的逻辑,还会处理一些date、buffer、reg等等,建议大家阅读lodash cloneDeep

--------------手动华丽分割线(掘金大大考虑编辑器加个分割线?)-------------

3. MessageChannel

MessageChanel MDN,关于这个是啥,请大家移步到此MDN链接阅读,这里就不再具体阐述概念了。

简单理解就是MessageChannel创建一个消息通道,有两个只读的port的端口,从一个端口postMessage发送的消息,可以在另一个端口使用onmessage监听到,这里传出的data是个深拷贝。

function _deepClone(source) {
    return new Promise((resolve, reject) => {
        const { port1, port2 } = new MessageChannel()
        port1.onmessage = ({ data }) => resolve(data)
        port2.postMessage(source)
    })
}

let a_clone

_deepClone(a).then(res => {
    a_clone = res
}).catch(err => {
    // do some thing
})

可以看到上述代码中含有catch,有人会说Promise好的代码习惯就应该写catch,小伙子,你说的很对!

注意:这里单独提一下主要是这种方式不能处理函数,也处理不了Symbol,比如定义的a中其实有一个say()函数,此处就会报错,如果Symbol类型,也会报错,这里可以通过catch捕捉一下失败的情况。

话外篇

文中有提到postMessage相关应用,其实目前来看这个技术已经应用于很多场景,比如:

同一公司主体下,两个不同域名之间跳转登录态的问题,可以使用iframe+postMessage做授权登录;

微信环境使用微信支付,可能会碰到微信点金计划中商户小票页面,这里的交互就是唤起微信支付后,会跳转微信默认支付结果页面,微信官方称之为“官方小票”页面,各商户如果需要实现定制化信息,可以提供自己的“商户小票”页面,而商户小票页面和微信之间的一些通信也是用的postMessage

所以这样看来,此技术已经被广泛应用于各个场景,各位砖友们还是需要紧跟技术的潮流,以防被拍在沙滩上。

我是代码搬运工,疯子,大家有什么想一起温习的技术点欢迎在评论区留言~