資深前端需要知道的那些事(2)

資深前端需要知道的那些事(2)

4 人贊了文章

Function.prototype.bind方法在ECMA-262版本中才被加入,這意味某些老版的瀏覽器尚不支持此屬性,那麼如果讓我們自己實現一個bind方法該怎麼做呢。這個問題需要對原型繼承有很清楚的理解才可以解決。

首先,讓我們拋開es6最新的class關鍵詞,從原型的角度看下如何實現一個類以及類的繼承。

es5中實現一個類的代碼大家應該很熟悉了,如下:

function Human(age){ this.age=age;}Human.prototype.say=function(){ console.log("hello, i am "+this.age+" years old");}var h=new Human(22);h.say();//輸出 "hello, i am 22 years old"

注意當對一個函數使用new表達式的時候,函數內部的this指向當前正在被創建的實例,此時這個函數也稱為構造函數,我們就是要在構造函數中完成實例的初始化。當沒有使用new而是直接調用函數的時候,內部的this指的是全局上下文,在瀏覽器中就是window對象,在nodejs中就是node上下文環境。

那麼標準的原型繼承是什麼樣的呢,如下

function Man(age,name){ Human.call(this,age); this.name=name;}Man.prototype=new Human();Man.prototype.constructor=Man;Man.prototype.say=function(){ console.log("hello, i am "+this.age+" years old, my name is "+this.name);}let m=new Man(22,"王五");m.say();//輸出 "hello, i am 22 years old, my name is 王五"

注意我們在Man類的構造函數中調用了父類Human的構造方法,並新增了name成員變數,同時我們還重寫父類的say方法。

很多人可能會覺得第五行

Man.prototype=new Human();

改為

Man.prototype=Human.prototype

也沒問題。是的,確實在效果上也可以實現原型的繼承,但是不是個好習慣,因為這麼做會導致原型鏈缺失一環,在沿著_proto_屬性一層層往上回溯的時候,找不到constructor為Human的一環(Human被Man替換了,注意第六行代碼)。

下面我覺得很有必要講講函數的三個重要方法,call, apply, bind

call和apply的區別這裡簡單的說下,例如下面的方法

function test(a,b){return a+b}

使用call去調用是

test.call(null,1,2)

使用apply調用是

test.apply(null,[1,2])

大家很容看到二者在傳參的時候的差別,call是一系列的值,而apply要傳一個數組。我們這裡更容易迷惑的其實是第一個參數的用法。無論是call還是apply,第一個參數表示的是函數執行時內部的this的指向,當第一個參數傳null或者undefined的時候,函數內部的this指向的當前執行環境的全局對象,瀏覽器中是window對象,nodejs環境下就是node全局對象。

call和apply的第一個參數存在的意義是是我們可以調用某些不是這個類的實例的實例的這個方法。聽起來很繞,舉個例子來說,我們知道,函數內部是可以訪問到一個叫做arguments的變數,這個變數指的是當前傳入的參數的集合,注意arguments可不是個數組,arguments其實只是個對象,是Object類的實例,只不過擁有length屬性,以及可索引的參數值。而Array類的實例擁有splice, push, pop等眾多的方法,這都是arguments所沒有的。那麼假設我們就想對arguments進行slice操作怎麼辦呢,我們可以這樣

Array.prototype.slice.call(arguments)

這個表達式的返回值是一個參數數組,真正的Array類的實例。因為Array.prototype.slice方法中用到的成員變數arguments都有,比如length值,用0,1,2... 等等可索引的成員變數,因此slice方法可以應用到arguments上(這不由得讓我想起鴨子類型的定義,如果它看起來能游泳,能嘎嘎叫,那它就是只鴨子,雖然世界上可能會存在其他擁有這兩種特徵的動物)。

說完了call和apply, bind其實很好理解了,bind和call很像,唯一的區別是bind不會執行這個函數,而是返回了對於這個函數的綁定了預定值的函數(我也不知道它是個什麼鬼)

以上面的test函數為例

var test2=test.bind(null,1);test2(2);//返回3

因為test2中已經綁定的預定的第一個參數,因為test2傳入的第一個參數其實會被當作第二個參數,所以1+2=3,返回3.

需要特別告知的一點是,如果test2被當作構造函數被new了,即

var t=new test2(2)

,此時test.bind傳入的第一個參數將被無視,test2/test執行時內部的this指的是當前正在被創建的實例。再例如:

function Human(weight,height){ this.weight=weight; this.height=height;}Human.prototype.des=function(){ return `體重:${this.weight},身高:${this.height}`;};let Man=Human.bind(null,50);let m=new Man(166);let des=m.des();console.log(des);//輸出"體重:50,身高:166"

現在讓我們總結下js中的原型繼承的規律

在討論原型繼承的時候,很難理解的其實是Object.prototype和Function,關係真的非常亂,簡單表示下大概是這樣:

Object.prototype//始祖Object.__proto__===Function.prototype===Function.__proto__Function.prototype.__proto__===Object.prototypefunction Test(){}Test.prototype.__proto__===Object.prototypeTest.__proto__===Function.prototypevar t=new Test()t.__proto__===Test.prototype

從上面的關係中我們可以很清楚為什麼我們隨便new一個對象,這個對象中就已經有了諸如toString, hasOwnProperty等的方法(來自Object.prototype)。我們隨便寫個函數,這個函數就有了bind, call等方法(來自Function.prototype)。

好吧,現在讓我們來實現Function.prototype.bind方法吧

這裡為了區分使用mybind命名,貼代碼:

Function.prototype.mybind=function(){ //被綁定的上下文對象,可能是null或undefined let obj=arguments[0]; //預先綁定的參數 let args=Array.prototype.slice.call(arguments,1); let that=this; let CONSTRUCT=function(){ //後面又傳入的參數 let newargs=Array.prototype.slice.call(arguments); //this有可能是正在被null的實例,也有可能是綁定的上下文對象 if(this instanceof CONSTRUCT){ //合併參數 that.apply(this,args.concat(newargs)); }else{ //合併參數 return that.apply(obj,args.concat(newargs)); } }; //保證原型繼承 CONSTRUCT.prototype=that.prototype; return CONSTRUCT; };

下圖是測試代碼

function Human(weight,height){ this.weight=weight; this.height=height; } Human.prototype.des=function(){ return `體重:${this.weight},身高:${this.height}`; }; let man={weight:60,height:185}; let des=Human.prototype.des.mybind(man)(); console.log(des);//輸出 "體重:60,身高:185" console.log("-----") let Man=Human.mybind(null,50); let m=new Man(166); let des2=m.des(); console.log(des2);//輸出 "體重:50,身高:166" console.log("-----") function Test(c1,c2){ console.log(c1,c2); } let t=Test.mybind(null,33); t(44);//輸出 "33,44"

推薦閱讀:

季度安全報告(技術周刊 2018-05-04)
yarn 安裝使用小記
一個價格校驗器的設計實現
2018年最值得關注的30個Vue開源項目
棒棒團第六期分享 編程經驗,建議,思維分享

TAG:前端工程師 | 前端入門 | 前端架構 |