欧美激情中文字幕在线_国产区精品视频_免费久久99精品国产自_日韩精品 欧美

JavaScript的深拷貝和淺拷貝

來源:https://segmentfault.com/a/1190000017469386

一直想梳理下工作中經(jīng)常會用到的深拷貝的內(nèi)容,然而遍覽了許多的文章,卻發(fā)現(xiàn)對深拷貝并沒有一個通用的完美實現(xiàn)方式。因為對深拷貝的定義不同,實現(xiàn)時的edge case過多,在深拷貝的時候會出現(xiàn)循環(huán)引用等問題,導(dǎo)致JS內(nèi)部并沒有實現(xiàn)深拷貝,但是我們可以來探究一下深拷貝到底有多復(fù)雜,各種實現(xiàn)方式的優(yōu)缺點,同時參考下常用庫對其的實現(xiàn)。

引用類型

之所以會出現(xiàn)深淺拷貝的問題,實質(zhì)上是由于JS對基本類型和引用類型的處理不同。基本類型指的是簡單的數(shù)據(jù)段,而引用類型指的是一個對象,而JS不允許我們直接操作內(nèi)存中的地址,也就是不能操作對象的內(nèi)存空間,所以,我們對對象的操作都只是在操作它的引用而已。

在復(fù)制時也是一樣,如果我們復(fù)制一個基本類型的值時,會創(chuàng)建一個新值,并把它保存在新的變量的位置上。而如果我們復(fù)制一個引用類型時,同樣會把變量中的值復(fù)制一份放到新的變量空間里,但此時復(fù)制的東西并不是對象本身,而是指向該對象的指針。所以我們復(fù)制引用類型后,兩個變量其實指向同一個對象,改變其中一個對象,會影響到另外一個。

var num = 10;
var obj = {
    name: 'Nicholas'
}

var num2 = num;
var obj2 = obj;

obj.name = 'Lee';
obj2.name; // 'Lee'

 

淺拷貝

如果我們要復(fù)制對象的所有屬性都不是引用類型時,就可以使用淺拷貝,實現(xiàn)方式就是遍歷并復(fù)制,最后返回新的對象。

function shallowCopy(obj) {
    var copy = {};
    // 只復(fù)制可遍歷的屬性
    for (key in obj) {
        // 只復(fù)制本身擁有的屬性
        if (obj.hasOwnProperty(key)) {
            copy[key] = obj[key];
        }
    }
    return copy;
}

如上面所說,我們使用淺拷貝會復(fù)制所有引用對象的指針,而不是具體的值,所以使用時一定要明確自己的需求,同時,淺拷貝的實現(xiàn)也是最簡單的。

JS內(nèi)部實現(xiàn)了淺拷貝,如Object.assign(),其中第一個參數(shù)是我們最終復(fù)制的目標(biāo)對象,后面的所有參數(shù)是我們的即將復(fù)制的源對象,支持對象或數(shù)組,一般調(diào)用的方式為

var newObj = Object.assign({}, originObj);

深拷貝

如果我們需要復(fù)制一個擁有所有屬性和方法的新對象,就要用到深拷貝,JS并沒有內(nèi)置深拷貝方法,主要是因為:

  1. 深拷貝怎么定義?我們怎么處理原型?怎么區(qū)分可拷貝的對象?原生DOM/BOM對象怎么拷貝?函數(shù)是新建還是引用?這些edge case太多導(dǎo)致我們無法統(tǒng)一概念,造出大家都滿意的深拷貝方法來。
  2. 內(nèi)部循環(huán)引用怎么處理,是不是保存每個遍歷過的對象列表,每次進(jìn)行對比,然后再造一個循環(huán)引用來?這樣帶來的性能消耗可以接受嗎。

解釋一些常見的問題概念,防止有些同學(xué)不明白我們在講什么。比如循環(huán)引用:

var obj = {};
obj.b = obj;

這樣當(dāng)我們深拷貝obj對象時,就會循環(huán)的遍歷b屬性,直到棧溢出。
我們的解決方案為建立一個集合[],每次遍歷對象進(jìn)行比較,如果[]中已存在,則證明出現(xiàn)了循環(huán)引用或者相同引用,我們直接返回該對象已復(fù)制的引用即可:

let hasObj = [];
function referCopy(obj) {
    let copy = {};
    hasObj.push(obj);
    for (let i in obj) {
        if (typeof obj[i] === 'object') {
            let index = hasObj.indexOf(obj[i]);
            if (index > -1) {
                console.log('存在循環(huán)引用或?qū)傩砸昧讼嗤瑢ο?);
                // 如果已存在,證明引用了相同對象,那么無論是循環(huán)引用還是重復(fù)引用,我們返回引用就可以了
                copy[i] = hasObj[index];
            } else {
                copy[i] = referCopy(obj[i]);
            }
        } else {
            copy[i] = obj[i];
        }
    }
    return copy;
}

處理原型和區(qū)分可拷貝的對象:我們一般使用function.prototype指代原型,使用obj.__proto__指代原型鏈,使用enumerable屬性表示是否可以被for ... in等遍歷,使用hasOwnProperty來查詢是否是本身元素。在原型鏈和可遍歷屬性和自身屬性之間存在交集,但都不相等,我們應(yīng)該如何判斷哪些屬性應(yīng)該被復(fù)制呢?

函數(shù)的處理:函數(shù)擁有一些內(nèi)在屬性,但我們一般不修改這些屬性,所以函數(shù)一般直接引用其地址即可。但是擁有一些存取器屬性的函數(shù)我們怎么處理?是復(fù)制值還是復(fù)制存取描述符?

var obj = {
    age: 10,
    get age() {
        return this.age;
    },
    set age(age) {
        this.age = age;
    }
};
var obj2 = $.extend(true, {}, obj);

obj2; // {age: 10}

這個是我們想要的結(jié)果嗎?大部分場景下不是吧,比如我要復(fù)制一個已有的Vue對象。當(dāng)然我們也有解決方案:

function copy(obj) {
    var copy = {};
    for (var i in obj) {
        let desc = Object.getOwnPropertyDescriptor(obj, i);
        // 檢測是否為存取描述符
        if (desc.set || desc.get) {
            Object.defineProperty(copy, i, {
                get: desc.get,
                set: desc.set,
                configuarable: desc.configuarable,
                enumerable: true
            });
        // 否則為數(shù)據(jù)描述符,則復(fù)用下面的深拷貝方法,此處簡寫
        } else {
            copy[i] = obj[i];
        }
    }
    return copy;
}

雖然邊界條件很多,但是不同的框架和庫都對該方法進(jìn)行了實現(xiàn),只不過定義不同,實現(xiàn)方式也不同,如jQuery.extend()只復(fù)制可枚舉的屬性,不繼承原型鏈,函數(shù)復(fù)制引用,內(nèi)部循環(huán)引用不處理。而lodash實現(xiàn)的就更為優(yōu)秀,它實現(xiàn)了結(jié)構(gòu)化克隆算法
該算法的優(yōu)點是:

  1. 可以復(fù)制 RegExp 對象。
  2. 可以復(fù)制 Blob、File 以及 FileList 對象。
  3. 可以復(fù)制 ImageData 對象。CanvasPixelArray 的克隆粒度將會跟原始對象相同,并且復(fù)制出來相同的像素數(shù)據(jù)。
  4. 可以正確的復(fù)制有循環(huán)引用的對象

依然存在的缺陷是:

  1. Error 以及 Function 對象是不能被結(jié)構(gòu)化克隆算法復(fù)制的;如果你嘗試這樣子去做,這會導(dǎo)致拋出 DATA_CLONE_ERR 的異常。
  2. 企圖去克隆 DOM 節(jié)點同樣會拋出 DATA_CLONE_ERROR 異常。
  3. 對象的某些特定參數(shù)也不會被保留
    • RegExp 對象的 lastIndex 字段不會被保留
    • 屬性描述符,setters 以及 getters(以及其他類似元數(shù)據(jù)的功能)同樣不會被復(fù)制。例如,如果一個對象用屬性描述符標(biāo)記為 read-only,它將會被復(fù)制為 read-write,因為這是默認(rèn)的情況下。
    • 原形鏈上的屬性也不會被追蹤以及復(fù)制。

我們先來看看常規(guī)的深拷貝,它跟淺拷貝的區(qū)別在于,當(dāng)我們發(fā)現(xiàn)對象的屬性是引用類型時,進(jìn)行遞歸遍歷復(fù)制,直到遍歷完所有屬性:

var deepClone = function(currobj){
    if(typeof currobj !== 'object'){
        return currobj;
    }
    if(currobj instanceof Array){
        var newobj = [];
    }else{
        var newobj = {}
    }
    for(var key in currobj){
        if(typeof currobj[key] !== 'object'){
            // 不是引用類型,則復(fù)制值
            newobj[key] = currobj[key];
        }else{
            // 引用類型,則遞歸遍歷復(fù)制對象
            newobj[key] = deepClone(currobj[key])    
        }
    }
    return newobj
}

這個的主要問題就是不處理循環(huán)引用,不處理對象原型,函數(shù)依然是引用類型。上面描述過的復(fù)雜問題依然存在,可以說是最簡陋但是日常工作夠用的深拷貝方式。

另外還有一種方式是使用JSON序列化,巧妙但是限制更多:

// 調(diào)用JSON內(nèi)置方法先序列化為字符串再解析還原成對象
newObj = JSON.parse(JSON.stringify(obj));

JSON是一種表示結(jié)構(gòu)化數(shù)據(jù)的格式,只支持簡單值、對象和數(shù)組三種類型,不支持變量、函數(shù)或?qū)ο髮嵗K晕覀児ぷ髦锌梢允褂盟鉀Q常見問題,但也要注意其短板:函數(shù)會丟失,原型鏈會丟失,以及上面說到的所有缺陷。

庫實現(xiàn)

上面的兩種方式可以滿足大部分場景的需求,如果有更復(fù)雜的需求,可以自己實現(xiàn)。現(xiàn)在我們可以看一些框架和庫的解決方案,下面拿經(jīng)典的jQuery和lodash的源碼看下,它們的優(yōu)缺點上面都說過了:

jQuery.extend()

// 進(jìn)行深度復(fù)制,如果第一個參數(shù)為true則深度復(fù)制,如果目標(biāo)對象不合法,則拋棄并重構(gòu)為{}空對象,如果只有一個參數(shù)則功能為擴展jQuery對象
jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[ 0 ] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    // 第一個參數(shù)可以為true來確定進(jìn)行深度復(fù)制
    if ( typeof target === "boolean" ) {
        deep = target;

        // Skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    // 如果目標(biāo)對象不合法,則強行重構(gòu)為{}空對象,拋棄原有的
    if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
        target = {};
    }

    // Extend jQuery itself if only one argument is passed
    // 如果只有一個參數(shù),擴展jQuery對象
    if ( i === length ) {
        target = this;
        i--;
    }

    for ( ; i < length; i++ ) {

        // Only deal with non-null/undefined values
        // 只處理有值的對象
        if ( ( options = arguments[ i ] ) != null ) {

            // Extend the base object
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                // Prevent never-ending loop
                // 阻止最簡單形式的循環(huán)引用
                // var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 就會形成復(fù)制的對象循環(huán)引用obj
                if ( target === copy ) {
                    continue;
                }
                // 如果為深度復(fù)制,則新建[]和{}空數(shù)組或空對象,遞歸本函數(shù)進(jìn)行復(fù)制
                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                    ( copyIsArray = Array.isArray( copy ) ) ) ) {

                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && Array.isArray( src ) ? src : [];

                    } else {
                        clone = src && jQuery.isPlainObject( src ) ? src : {};
                    }

                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified object
    return target;
};

lodash _.baseClone()

/**
     * The base implementation of `_.clone` and `_.cloneDeep` which tracks
     * traversed objects.
     *
     * @private
     * @param {*} value The value to clone.
     * @param {boolean} bitmask The bitmask flags.
     *  1 - Deep clone
     *  2 - Flatten inherited properties
     *  4 - Clone symbols
     * @param {Function} [customizer] The function to customize cloning.
     * @param {string} [key] The key of `value`.
     * @param {Object} [object] The parent object of `value`.
     * @param {Object} [stack] Tracks traversed objects and their clone counterparts.
     * @returns {*} Returns the cloned value.
     */
    function baseClone(value, bitmask, customizer, key, object, stack) {
      var result,
          isDeep = bitmask & CLONE_DEEP_FLAG,
          isFlat = bitmask & CLONE_FLAT_FLAG,
          isFull = bitmask & CLONE_SYMBOLS_FLAG;

      if (customizer) {
        result = object ? customizer(value, key, object, stack) : customizer(value);
      }
      if (result !== undefined) {
        return result;
      }
      if (!isObject(value)) {
        return value;
      }
      var isArr = isArray(value);
      if (isArr) {
        result = initCloneArray(value);
        if (!isDeep) {
          return copyArray(value, result);
        }
      } else {
        var tag = getTag(value),
            isFunc = tag == funcTag || tag == genTag;

        if (isBuffer(value)) {
          return cloneBuffer(value, isDeep);
        }
        if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
          result = (isFlat || isFunc) ? {} : initCloneObject(value);
          if (!isDeep) {
            return isFlat
              ? copySymbolsIn(value, baseAssignIn(result, value))
              : copySymbols(value, baseAssign(result, value));
          }
        } else {
          if (!cloneableTags[tag]) {
            return object ? value : {};
          }
          result = initCloneByTag(value, tag, baseClone, isDeep);
        }
      }
      // Check for circular references and return its corresponding clone.
      stack || (stack = new Stack);
      var stacked = stack.get(value);
      if (stacked) {
        return stacked;
      }
      stack.set(value, result);

      var keysFunc = isFull
        ? (isFlat ? getAllKeysIn : getAllKeys)
        : (isFlat ? keysIn : keys);

      var props = isArr ? undefined : keysFunc(value);
      arrayEach(props || value, function(subValue, key) {
        if (props) {
          key = subValue;
          subValue = value[key];
        }
        // Recursively populate clone (susceptible to call stack limits).
        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
      });
      return result;
    }

參考資料

  1. 知乎 JS的深拷貝和淺拷貝:?https://www.zhihu.com/questio...
  2. Javascript之深拷貝:?https://aepkill.github.io/201...
  3. js對象克隆之謎:http://b-sirius.me/2017/08/26...
  4. 知乎 JS如何完整實現(xiàn)深度Clone對象:https://www.zhihu.com/questio...
  5. github lodash源碼:https://github.com/lodash/lod...
  6. MDN 結(jié)構(gòu)化克隆算法:https://developer.mozilla.org...
  7. jQuery v3.2.1 源碼
  8. JavaScript高級程序設(shè)計 第4章(變量、作用域和內(nèi)存問題)、第20章(JSON)

 

上一篇 使用JS為網(wǎng)頁制作菜單
下一篇 CSS3 Flex 布局詳解文檔
目錄
applek

applek管理員

個人說明在個人中心里面設(shè)置

本月創(chuàng)作熱力圖

最新評論
小超
小超
7月9日
下個版本考慮將下載地址加密(防止采集)
Lvtu
Lvtu
7月4日
qiang厲害
評論于留言建議
chinacnd
chinacnd
6月26日
這個主題是怎么做到加載速度那么快的?
風(fēng)起云涌
風(fēng)起云涌
6月17日
前臺投稿現(xiàn)在有點太抽象了 希望可以弄成那種分區(qū)填寫的 標(biāo)題,內(nèi)容,圖片,目錄,標(biāo)簽,這樣的
評論于留言建議
主站蜘蛛池模板: 国产在线播放不卡| 欧美日韩电影在线观看| 欧美 日韩 国产 激情| 国产精品久久久久久婷婷天堂| 久久香蕉国产线看观看av| 日韩国产一区久久| 亚洲国产欧美一区二区三区不卡 | 日韩一区二区三区在线播放| 国产精品精品视频| 91精品国产自产91精品| 真实国产乱子伦对白视频| 亚洲欧洲精品一区| 男女视频一区二区三区| 国产精品视频在线免费观看| 国产va免费精品高清在线观看| 99久久99| 久久久国产一区| 97精品欧美一区二区三区| 日本成人中文字幕在线| 欧美亚洲激情在线| 国产女精品视频网站免费| 国产日产久久高清欧美一区| 99热在线国产| 精品视频导航| 日本欧洲国产一区二区| 国产精品综合不卡av| 日韩欧美视频第二区| 国产精品亚洲a| 亚洲精品tv久久久久久久久| 日本三级韩国三级久久| 国产精品国产精品国产专区不卡 | 久久精品国产91精品亚洲| 国产精品国产三级国产专播精品人 | 奇米精品一区二区三区| 国产麻豆一区二区三区在线观看| 午夜精品一区二区三区在线播放| 精品国产美女在线| 日本午夜精品电影| 日韩一级片免费视频| 中文字幕免费在线不卡 | 国产精品视频二|