看了不少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 suber1 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;
好了,这样就把三方法的弊端干掉了,这个可以完美的使用了吧。
五、复制属性实现
拷贝我们可以写一个拷贝函数来实现。
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); //true console.log(child.num === parent.num); //true
复制成功,那么child成功继承了parent的一些属性,但是后面两个测试发现他们是相等的,就表明了他们在公用同一个数组,同一个函数,函数这个可以,但是数组这个就有问题了,如果一个chiild改变了数组,几个被继承对象的数组也跟着变了,这就不给力了啊。
为什么会发生这种情况呢,js里边对象存储的是指针,然后这个指针指向这个值,我们在这复制的实际是指向该对象的指针的值,所以继承对象和被继承对象都指向了同一个对象,接下来看看如何使用深度复制来解决这个问题。
深度复制对象属性:
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 继承学习心得总结就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。