尾遞迴實現深複製

西芹兒發表於2019-03-02

1 尾遞迴

(普通遞迴) :

function f(x) {
   if (x === 1) return 1;
   return 1 + f(x-1);
}1234複製程式碼

尾遞迴的判斷標準是函式執行【最後一步】是否呼叫自身,而不是是否在函式的【最後一行】呼叫自身。

尾遞迴

function f(x) {
   if (x === 1) return 1;
   return f(x-1);
}
複製程式碼

使用尾遞迴可以帶來一個好處:因為進入最後一步後不再需要參考外層函式(caller)的資訊,因此沒必要儲存外層函式的stack,遞迴需要用的stack只有目前這層函式的,因此避免了棧溢位風險

尾遞迴優化主要是對棧記憶體空間的優化, 這個優化是O(n)到O(1)的; 至於時間的優化, 其實是由於對空間的優化導致記憶體分配的工作減少所產生的, 是一個常數優化, 不會帶來質的變化.
尾遞迴形式和迴圈(或者說”迭代”)形式大致就是同一個邏輯的兩種表達形式而已. 經過尾遞迴優化的尾遞迴程式碼和迴圈的程式碼的執行效率基本上是相當的. 這也是函數語言程式設計效率上沒有落後的一個很重要的原因.

2、javaScript的變數型別

(1)基本型別:
5種基本資料型別Undefined、Null、Boolean、Number 和 String,變數是直接按值存放的,存放在棧記憶體中的簡單資料段,可以直接訪問。

(2)引用型別:
存放在堆記憶體中的物件,變數儲存的是一個指標,這個指標指向另一個位置。當需要訪問引用型別(如物件,陣列等)的值時,首先從棧中獲得該物件的地址指標,然後再從堆記憶體中取得所需的資料。

JavaScript儲存物件都是存地址的,所以淺拷貝會導致 obj1 和obj2 指向同一塊記憶體地址。改變了其中一方的內容,都是在原來的記憶體上做修改會導致拷貝物件和源物件都發生改變,而深拷貝是開闢一塊新的記憶體地址,將原物件的各個屬性逐個複製進去。對拷貝物件和源物件各自的操作互不影響。

例如:陣列拷貝

//淺拷貝,雙向改變,指向同一片記憶體空間
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1[0] = `change`;
console.log(`shallow copy: ` + arr1 + " );   //shallow copy: change,2,3
console.log(`shallow copy: ` + arr2 + " );   //shallow copy: change,2,3
複製程式碼

2、淺拷貝的實現

2.1、簡單的引用複製###

function shallowClone(copyObj) {
  var obj = {};
  for ( var i in copyObj) {
    obj[i] = copyObj[i];
  }
  return obj;
}
var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f);     // true
複製程式碼

2.2、Object.assign()

Object.assign() 方法可以把任意多個的源物件自身的可列舉屬性拷貝給目標物件,然後返回目標物件。

var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [ 1, 2, 3 ]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f);     // true複製程式碼

3、深拷貝的實現

3.1、Array的slice和concat方法

Array的slice和concat方法不修改原陣列,只會返回一個淺複製了原陣列中的元素的一個新陣列。之所以把它放在深拷貝里,是因為它看起來像是深拷貝。而實際上它是淺拷貝。原陣列的元素會按照下述規則拷貝:

  • 如果該元素是個物件引用 (不是實際的物件),slice 會拷貝這個物件引用到新的陣列裡。兩個物件引用都引用了同一個物件。如果被引用的物件發生改變,則新的和原來的陣列中的這個元素也會發生改變。
  • 對於字串、數字及布林值來說(不是 String、Number 或者 Boolean 物件),slice 會拷貝這些值到新的陣列裡。在別的陣列裡修改這些字串或數字或是布林值,將不會影響另一個陣列。

如果向兩個陣列任一中新增了新元素,則另一個不會受到影響。例子如下:

var array = [1,2,3]; 
var array_shallow = array; 
var array_concat = array.concat(); 
var array_slice = array.slice(0); 
console.log(array === array_shallow); //true 
console.log(array === array_slice); //false,“看起來”像深拷貝
console.log(array === array_concat); //false,“看起來”像深拷貝
複製程式碼

可以看出,concat和slice返回的不同的陣列例項,這與直接的引用複製是不同的。而從另一個例子可以看出Array的concat和slice並不是真正的深複製,陣列中的物件元素(Object,Array等)只是複製了引用。如下:

var array = [1, [1,2,3], {name:"array"}]; 
var array_concat = array.concat();
var array_slice = array.slice(0);
array_concat[1][0] = 5;  //改變array_concat中陣列元素的值 
console.log(array[1]); //[5,2,3] 
console.log(array_slice[1]); //[5,2,3] 
array_slice[2].name = "array_slice"; //改變array_slice中物件元素的值 
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice
複製程式碼

3.2、JSON物件的parse和stringify

JSON物件是ES5中引入的新的型別(支援的瀏覽器為IE8+),JSON物件parse方法可以將JSON字串反序列化成JS物件,stringify方法可以將JS物件序列化成JSON字串,藉助這兩個方法,也可以實現物件的深拷貝。

//例1
var source = { name:"source", child:{ name:"child" } } 
var target = JSON.parse(JSON.stringify(source));
target.name = "target";  //改變target的name屬性
console.log(source.name); //source 
console.log(target.name); //target
target.child.name = "target child"; //改變target的child 
console.log(source.child.name); //child 
console.log(target.child.name); //target child
//例2
var source = { name:function(){console.log(1);}, child:{ name:"child" } } 
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
//例3
var source = { name:function(){console.log(1);}, child:new RegExp("e") }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}
複製程式碼

這種方法使用較為簡單,可以滿足基本的深拷貝需求,而且能夠處理JSON格式能表示的所有資料型別,但是對於正規表示式型別、函式型別等無法進行深拷貝(而且會直接丟失相應的值)。還有一點不好的地方是它會拋棄物件的constructor。也就是深拷貝之後,不管這個物件原來的建構函式是什麼,在深拷貝之後都會變成Object。同時如果物件中存在迴圈引用的情況也無法正確處理。

4、jQuery.extend()方法原始碼實現

jQuery的原始碼 – src/core.js #L121原始碼及分析如下:

jQuery.extend = jQuery.fn.extend = function() { //給jQuery物件和jQuery原型物件都新增了extend擴充套件方法
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1,
  length = arguments.length,
  deep = false;
  //以上其中的變數:options是一個快取變數,用來快取arguments[i],name是用來接收將要被擴充套件物件的key,src改變之前target物件上每個key對應的value。
  //copy傳入物件上每個key對應的value,copyIsArray判定copy是否為一個陣列,clone深拷貝中用來臨時存物件或陣列的src。

  // 處理深拷貝的情況
  if (typeof target === "boolean") {
    deep = target;
    target = arguments[1] || {};
    //跳過布林值和目標 
    i++;
  }

  // 控制當target不是object或者function的情況
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 當引數列表長度等於i的時候,擴充套件jQuery物件自身。
  if (length === i) {
    target = this; --i;
  }
  for (; i < length; i++) {
    if ((options = arguments[i]) != null) {
      // 擴充套件基礎物件
      for (name in options) {
        src = target[name];	
        copy = options[name];

        // 防止永無止境的迴圈,這裡舉個例子,
            // 如 var a = {name : b};
            // var b = {name : a}
            // var c = $.extend(a, b);
            // console.log(c);
            // 如果沒有這個判斷變成可以無限展開的物件
            // 加上這句判斷結果是 {name: undefined}
        if (target === copy) {
          continue;
        }
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是陣列的話就讓clone副本等於src否則等於空陣列。
          } else {
            clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是物件的話就讓clone副本等於src否則等於空陣列。
          }
          // 遞迴拷貝
          target[name] = jQuery.extend(deep, clone, copy);
        } else if (copy !== undefined) {
          target[name] = copy; // 若原物件存在name屬性,則直接覆蓋掉;若不存在,則建立新的屬性。
        }
      }
    }
  }
  // 返回修改的物件
  return target;
};
複製程式碼

jQuery的extend方法使用基本的遞迴思路實現了淺拷貝和深拷貝,但是這個方法也無法處理源物件內部迴圈引用,例如:

var a = {"name":"aaa"};
var b = {"name":"bbb"};
a.child = b;
b.parent = a;
$.extend(true,{},a);//直接報了棧溢位。Uncaught RangeError: Maximum call stack size exceeded複製程式碼

5、自己動手實現一個拷貝方法

(function ($) {
    `use strict`;

    var types = `Array Object String Date RegExp Function Boolean Number Null Undefined`.split(` `);

	function type () {
	   return Object.prototype.toString.call(this).slice(8, -1);
	}

	for (var i = types.length; i--;) {
	    $[`is` + types[i]] = (function (self) {
	        return function (elem) {
	           return type.call(elem) === self;
	        };
	    })(types[i]);
	}

    return $;
})(window.$ || (window.$ = {}));//型別判斷

function copy (obj,deep) { 
    if (obj === null || (typeof obj !== "object" && !$.isFunction(obj))) { 
        return obj; 
    } 

    if ($.isFunction(obj)) {
    	return new Function("return " + obj.toString())();
    }
    else {
        var name, target = $.isArray(obj) ? [] : {}, value; 

        for (name in obj) { 
            value = obj[name]; 

            if (value === obj) {
            	continue;
            }

            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value,deep);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                } else {
            	    target[name] = value;
                } 
            } else {
            	target[name] = value;
            } 
        } 
        return target;
    }         
}複製程式碼

相關文章