js原型继承
這里寫自定義目錄標題
- 前言
- 一、原型的特性
- 二、原型繼承到底有幾種
- 1、原型鏈繼承
- 2、構造函數(shù)繼承
- 3、組合式繼承
- 4、原型式繼承
- 5、寄生式繼承
- 6、寄生組合繼承
- 7、對象冒充
- 8、class繼承
- 三、引出的幾道經(jīng)典題目
- 1、new的時候做了什么
- 2、es6箭頭函數(shù)的特性
- 4、var、let、const的區(qū)別
- 5、call、apply、bind的區(qū)別
- 6、編程的幾種范式
- 使用原型封裝一個插件
前言
這是一道非常經(jīng)典的前端面試題,尤其是對于初中級前端來說,基本上是必考題。當我還是一個前端萌新的時候,也被問到過很多次。當時我對原型、原型鏈、繼承的理解,也就是面試題看懂了,面試的時候能忽悠幾句的程度,面試官也不一定真的搞懂了,所以也總能蒙混過關。
但是隨著新一代前端的成長,大家用的都是class,相信這道題很快很成為歷史,到時候只有類,沒有原型這個詞。
為什么原型這么難懂?我覺得有下面三個主要原因:
1、它確實比較難,你找一個寫java的人來解釋js的原型繼承他也不一定能講清楚
2、你可能干了好幾年都不一定會深入使用它,即使react玩家天天在寫,但是應該有很多人只是在套模板,更何況vue那種更舒服的模板寫法(vue最終也是原型繼承,比如一開始的 new vue())。
3、網(wǎng)上的博客五花八門,有說繼承有五種,有說有六種,有的說有八種,而且名字還可能不一樣
本文試圖通過一個實際應用的例子來幫助初學者理解原型繼承 (但是沒成功,有興趣看最后一節(jié))
本文對原型繼承的幾種方式做了整理,算是一篇筆記
一、原型的特性
雖然很多教程跟書本上都有,這里還是簡單介紹一下,避免有些同學忘記
原型有三大特性:封裝 繼承 多態(tài)
封裝:指的是把很多屬性、方法全都集中到原型里,要訪問的時候都是統(tǒng)一入口,不會像過程式編程(后面有介紹)一樣到處都是作用域比較廣的變量。
繼承:指的是兩個原型之間,子類可以繼承父類的屬性和方法。
多態(tài):父類有一個屬性叫做“name”,子類也可以有一個叫“name”的屬性,子類只會生效自己的“name”。
這里是按我自己的理解簡單介紹的,書面化的介紹請看《javascript權威指南》,畢竟作者也不一定是對的,不管是博客還是視頻,都不是權威的,都可能會講錯,對于自己不理解的東西,一定要從源頭找答案對照。
二、原型繼承到底有幾種
講了這么多廢話,先上個總結吧,畢竟刷到這篇博客的人基本上都是為了面試:
分細一點應該有八種,看了好多其它的博客,最多的也是介紹到八種。
1、原型鏈繼承(Child.prototype=new Parent())
2、構造函數(shù)繼承(在構造函數(shù)里面使用call)
3、組合繼承(原型鏈繼承+構造繼承)
4、原型式繼承(用了object.create,也有把他歸類到寄生式)
5、寄生式繼承(原型式繼承的進階版)(也有叫拷貝繼承)
6、寄生組合式繼承(寄生式+組合式,是class出現(xiàn)前的終極繼承方案)
7、對象冒充(不知道誰先想出來的怪招)
8、class繼承(大家都嫌寄生組合太麻煩了,所以出現(xiàn)了它,屠龍術)
因果關系記憶法:
因為原型鏈繼承不能傳參,所以有了構造繼承,但是構造繼承不能繼承父級的原型,所以出現(xiàn)了結合兩種方式的組合繼承。
因為組合繼承實例化了兩次父類,性能有缺陷,強迫癥的前端們?nèi)滩涣?#xff0c;所以想出了原型式繼承,再增強成寄生繼承,把寄生繼承跟組合繼承一結合,變成了寄生組合繼承,用來解決組合繼承的小缺陷
有一位腦洞清奇的人才,發(fā)現(xiàn)了對象冒充,讓大家的面試題又多了一個答案。
最后大家都覺得寫寄生組合繼承太費勁了,所以出現(xiàn)了class
1、原型鏈繼承
優(yōu)點:繼承了父類的所有,包括原型
缺點:
1、不能給父類傳參
2、引用類型的屬性會被子類修改(比如一個子類改了父類的Array類型的屬性,另一個子類的原型上Array也會變)
2、構造函數(shù)繼承
優(yōu)點:
1、可以傳參數(shù)
2、引用類型的屬性不會被子類修改
缺點:
1、不能繼承原型,因為只是把屬性用call綁定到了this上
2、每次構造函數(shù)都要多走一個函數(shù)(call)
3、組合式繼承
優(yōu)點:
1、可以傳參又繼承了原型
2、引用類型的屬性不會被子類修改
缺點:
1、實例和原型上存在兩份相同的屬性,一份在this,一份在prototype(this不等于prototype,雖然簡單用起來差不多),也就是說實例化了兩次Parent,性能上欠佳,所以才有了下面的原型式到寄生組合的進化
2、每次構造函數(shù)都要多走一個函數(shù)(call)
4、原型式繼承
優(yōu)點:不用實例化父類,只是用Object.create創(chuàng)建了一個副本
缺點:不能改動自己的原型(因為返回new已經(jīng)實例化了),所以也不能復用
5、寄生式繼承
也有把原型式歸到寄生式里面的,因為就包了個殼
優(yōu)點:在原型式繼承的基礎上,增強了對象
缺點:
1、多包了一層,當然是多走了一次函數(shù)啦
2、也是不能復用,跟原型式一樣
6、寄生組合繼承
優(yōu)點:
以上所有的優(yōu)點
缺點:
1、寫起來復雜
2、多執(zhí)行了call、create
7、對象冒充
優(yōu)點: 讓你的面試答案多了一個
缺點:
1、當父類的屬性相同時,后面定義的會覆蓋前面定義的屬性(看實現(xiàn)就知道為什么)
2、每次構造函數(shù)都要多走一個函數(shù)
8、class繼承
優(yōu)點:優(yōu)點就是沒有缺點
class Parent { static staticMethod() { return true; } constructor(name) { this.name = name; } ParentMethod() { console.log('method2'); } }; class Child extends Parent{ constructor(name) { super(name); this.childName = 'child'; } childMethod() { console.log('childMethod'); } }三、引出的幾道經(jīng)典題目
1、new的時候做了什么
也就是 var Child = new Parent(); 干了啥
通過對原型的理解,我們很容易解答
1、創(chuàng)建一個空對象(不創(chuàng)建一個空的怎么往里面塞東西)
2、讓Prarent中的this指向Child,并執(zhí)行Parent的函數(shù)體(classconstructor,Parent本身)
3、設置原型鏈,將Child的__proto__的成員指向了Prarent的prototype的成員
4、給Child賦值,Parent的返回值類型是個值child就是個值,是個對象,child就是這個對象
也有回答說:將初始化完畢的新對象地址,保存到等號左邊的變量中
就是賦值,沒啥好解釋的,面試官聽不懂公司就沒必要去了
名詞解釋:
函數(shù)體:用class就是constructor,用構造函數(shù)就是Parent本身
prototype是原型才有的屬性,__proto__對象跟原型都有,__proto__里面存的是Parent的constructor
__proto__跟prototype可以額外找別的文章看
2、es6箭頭函數(shù)的特性
1、簡潔,直接返回的時候可以省略花括號跟return,一個參數(shù)的時候可以不寫入?yún)⒗ㄌ?br /> 2、this指向上層,上一層是箭頭函數(shù)繼續(xù)向上
3、不可以使用arguments對象,該對象在函數(shù)體內(nèi)不存在。如果要用,可以用 rest 參數(shù)代替。
4、不可以使用yield命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
5、不可以作為構造函數(shù),因為this不是指向自己(這里只是為了說明這一點)
4、var、let、const的區(qū)別
這里主要是想說一下var的聲明提前,為什么我的例子里面都是用的var
因為在我們練手的時候經(jīng)常會反復聲明同一個變量,只有var可以反復賦值
而且聲明提前可以讓我們把想放一起的代碼集中到一塊
但是在實際使用中我們并不希望聲明提前,并且反復聲明,所以用let、const
同作用域的情況下:
1、var可以反復賦值,var是聲明提前
2、let、const只能賦值一次
3、const是常量,數(shù)值不能改變
4、const如果是聲明的對象,只是引用被固定
5、call、apply、bind的區(qū)別
首先他們的作用都是改變this的指向(就是給this賦值)
區(qū)別:
1、入?yún)⒎矫?call、bind都是接收一個個逗號隔開的參數(shù),apply接收的是數(shù)組
2、使用入?yún)⒌臅r候都一樣,apply入?yún)⑹菙?shù)組,取的時候還是跟call,bind一樣一個個逗號隔開
3、call、apply是立即執(zhí)行this賦值,bind返回了一個函數(shù),需要手動執(zhí)行了才會給this賦值
通過下面的代碼來理解
6、編程的幾種范式
1、聲明式編程----html、css,看到他們你應該懂的
2、過程式編程----一步步變量聲明,執(zhí)行函數(shù)下來,這個是基礎,只要是開發(fā)都在用,即使是面向?qū)ο罄锩嬉矔嬖谶^程式
3、面向?qū)ο缶幊?---也叫oop,就是原型和對象
4、函數(shù)式編程----比較復雜
個人感覺跟過程式也差不多,各種說法都有,我也不敢寫太絕對,這里就講我知道的
函數(shù)式要先知道純函數(shù)的概念
像是數(shù)組方法 slice是截取生成新函數(shù),不改變輸入的值,就是純函數(shù)
像是 splice會改變原數(shù)組,就不是純函數(shù)
函數(shù)式的典型,輸入一個函數(shù),返回一個函數(shù)
代碼全都是(也有人認為大多數(shù)是)純函數(shù)來寫,使用了大量的可復用的函數(shù)的編程,就差不多算是函數(shù)式編程。
咱級別不夠,接觸不到函數(shù)式寫得很正宗的大佬,只能用自己的微薄經(jīng)驗總結一下:代碼里多點純函數(shù),復用性會強一點
使用原型封裝一個插件
本來是想要通過一個實際例子來講解一下原型繼承,但是寫著寫著發(fā)現(xiàn)舉個例子說明起來更不好理解了。本著寫了就不要浪費的原則,這里把還沒寫一半的代碼貼出來湊湊字數(shù),感興趣可以瞅瞅。
在vue跟react統(tǒng)治國內(nèi)前端的情況下,很多人都是在寫組件、套模板,只有以前jquery時代大家會經(jīng)常用原型封裝插件。現(xiàn)在需要從頭寫原型的場景很少,個人認為canvas是最可能自己寫代碼會深入使用原型繼承的。
假如我們想用canvas畫一個圓弧進度條
1、先貼出html部分
2、如果習慣過程式編程的人(比如作者,因為寫起來確實順手),最開始會寫成下面的風格
// 角度轉(zhuǎn)弧度 function radians(degrees){return (Math.PI / 180) * degrees } // canvas畫圓弧,可以不用懂 function drawArc(strokeStyle, endAngle, ctx, lineWidth, centerXY, startAngle, round){// 開始一段繪制ctx.beginPath();// 顏色ctx.strokeStyle = strokeStyle;// 線寬ctx.lineWidth = lineWidth;// 線的兩端以圓角結束ctx.lineCap = 'round';// 畫圓弧參數(shù)依次為: 中心點x,y坐標,半徑,起始弧度,結束弧度ctx.arc(centerXY, centerXY, round, radians(startAngle), radians(endAngle));// 結束繪制ctx.stroke(); } // 畫圓弧 function draw(opitons) {let {lineWidth = 10, bgColor = 'rgb(198, 219, 223)', startAngle = 150, endAngle = 30, round, margin = 20, color = '#3399ff', percent, centerXY,id} = opitons;// 中心點等于半徑+左(上)邊距let centerXY = centerXY || (round + margin);// 獲取canvas畫筆var canvasDom = document.getElementById(id);var ctx = canvasDom.getContext('2d');// 畫背景圓弧drawArc(bgColor, endAngle, ctx, lineWidth, centerXY, startAngle, round);// 根據(jù)百分比算出弧度let end = percent * 240 / 100 + 150;end= end > 360 ? (end - 360) : end;drawArc(color, end, ctx, lineWidth, centerXY, startAngle, round) } // 執(zhí)行 draw({percent: 80, id: 'canvas', round: 80})3、當作者想要做成插件的時候,就會改成面向?qū)ο缶幊?#xff08;用class)
// 因為上面注釋很充足,這邊就不寫注釋了 class Echarts {centerXY = 0;startAngle = 150;endAngle = 30;round = 0;margin = 0;color = '#3399ff';bgColor = 'rgb(198, 219, 223)';name = '進度';percent = 100;lineWidth = 10;constructor(opitons){this.name = opitons.name || this.name;this.round = opitons.round || this.round;this.margin = opitons.margin || this.margin;this.color = opitons.color || this.color;this.percent = opitons.percent || this.percent;this.centerXY = this.centerXY || (this.round + this.margin);var canvasDom = document.getElementById(opitons.id);this.ctx = canvasDom.getContext('2d');}// 畫進度條drawGauge(){this.drawArc(this.bgColor, this.endAngle);let endAngle = this.percent * 240 / 100 + 150;endAngle = endAngle > 360 ? (endAngle - 360) : endAngle;this.drawArc(this.color, endAngle);}// 畫圓弧drawArc(strokeStyle, endAngle){let ctx = this.ctxctx.beginPath();ctx.strokeStyle = strokeStyle;ctx.lineWidth = this.lineWidth;ctx.lineCap = 'round';ctx.arc(this.centerXY, this.centerXY, this.round, this.radians(this.startAngle), this.radians(endAngle));ctx.stroke();}// 角度轉(zhuǎn)弧度radians(degrees) {return (Math.PI / 180) * degrees} } var echarts = new Echarts({percent: 80, id: 'canvas', round: 80}); echarts.drawGauge();可以很直觀的看到原型的封裝的優(yōu)點:
1)內(nèi)部屬性可以儲存一些配置,內(nèi)部方法可用,不用通過參數(shù)傳進去
2)暴露到外部的只有原型本身,不會出現(xiàn)很多全局變量
4、當想要在原本的圓弧的基礎上加一個圓弧,變成這樣
實際項目中用到繼承需要比較復雜的場景,作者也只是在剛學習前端的時候?qū)懥酥袊笃濉⒍砹_斯方塊練了練手就再也沒在項目中遇到過了,這里刻意用一下繼承(用得很生硬)
總結
- 上一篇: 插入CSS样式表
- 下一篇: gluster快照创建