看了不少js繼承的東西也該總結總結了。
先說一下大概的理解,有不對的還望指正,也好更正一下三觀。另外說明下,下面的例子並非原創基本就是改了個變數名啥的,有的甚至直接拿過來用的。
js繼承是用來幹啥的:
首先說js沒有真正的跟其他物件導向的語言一樣概念的繼承,js裡邊所說的繼承是指類比繼承。
具體js繼承是幹啥呢,剛開始做前端的時候我是用來面試的(最早寫些小效果的時候基本用不到,為啥要看呢,因為面試官很愛問這個問題啊),所以就看看大概的,面試時候能說個大概,在這個問題上算是面試黨了。後來跟著慢慢的實際上雖然概念不是很明確也用到一些。
真正是用來幹啥的呢,主要是用來複用我們之前寫過的代碼。比如寫過一個功能,一個對象,或者用別人寫的東西,我們要自己加點兒什麼,總不能改人家的東西吧,直接繼承過來用一下就好了,這才是繼承的真正用途。
js繼承怎麼實現:
先不上代碼,先說說想法。其實繼承呢就是想辦法把其他對象(js裡邊一切皆對象哈)的屬性或者方法搞到我們自己的對象上,讓我們自己的這個對象可以用。這也就達到複用的目的了。
目的搞明白了,下面就是實現手段了。
根據js的特性,實現無非就是以下幾種方法的其中一種或者組合使用。
1、建構函式,js好像沒有嚴格的建構函式的定義,但是可以用new來建立新的對象。建構函式據說也是嚴格的物件導向的語言實現繼承的方法,那麼js當然可以類比一下了,所以學過oop語言的人們會最先想到這個。
2、利用函數原型,利用原型鏈把兩個對象連結起來,因為js原型鏈是比較獨特所以想到這個也是很容易的。
原型也分幾種,就是把什麼作為繼承對象的原型,被繼承對象的原型或者被繼承對象的執行個體,或者直接被繼承者。這幾種作為繼承對象的原型得到的繼承效果是不一樣的。
3、複製屬性和方法,把被繼承對象的屬性或者方法統統複製複製過來變成我們自己對象的屬性和方法就好了啊,我們就可以無比順暢的用了。當然這個還分淺複製和深度複製兩種情況。
4、利用call和apply這兩個方法,這兩個方法比較神奇,可以改變函數執行的上下文(this),所以利用這兩個方法也可以實現對被繼承對象的方法的繼承複用。
總的來js實現繼承的途徑大概就是這些,千變萬化的實現方法都是從這幾種方法的基礎上組合升級完善出來的,為毛大多數要組合使用呢,當然是因為使用單個方法實現的效果不太理想啊。當然可以根據自己項目中實際的需求選擇使用哪種方式,只要滿足自己的需求並沒有說必須使用哪種方法去實現。就像說從北京去石家莊,最快當然是飛機啦。但是如果離機場遠,算上到機場,從機場去市裡,整體算下來還不如高鐵快,那就可以做高鐵。又比如自己有車可以開車,想要挑戰一下還可以騎單車,這個根據自己的實際情況來選就可以。
代碼實現,下面結合代碼說說上面的幾種實現方法,有些是從其他地方摘來的,加點兒注釋啥的。
看了不少js繼承的東西也該總結總結了。
先說一下大概的理解,有不對的還望指正,也好更正一下三觀。另外說明下,下面的例子並非原創基本就是改了個變數名啥的,有的甚至直接拿過來用的。
js繼承是用來幹啥的:
首先說js沒有真正的跟其他物件導向的語言一樣概念的繼承,js裡邊所說的繼承是指類比繼承。
具體js繼承是幹啥呢,剛開始做前端的時候我是用來面試的(最早寫些小效果的時候基本用不到,為啥要看呢,因為面試官很愛問這個問題啊),所以就看看大概的,面試時候能說個大概,在這個問題上算是面試黨了。後來跟著慢慢的實際上雖然概念不是很明確也用到一些。
真正是用來幹啥的呢,主要是用來複用我們之前寫過的代碼。比如寫過一個功能,一個對象,或者用別人寫的東西,我們要自己加點兒什麼,總不能改人家的東西吧,直接繼承過來用一下就好了,這才是繼承的真正用途。
js繼承怎麼實現:
先不上代碼,先說說想法。其實繼承呢就是想辦法把其他對象(js裡邊一切皆對象哈)的屬性或者方法搞到我們自己的對象上,讓我們自己的這個對象可以用。這也就達到複用的目的了。
目的搞明白了,下面就是實現手段了。
根據js的特性,實現無非就是以下幾種方法的其中一種或者組合使用。
1、建構函式,js好像沒有嚴格的建構函式的定義,但是可以用new來建立新的對象。建構函式據說也是嚴格的物件導向的語言實現繼承的方法,那麼js當然可以類比一下了,所以學過oop語言的人們會最先想到這個。
2、利用函數原型,利用原型鏈把兩個對象連結起來,因為js原型鏈是比較獨特所以想到這個也是很容易的。
原型也分幾種,就是把什麼作為繼承對象的原型,被繼承對象的原型或者被繼承對象的執行個體,或者直接被繼承者。這幾種作為繼承對象的原型得到的繼承效果是不一樣的。
3、複製屬性和方法,把被繼承對象的屬性或者方法統統複製複製過來變成我們自己對象的屬性和方法就好了啊,我們就可以無比順暢的用了。當然這個還分淺複製和深度複製兩種情況。
4、利用call和apply這兩個方法,這兩個方法比較神奇,可以改變函數執行的上下文(this),所以利用這兩個方法也可以實現對被繼承對象的方法的繼承複用。
總的來js實現繼承的途徑大概就是這些,千變萬化的實現方法都是從這幾種方法的基礎上組合升級完善出來的,為毛大多數要組合使用呢,當然是因為使用單個方法實現的效果不太理想啊。當然可以根據自己項目中實際的需求選擇使用哪種方式,只要滿足自己的需求並沒有說必須使用哪種方法去實現。就像說從北京去石家莊,最快當然是飛機啦。但是如果離機場遠,算上到機場,從機場去市裡,整體算下來還不如高鐵快,那就可以做高鐵。又比如自己有車可以開車,想要挑戰一下還可以騎單車,這個根據自己的實際情況來選就可以。
代碼實現,下面結合代碼說說上面的幾種實現方法,有些是從其他地方摘來的,加點兒注釋啥的。
一、建構函式實現(借用建構函式):
function Super(arg){ this.arr1 = "I'm super "+arg; this.show = function(){ alert(this.arr1); } } Super.prototype.say = function(){ alert(this.arr1); } function suber(arg){ Super.apply(this, arguments); //在suber的上下文中運行super } var sub =new suber("suber"); var sub2 = new suber("suber1"); console.log(sub.arr1); //I'm super suber console.log(sub.show); //function (){ alert(this.arr1);} console.log(sub.say); //undefined console.log(sub.show === sub2.show); //false
哎呀,發現sub.say是undefined,這說明它沒有被繼承過來啊,下邊兩個對象sub,sub2的show不相等,說明兩個函數指向了兩個不同的對象,也就是說被複製了兩份出來。
所以這個方法實現繼承的話原型對象上的屬性和方法沒有被繼承過來,Super上的屬性和方法為每個new出來的對象分別複製一份。
所以單單使用這個方法來實現繼承還是不妥啊,因為原型上的方法都沒有被繼承過來啊。於是大神們就想到了原型繼承
二、原型繼承:
function Super(arg){ this.arr1 = "I'm super "+arg; this.show = function(){ alert(this.arr1); } } Super.prototype.say = function(){ alert(this.arr1); } function suber(arg){} suber.prototype = new Super(); var sub = new suber("suber1"); var sub2 = new suber("suber2"); console.log(sub.arr1); //I'm super undefined console.log(sub.show); //function (){ alert(this.arr1);} console.log(sub.say); //function (){ alert(this.arr1);} console.log(sub.show === sub2.show); //true; console.log(sub.say === sub2.say); //true;
這次是arr1繼承過來了,但是參數沒有添加進來,是undefined,所以這個方法子類聲明時候這個參數傳進來付類繼承過來的這個屬性沒法收到。其他的都還算正常。show和say都繼承過來了。但是有一點兒需要注意,say是通過super的原型對象繼承過來的,而show是建立super對象執行個體時執行個體的屬性。
那麼怎麼實現參數傳輸又能把原型裡邊的東東繼承過來呢,當然上邊兩種方法組合一下就好了啊,於是前輩們又發明了下面這種方法
三、組合繼承(借用建構函式並設定原型):
function Super(arg){ this.arr1 = "I'm super "+arg; this.show = function(){ alert(this.arr1); } } Super.prototype.say = function(){ alert(this.arr1); } function suber(arg){ Super.apply(this, arguments); } suber.prototype = new Super(); var sub = new suber("suber1"); var sub2 = new suber("suber2"); console.log(sub.arr1); //I'm super suber1 console.log(sub.show); //function (){ alert(this.arr1);} console.log(sub.say); //function (){ alert(this.arr1);} console.log(sub.show === sub2.show); //false; console.log(sub.say === sub2.say); //true;
這次幾乎完美了,但是可以發現sub.show 和sub2.show並不相等啊,這是為啥呢,因為apply這個地方使得show成為了suber的自己的屬性,那麼就吧suber原型裡的show(Super的當做suber原型對象的執行個體對象的show)給覆蓋了,所以又變成每次複製一個,當然這個地方沒有辦法避免啊。為了不產生這種多餘的開消這種可以共用的函數可以多放到原型對象裡邊。
因為suber的構造裡邊的調用,和給suber原型對象賦值時候的調用導致Super被調用了兩次,那麼每次new suber對象時候就調用了兩次Super,調用兩次就會產生兩個執行個體對象,需要消耗多餘的資源了。
於是前輩們為瞭解決這個問題又開了開腦洞,開發出來下面這種方法。
四、寄生組合繼承:
該方法跟方法三最主要的不同就是把父類原型賦給了子類原型而不是父類樣本,看例子
function Super(arg){ this.arr1 = "I'm super "+arg; }Super.prototype.show = function(){ //這個方法放到了原型對象上。 alert(this.arr1); }Super.prototype.say = function(){ alert(this.arr1);}function suber(arg){ Super.apply(this, arguments);}/*inherit函數的作用,使用一個新的空函數,來切斷父類對象的原型對象與子類原型對象的直接聯絡,而是通過這個空構造的執行個體對象實現繼承,這樣可以避免更改子類原型的屬性或者方法而影響了父類原型對象的屬性或者方法。*/function inherit(obj){ function F(){} F.prototype = obj; return new F(); }suber.prototype = inherit(Super.prototype);var sub = new suber("suber1");var sub2 = new suber("suber2");console.log(sub.arr1); //I'm super suber1console.log(sub.show); //function (){ alert(this.arr1);}console.log(sub.say); //function (){ alert(this.arr1);}console.log(sub.show === sub2.show); //true;console.log(sub.say === sub2.say); //true;
好了,這樣就把三方法的弊端幹掉了,這個可以完美的使用了吧。
五、複製屬性實現
拷貝我們可以寫一個拷貝函數來實現。
function extend(Super,suber){ suber = suber || {}; for(var i in Super){ if(Super.hasOwnProperty(i)){ suber[i] = Super[i]; } } return suber;}var parent = { name:"dad", num:[1,2,3], say:function(){alert("dad");}}var child = { age:5, sex:"boy"};child = extend(parent, child);//以下測試console.log(child); /*{ age:5, sex:"boy", name:"dad", num:[1,2,3], say:function(){alert("dad");} }*/console.log(child.say === parent.say); //trueconsole.log(child.num === parent.num); //true
複製成功,那麼child成功繼承了parent的一些屬性,但是後面兩個測試發現他們是相等的,就表明了他們在公用同一個數組,同一個函數,函數這個可以,但是數組這個就有問題了,如果一個chiild改變了數組,幾個被繼承對象的數組也跟著變了,這就不給力了啊。
為什麼會發生這種情況呢,js裡邊Object Storage Service的是指標,然後這個指標指向這個值,我們在這複製的實際是指向該對象的指標的值,所以繼承對象和被繼承對象都指向了同一個對象,接下來看看如何使用深度複製來解決這個問題。
深度複製對象屬性:
function extenddeep(Super, suber){ var tostr = Object.prototype.toString, astr = "[object Array]"; suber = suber || {}; for(var i in Super){ if(Super.hasOwnProperty(i)){ if(typeof Super[i] === "object"){ suber[i] = (tostr.call(Super[i]) == astr) ? [] : {}; extenddeep(Super[i],suber[i]); }else { suber[i] = Super[i]; } } } return suber; } var parent = { name:"papa", num:[1,2,3], say:function(){alert("I'm father of my son!");} } var child = { name:"jone", sex:"boy", } var kid = extenddeep(parent, child); console.log(kid); // {name: "papa" num: Array[3] say: () sex: "boy" // } console.log(kid.say === parent.say); //true console.log(kid.num === parent.num); //false console.log(kid.name); //papa
好了,深度複製完畢,但似有木有發現問題,name是parent的,也就是說如果繼承對象有和被繼承對象一樣的屬性名稱的屬性如果不做處理就會被替換掉。那麼我們可以做一下處理,先聲明一個屬性,儲存parent裡的東西,剩下的的當然就是child自己的東西了,最後再把屬性給child對象就可以了。
六、利用call和apply這兩個方法(借用方法)。
這個就是通過call和apply來複用其他對象的方法,達到複用的目的。
var one = { name:"object", say: function(greet){ return greet + ', ' + this.name; } } var tow = { name:"two" } one.say.call(tow, "hi"); //hi, two
這個就是借用了,好了,下課。
好吧,好吧,其實這裡邊還有其他東西要看。可以借用並不“帶表”可以隨便把某個方法賦值給誰然後跟沒發生什麼似的繼續用。所以我們平時使用借用時要注意一下上下文,下面看下那些容易出錯的地方。
//賦值給一個變數時候上下文會變化 var say = one.say; console.log(say('hoho')); // "hoho, undefined" //作為回呼函數時也會發生變化 var yetother = { name:"yetother obj", method:function(callback){ return callback("Hola"); } } console.log(yetother.method(one.say)); //"Hola, "
神馬意思呢,就是this.name是undefined,當one.say賦值給say是,實際上是say儲存了指向函數對象的指標,say這個變數明顯又是全域變數的一個屬性,那麼它啟動並執行時候實際的上下文就變成了windows了,當然這個時候name就變成undefined了。回調這個也是一樣,return 的是函數啟動並執行結果。如果我們事先設定 windows.name="windows" 那麼得到的結果就變成了 "hoho, windows" 和"Hola, windows" 了。
function bind(o, m){ return function(){ return m.apply(o, [].slice.call(arguments)); } } var othersay = bind(yetother, one.say); othersay("Hola"); //"Hola, yetother obj"
通過apply可以改變方法執行的上下文,那麼我們構建一個函數來實現這樣一個功能,通過使用方法調用實現內容相關的改變,這樣就不會出現上下文不是我們期望的內容相關的情況了。
//這段是直接複製過來的。 // ECMAScript 5給Function.prototype添加了一個bind()方法,以便很容易使用apply()和call()。 if (typeof Function.prototype.bind === 'undefined') { Function.prototype.bind = function (thisArg) { var fn = this, slice = Array.prototype.slice, args = slice.call(arguments, 1); return function () { return fn.apply(thisArg, args.concat(slice.call(arguments))); }; }; } var twosay2 = one.say.bind(two); console.log(twosay2('Bonjour')); // "Bonjour, another object" var twosay3 = one.say.bind(two, 'Enchanté'); console.log(twosay3()); // "Enchanté, another object"
介紹完了,該說說自己的疑惑了,當複製屬性方法遇到的被繼承對象裡邊存在方法,如何單獨複製出來呢,現在的是直接共用了,因為畢竟方法一般不會經常改動。求解答?
下面是轉載過來的jQuery的extend方法,好像也沒有特殊處理函數這塊,繼承完了兩個函數也是共用的。
$.extend源碼
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 //如果第一個參數是boolean類型 //修正參數,將第二個參數作為target if ( typeof target === "boolean" ) { deep = target; // skip the boolean and the target target = arguments[ i ] || {}; //i++是為了後續 i === length的判斷 i++; } // Handle case when target is a string or something (possible in deep copy) //如果目標既不是對象也不是方法(例如給基本類型擴充屬性方法和屬性不會報錯但是是無用的),修正target為 js對象 if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // extend jQuery itself if only one argument is passed //如果只有一個參數,修正對象為JQuery函數或JQuery對象 if ( i === length ) { target = this ; //修正target所在位置,後面的都是要添加給target的對象 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 //如果target和copy是同一個對象,略過,防止自己的屬性引用了本身對象導致的循環參考,以致GC無法回收 if ( target === copy ) { continue ; } // Recurse if we're merging plain objects or arrays //如果是deep為true,並且要添加給target的copy為對象獲數組 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; };
以上這篇javascript 繼承學習心得總結就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援雲棲社區。