欧美激情中文字幕在线_国产区精品视频_免费久久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ù)制的目標對象,后面的所有參數(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)引用怎么處理,是不是保存每個遍歷過的對象列表,每次進行對比,然后再造一個循環(huán)引用來?這樣帶來的性能消耗可以接受嗎。

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

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

這樣當(dāng)我們深拷貝obj對象時,就會循環(huán)的遍歷b屬性,直到棧溢出。
我們的解決方案為建立一個集合[],每次遍歷對象進行比較,如果[]中已存在,則證明出現(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;
}

雖然邊界條件很多,但是不同的框架和庫都對該方法進行了實現(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ù)制。例如,如果一個對象用屬性描述符標記為 read-only,它將會被復(fù)制為 read-write,因為這是默認的情況下。
    • 原形鏈上的屬性也不會被追蹤以及復(fù)制。

我們先來看看常規(guī)的深拷貝,它跟淺拷貝的區(qū)別在于,當(dāng)我們發(fā)現(xià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()

// 進行深度復(fù)制,如果第一個參數(shù)為true則深度復(fù)制,如果目標對象不合法,則拋棄并重構(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來確定進行深度復(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)
    // 如果目標對象不合法,則強行重構(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ù)進行復(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ū)填寫的 標題,內(nèi)容,圖片,目錄,標簽,這樣的
評論于留言建議
主站蜘蛛池模板: 欧美一区二区三区精美影视| 亚洲自拍另类欧美丝袜| 国产精品高潮呻吟久久av野狼| 国产精品99久久久久久久久久久久| 亚洲国产精品女人| 国产精品久久精品国产| 久久在线精品视频| www.色综合| 国产精品午夜av在线| 狠狠干视频网站| 国产精品com| 久久99精品久久久久子伦| 九九久久九九久久| 国产精品国产三级国产专播精品人| 国产精品美女久久久久av福利| 日本一区二区黄色| 久久亚洲国产精品| 国产精品久久久久av免费| 亚洲欧洲久久| 精品国产一区av| 国产suv精品一区二区| 亚洲欧美久久234| 精品日韩欧美| 午夜精品一区二区在线观看| 久久天天躁狠狠躁夜夜躁2014| 国产美女久久久| 婷婷久久五月天| 国产噜噜噜噜噜久久久久久久久| 尤物国产精品| 久久99亚洲精品| 国产精品久久久久91| 日韩在线视频观看| 高清国产一区| 激情小说综合区| 欧美激情一级精品国产| 欧美精品尤物在线| 国产亚洲精品自在久久| 久久福利视频网| 国产精品久久精品国产| 色妞一区二区三区| 日本高清视频一区二区三区|