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時,會實現對象的多層深復制!
最后謝謝@笨雷雷的評論,幫我找出了自己的粗心壞毛病。歡迎大家對我的部落格提出問題,一起分享,共同進步~
