js 對象深複製,創建對象和繼承

js 對象深複製,創建對象和繼承。主要參考高級編程第三版,總結網上部分資料和自己的代碼測試心得。每走一小步,就做一個小結。

1.對象/數組深複製

  一般的=號傳遞的都是對象/數組的引用,如在控制檯輸入

var a=[1,2,3],
    b=a;
b[0]=0;
a[0]

  此時顯示的結果爲0,也就是說a和b指向的是同一個數組,只是名字不一樣罷了。

 

  單層深複製:

  1.js的slice函數:

  返回一個新的數組,包含下標從 start 到 end (不包括該元素,此參數可選)的元素。

  控制檯輸入:

var a=[1,2,3],
    b=a.slice(0);
b[0]=5;
a

  返回的a並沒有變,說明b是a的副本,修改副本對a沒有影響。

  然後輸入一下代碼:

var a=[[1,4],2,3],
    b=a.slice(0);
b[0][1]=5;
a

  可以看到a的值變了。說明slice函數只是單層複製。類似原型繼承(見下文),基本類型的屬性複製了(有自己的副本),引用類型的屬性指向了同一個引用。

 

  2.concat函數

  用於連接兩個或多個數組。不會改變現有的數組,而僅僅會返回被連接數組的一個副本。

  同樣用上面的例子測試,只是改動第二句

b=a.concat([]);

  可以看到一樣的結果。

 

  3.c=$.extend({}, {}, b)  (jquery的extend方法)(此處不完整,見最後)

  1和2兩個是百度上搜索的,自己驗證了一下。第三個是看js OOP的時候忽然想到的,jq的extend是多層深複製麼?

  首先是jquery.1.11.0的extend源碼

複製代碼
jQuery.extend = jQuery.fn.extend = function() {
    var src, copyIsArray, copy, name, options, clone,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    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)
    if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
        target = {};
    }

    // extend jQuery itself if only one argument is passed
    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
                if ( target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && jQuery.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;
};
複製代碼

  注意標紅的那一句,繼承基本對象,裏面的實現是用in遍歷屬性,很明顯如果是引用對象肯定也是複製引用了,並非深層對象的副本。我們來測試一下:

var a=[1,2,3],
    b=[2,3,4],
    d=$.extend( a, b);
d[0]=5;
a

  其實此時返回的d就是a的別名,指向同一個數組。這句話只是讓a去繼承b的屬性。於是我們可以變一下

var a=[1,2,3],
    b=[2,3,4],
    e=$.extend({}, b);

  這時候的e也就是b的一個副本了,相當於用b的屬性擴充了{},然後e指向擴充後了的{}。這樣的話,一般插件裏面用傳遞的參數覆蓋默認的參數的寫法

c=$.extend({}, a, b);

  也就不難理解了,畢竟只是改了{},再次調用插件的時候裏面的默認參數a還是沒有變滴!

  接下來是重點,用二維數組測試

var a=[[1,2],2,3],
    f=$.extend({}, a);
f[0][0]=5;
a[0]

  發現改變f[0][0],a[0][0]也跟着變了!如果extend有多個參數的時候,如

var a=[[1,2],2,3],
    b=[[2,3,4],4,6],
    g=$.extend({}, a, b);
g[0][0]=5;
b[0]

  可以發現b跟着變了,而測試a可以看到a並沒有變化。因此,這種方法寫插件參數的時候,在插件裏面對引用型參數的改變會反饋到傳入的相應參數上,小夥伴們注意咯!(不過一般貌似也不會在裏面改參數吧?)

 

  多層深複製

  1.網上摘錄的代碼,用遞歸實現層層基本類型屬性的複製。

複製代碼
function getType(o)
    {
        var _t;
        return ((_t = typeof(o)) == "object" ? o==null && "null" || Object.prototype.toString.call(o).slice(8,-1):_t).toLowerCase();
    }
    function extend(destination,source)
    {
        for(var p in source)
        {
            if(getType(source[p])=="array"||getType(source[p])=="object")
            {
                destination[p]=getType(source[p])=="array"?[]:{};
                arguments.callee(destination[p],source[p]);        //遞歸調用在這裏
            }
            else
            {
                destination[p]=source[p];
            }
        }
    }
複製代碼

  這個我在前面的AntSystem裏面用過,確實寫得簡單易懂。

 

  2.使用new操作符,以構造函數的形式實現多層深複製

  不得不承認,new是一個很神奇的操作符,雖然這樣做可能有些繁瑣。

複製代碼
function newF(){
    var a=0,
b=[5,[4,5]]; this.name="codetker"; this.a=a; this.b=b; } var temp=new newF(); temp.a=5; temp.b[1][0]=6; var temp2=new newF(); temp2.a temp2.b[1][0]
複製代碼

  可以看到temp2的a和b[1][0]都沒有被temp影響。好吧,我承認,其實這就是構造函數模式而已。管他呢,理解了,能用就行!

2.創建對象

  討厭的設計模式來了。。。說不定什麼時候能喜歡上這些呢?畢竟是前輩們的結晶。

  (摘自高級編程第三版)

  1.確定原型和實例的關係

//b是a的原型 
a instanceof b  
b.prototype.isPrototypeOf(a)

  注意construtor針對的是構造函數。

  2.工廠模式:在內部創建對象

複製代碼
function createPerson(name) {
    var o = new Object();
    o.name = name;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}
var person = createPerson('codetker');
person.sayName();
複製代碼

  缺點:無法知道對象的類型。也就是1裏面的判斷爲false

  3.構造函數模式:

複製代碼
function Person(name) {
    this.name = name;
    this.sayName = function() {
        alert(this.name);
    };
}
var person = new Person('codetker'); //能判斷類型
person.sayName();
複製代碼

  缺點:實例會擁有多餘的屬性(每個實例均新創建一次所有方法)

  4.原型模式:

複製代碼
function Person() {

}
Person.prototype.name = 'codetker'; //將屬性和方法都寫在了構造函數的原型上
Person.prototype.sayName = function() {
    alert(this.name);
};
var person = new Person(); //建立了實例和Person.prototype之間的連接(person._proto_ FF/Chrome/Safari or person.[[prototype]] in ES5)
person.sayName();
複製代碼

  在這裏面,可以用Person.prototype.isPrototypeOf(person) or Object.getPrototypeOf(person)==Person.prototype來確認是否爲原型。用hasOwnProterty()判斷對象實例屬性和原型屬性。

  in操作符可以在通過對象能夠訪問屬性時返回true,因此結合property in object與hasOwnProperty(object,property)可以判斷屬性到底是存在於對象中,還是存在於原型中。如

function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object);
}

  另外,對象的原型可以用對象字面量簡寫,如

複製代碼
Person.prototype = {
    constructor: Person, //如果想用constructor的話
    name: 'codetker',
    sayName: function() {
        alert(this.name);
    }
}; //相當於創建新對象,因此constructor不指向Person。如果在之前new一個實例,則實例取不到Person.prototype修改後的內容
複製代碼

  問題也來了,這樣相對於重寫了默認的prototype對象,因此constructor屬性也變了。如果需要constructor,可以像上面手動設置一下,不過這樣的constructor屬性就會被默認爲可枚舉的。要改成一模一樣,可以用Object.defineProperty方法。

  原型模式缺點:
  實例和構造函數沒關係,而和原型有鬆散關係。但是前面的實例可能修改了原型導致後面的實例不好受。實例應該有屬於自己的全部屬性。

  5.組合使用構造函數模式和原型模式:分開寫

複製代碼
function Person(name, age) { //每個實例都有自己的屬性
    this.name = name;
    this.age = age;
    this.friends = ['a', 'b'];
}
person.prototype = { //所有的實例共用原型的方法
    constructor: Person,
    sayName: function() {
        alert(this.name);
    }
};
var person = new Person('codetker', 21);
//一般插件的形式
複製代碼

  6.動態原型模式:將所有信息封裝在構造函數中,在構造函數中初始化原型

複製代碼
function Person(name, age) { //每個實例都有自己的屬性
    this.name = name;
    this.age = age;
    this.friends = ['a', 'b'];
    //方法
    if (typeof this.sayName != 'function') {
        Person.prototype.sayName = function() {
            alert(this.name);
        };
    }
}
var person = new Person('codetker', 21);
複製代碼

  7.寄生構造函數模式:在工廠模式的基礎之上使用new,返回的對象在構造函數和構造函數的原型屬性之間沒有任何關係

複製代碼
function createPerson(name) {
    var o = new Object();
    o.name = name;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}
var person =new createPerson('codetker');
person.sayName();
//person與Person以及Person.prototype之間沒有聯繫,不能用instanceof判斷對象類型
複製代碼

  8.穩妥構造函數模式:不使用new,不引用this,私有變量外部無法訪問,僅暴露方法

複製代碼
//應用於安全的環境中
function createPerson(name) {
    var o = new Object();

    //這兒可以定義私有變量

    o.sayName = function() {
        alert(name);
    };
    return o;
}
var person =createPerson('codetker');
person.sayName();
//僅能通過sayName()方法訪問
//person與Person以及Person.prototype之間沒有聯繫,不能用instanceof判斷對象類型
複製代碼

3.繼承

  聽起來很高大上的樣子!其實,,,還是挺高大上的。。。

  1.原型鏈繼承

複製代碼
function Super() {
    this.property = true;
}
Super.prototype.getValue = function() {
    return this.property;
};

function Sub() {
    this.sub = false;
}
//繼承,創建Super的實例,並將實例的原型賦給Sub的原型。即用Super的實例重寫了Sub的原型對象
Sub.prototype = new Super();
//原型上添加方法(一定要放在替換原型的語句之後,不然就miss)
Sub.prototype.getSub = function() {
    return this.sub;
};
//實例
var instance = new Sub();
console.log(instance.getValue());
複製代碼

  缺點:
  1.通過原型實現繼承的時候,原型實際上會變成另一個類型的實例,於是原來的實例屬性就變成了現在的原型屬性了。即第二個實例會受到第一個實例的影響
  2.沒有辦法在不影響所有對象的情況下,給超類的構造函數傳遞參數

  2.借用構造函數實現繼承(僞造對象/經典繼承)

複製代碼
function Super(name) {
    this.color = ['red', 'blue'];
    this.name = name;
    this.sayName = function() {
        console.log(this.name);
    };
}
Super.prototype.say = function() {
    console.log('not seen');
}

function Sub(name2) {
    //繼承了Super,在子類型構造函數的內部調用超類型的構造函數,從而執行了Super()中定義的初始化代碼
    Super.call(this, name2); //可以在子類型的構造函數裏面給超類傳遞參數
}

var instance = new Sub('codetker'); //實例之間不衝突,擁有自己的屬性
console.log(instance.color);
console.log(instance.name);
instance.sayName();
instance.say(); //not a function
複製代碼

  缺點:

  類似構造函數的問題,方法都在構造函數中定義,外面無法定義
  超類中原型定義的方法,對子類型都不可見

  3.組合繼承

複製代碼
function Super(name) {
    this.color = ['red', 'blue'];
    this.name = name;
    this.sayName = function() {
        console.log(this.name);
    };
}
Super.prototype.say = function() {
    console.log(this.name);
}

function Sub(name2, age) {
    //繼承屬性
    Super.call(this, name2); //調用一次
    this.age = age;
}
Sub.prototype = new Super(); //調用一次,繼承方法
Sub.prototype.constructor = Super;

var instance = new Sub('codetker', 21); //實例之間不衝突,擁有自己的屬性
instance.say(); //OK now
複製代碼

  缺點:無論什麼情況下,都會調用兩次超類型構造函數

  4.原型式繼承

複製代碼
function object(Super) { //淺複製了Super
    function F() {} //臨時性構造函數
    F.prototype = Super;
    return new F();
}
var person = {
    name: 'codetker',
    friends: ['a', 'b']
};
var person2 = object(person);
person2.name = 'code';
person2.friends.push('c');
console.log(person2.name);
console.log(person.name); //name沒變(基本類型),用於創建類似對象
console.log(person2.friends);
console.log(person.friends); //friends變了(引用類型)
複製代碼

  ES5用Object.create()方法規範化了原型繼承,只有一個參數的時候同object(),而兩個參數的時候後面的參數爲傳入的屬性,如Object.create(person,{name:{value:'TK'}});

  5.寄生式繼承

複製代碼
function object(Super) { //淺複製了Super
    function F() {} //臨時性構造函數
    F.prototype = Super;
    return new F();
}

function create(o) {
    var clone = object(o); //通過調用函數創建一個對象
    clone.sayHi = function() { //以某種方式來增強這個對象
        alert('Hi!');
    };
    return clone;
}
var person = {
    name: 'codetker',
    friends: ['a', 'b']
};
var another = create(person);
another.sayHi();
複製代碼

  6.寄生組合式繼承

複製代碼
//不必爲了指定子類型的原型而調用超類型的構造函數(YUI.lang.extend()採用寄生組合繼承)
function object(Super) { //淺複製了Super
    function F() {} //臨時性構造函數
    F.prototype = Super;
    return new F();
}

function inheritPrototype(sub, super) {
    var prototype = object(super.prototype); //創建對象,超類型原類型的副本
    prototype.constructor = sub(); //增強對象,爲副本添加constructor屬性
    sub.prototype = prototype; //指定對象,賦值
}

function Super(name) {
    this.color = ['red', 'blue'];
    this.name = name;
    this.sayName = function() {
        console.log(this.name);
    };
}
Super.prototype.say = function() {
    console.log(this.name);
}

function Sub(name2, age) {
    //繼承屬性
    Super.call(this, name2); //調用一次
    this.age = age;
}
inheritPrototype(Sub, Super);
Sub.prototype.sayAge = function() {
    alert(this.age);
};
複製代碼

  感覺內容不少,完全屬於自己的卻不多。。。不過高級編程第三版確實講得很詳細,且做分享吧~

 

-------------------------------------------------  補充  -------------------------------------------------------

  剛剛看了評論,發現自己着實太粗心了。。。看代碼看一半就自以爲是了,對不住大家哈。按照評論裏的內容,重新解讀一下源碼:

複製代碼
jQuery.extend = jQuery.fn.extend = function() {
    var src, copyIsArray, copy, name, options, clone,
        target = arguments[0] || {},               //這裏說明第一個參數其實空着也是可以的
        i = 1,
        length = arguments.length,
        deep = false;                              //多層深複製的參數在這裏

    // Handle a deep copy situation
    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)
    if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
        target = {};
    }

    // extend jQuery itself if only one argument is passed
    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
                if ( target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && jQuery.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;
};
複製代碼

  仔細看jquery.extend()方法的源碼,會看到我們這些使用者和庫創建者的差距。首先代碼的註釋已經相當完整了,然後對object,array引用類型的處理,對string等基本類型的處理,對多參數的處理,用isPlainObject()判斷是否爲純粹的對象,用isArray()判斷是否爲數組,涵蓋了所有能想到的情況,最後deep參數可選的設置,判斷要複製的屬性是否爲undefined未定義類型,無論是在設計上,代碼排版上都可圈可點,值得借鑑。山高人爲峯,學習之路漫漫~

  最後來看一下我的錯誤,注意紅色的標記,是判斷是否多層深複製的關鍵。在判斷條件中,首先是可選參數deep的設置是否爲true,然後是要複製的屬性是否存在,最後是屬性的類型是否是對象或者數組,滿足三者才能遞歸調用以實現多層深複製。再來改一下上面的例子,可見如下

  當$.extend()的agruments[0]爲true時,會實現對象的多層深複製!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章