手写call,apply和bind(分析三者的用法与区别)
它們有什么用及區別?
在闡述它們如何使用之前,我們有必要整理清楚this的用法,簡單的說this是JavaScript語言的一個關鍵字,它是函數運行時,在函數體內部自動生成的一個對象,只能在函數體內部使用。
那么問題又來了,this的值是什么呢?
因為this是在函數運行時,函數內部自動生成的一個對象,那么接下來我們通過函數來對this進行分析。 首先JavaScript中的函數可以分為兩類:
- 常規函數:函數聲明式,函數表達式,構造函數
- 箭頭函數:(ES6引入使用)
接下來分別分析this在這些函數中究竟是什么?
理解常規函數中的this
1.純粹的函數調用
function test(name) {console.log(name)console.log(this) } test('Jerry') //調用函數 復制代碼以上函數調用的方式是非常常見的,然而這只是一種簡寫的形式,完整的寫法應該如下:
function test(name) {console.log(name)console.log(this) } test.call(undefined, 'Tom') 復制代碼這里面便出現了我們將要學習的call,先不討論它的作用,我們繼續討論this的用處,call方法接受的第一個參數就是this,但是我們這里是undefined,按照規定,如果你傳的context 是 null 或者 undefined,那么 window對象就是默認的context(嚴格模式下默認context 是 undefined)。
2.對象中函數的調用
const obj = {name: 'Jerry',greet: function() {console.log(this.name)} } obj.greet() //第一種調用方法 obj.greet.call(obj) //第二種調用方法 復制代碼從上面的例子中,我們發現這次call方法的第一個參數為obj,此時說明函數greet內部的this指向了obj對象,這顯而易見便知call方法的作用是改變this的指向,又因為上面兩種調用方式結果一樣可知函數的this指向可以理解為誰調用便指向誰。
3.構造函數中的this
每個構造函數在new之后都會返回一個對象,這個對象就是this,也就是context上下文。
理解箭頭函數中的this
在使用箭頭函數的時候,箭頭函數會默認綁定外層的this值,所以在箭頭函數中this的值和外層的this是一樣的。因為箭頭函數沒有this,所以需要通過查找作用域鏈來確定this的值。
這就意味著如果箭頭函數被非箭頭函數包含, this綁定的就是最近一層非箭頭函數的 this。
注意:多層對象件套里面的this是和最外層保持一致的。
因為今天的重點是講解call,apply,bind的用法及實現,然而箭頭函數是沒有這些方法的,所以箭頭函數的使用僅限于此。
首先說明call,apply是ES5中的語法,bind是ES6新引入的,它們三者的相似之處為:
- 都是用來改變函數的this對象的指向
- 第一個參數都是this要指向的對象
- 都可以利用后續參數進行傳參
不同之處使用一個例子進行說明:
const personOne = {name: "張三",age: 12,say: function () {console.log(this.name + ',' + this.age);} }const personTwo = {name: "李四",age: 24 }personOne.say(); //張三,12 復制代碼對于以上的結果,我們應該都非常清楚,那么問題來了,如果我們想要知道personTwo對象的信息如何實現呢?
分別使用call,apply以及bind方法實現,并從中得到它們三者的區別:
personOne.say.call(personTwo); //李四,24 personOne.say.apply(personTwo); //李四,24 personOne.say.bind(personTwo); //沒有輸出任何東西 復制代碼修改以上代碼對比可知:call和apply都是對函數的直接調用,而bind方法返回的仍然是一個函數,因此我們需要執行它才會有結果。
personOne.say.call(personTwo); //李四,24 personOne.say.apply(personTwo); //李四,24 personOne.say.bind(personTwo)(); //李四,24 復制代碼接著繼續討論其余參數
const personOne = {name: "張三",age: 12,say: function (gender, phone) {console.log(this.name + ',' + this.age + ',' + gender + ',' + phone);} }const personTwo = {name: "李四",age: 24 }personOne.say("女", "123"); 復制代碼這個例子的區別于上面的即為say函數需要傳遞參數,我們分別使用這三種方法實現傳遞參數:
personOne.say.call(personTwo, "女", "123"); //李四,24,女,123 personOne.say.apply(personTwo, ["女", "123"]); //李四,24,女,123 personOne.say.bind(personTwo, "女", "123")(); //李四,24,女,123 復制代碼顯而易見的區別call和bind除了第一個參數外,之后的參數均為一一傳遞,而apply除了第一個參數外,只有一個參數即為一個數組,數組中的每一項為函數需要的參數。
說明它們的用法以及區別之后,我們就要自己嘗試著剖析它的原理,自己書寫這三個方法啦~~~~~
call實現
在知道了它的使用即原理之后,想必直接看實現方法應該也可以理解的,那么先上代碼:
Function.prototype.myCall = function (obj) {const object = obj || window; //如果第一個參數為空則默認指向window對象let args = [...arguments].slice(1); //存放參數的數組object.func = this;const result = object.func (...args);delete object.func; //記住最后要刪除掉臨時添加的方法,否則obj就無緣無故多了個fnreturn result; } 復制代碼代碼非常簡短,一步步進行說明解釋:
因為call方法是每一個函數都擁有的,所以我們需要在Function.prototype上定義myCall,傳遞的參數obj即為call方法的第一個參數,說明this的指向,如果沒有該參數,則指向默認為window對象。
args為一個存放除第一個參數以外的其余參數的數組(arguments為函數中接收到的多有參數,[...arguments]可以將arguments類數組轉換為真正的數組,詳細講解可以查看ES6語法)。
解釋object.func=this之前,我們先使用示例使用一下自己定義的myCall函數:
const personOne = {name: "張三",age: 12,say: function (gender, phone) {console.log(this.name + ',' + this.age + ',' + gender + ',' + phone);} }const personTwo = {name: "李四",age: 24 }Function.prototype.myCall = function (obj) {const object = obj || window; //如果第一個參數為空則默認指向window對象let args = [...arguments].slice(1); //存放參數的數組object.func = this;const result = object.func (...args);delete object.func; //記住最后要刪除掉臨時添加的方法,否則object就無緣無故多了個funcreturn result; }personOne.say.myCall(personTwo,"女",18333669807); //李四,24,女,18333669807 復制代碼根據示例,我們進行解釋,myCall里面的this指的是personOne.say這個方法(因為myCall是一個方法,上面所說的,誰調用它,它的this便指向誰),object.func=this相當于給object這個對象克隆了一個personOne.say方法,讓object在調用這個方法,相當于object.personOne.say,達到了call的效果。(記住最后要刪除掉臨時添加的方法,否則object就無緣無故多了個func)
object.func (...args)里面的參數即為傳入的其余參數。
apply實現
通過上面的分析,想必大家應該已經基本明白了它是如何實現的了,那么接下來實現apply就非常簡單了,因為兩者的區別主要就是參數的傳遞方式不同,和上面一樣,先直接看一下代碼:
Function.prototype.myApply = function (obj) {const object = obj || window; //如果第一個參數為空則默認指向window對象if (arguments.length > 1) {var args = arguments[1]; //存放參數的數組} else {var args = []; //存放參數的數組}object.func = this;const result = object.func(...args);delete object.func; //記住最后要刪除掉臨時添加的方法,否則obj就無緣無故多了個fnreturn result; }personOne.say.myApply(personTwo, ["女", 24]); 復制代碼主要區別就是獲取參數不同,因為apply的帶二個參數為數組,數組中包含函數需要的各項參數值,其余內容實現myCall相同,此處就不在做解釋。
bind實現
話不多說,依舊是先上代碼
Function.prototype.myBind = function (obj) {const object = obj || window; //如果第一個參數為空則默認指向window對象let self = this;let args = [...arguments].slice(1); //存放參數的數組return function () {let newArgs = [...arguments]return self.apply(object, args.concat(newArgs))} }personOne.say.myBind(personTwo, "女", 24)(); 復制代碼前面的知識不重復說,return function是因為bind返回的是一個函數,并且這個函數不會執行,需要我們再次調用,那么當我們調用的時候,我們依舊可以對這個函數進行傳遞參數,即為支持柯里化形式傳參,所以需要在返回的函數中聲明一個空的數組接收調用bind函數返回的函數時傳遞的參數,之后對兩次的參數使用concat()方法進行連接,調用ES5中的apply方法。
到此為止,這三種方法的使用,區別以及實現已經都講述完了,希望這篇文章對大家有所幫助~~總結
以上是生活随笔為你收集整理的手写call,apply和bind(分析三者的用法与区别)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈数组常见遍历方法
- 下一篇: 修改Ubuntu主机名