this指向與call,apply,bind
❝「this」問題對於每個前端同學來說相信都不陌生,在平時開發中也經常能碰到,有時候因爲「this」還踩過不少坑,並且「this」問題在面試題中出現的概率也非常高,我們一起來了解一下
❞this
的指向與call
,apply
,bind
this的指向
ES5中的this
「在ES5中,this一般指向函數調用時所在的執行環境,與函數定義的位置無關。也可以理解成this永遠指向最後調用它的對象」
在普通函數中的this總是指向它的「直接調用者」,默認情況下指向全局對象(瀏覽器爲window) 在嚴格模式下,沒有直接調用者的函數中的 this
爲undefined
Call, apply,bind函數,this指向的是綁定的對象 對象函數調用,this指向調用它的那個對象 構造函數中的this,指向該構造函數new出來的實例對象
var obj = {
a:function(){
console.log(this)
console.log(this.b)
console.log(this.c)
console.log(this.a)
},
b:2,
c:3
}
var b = obj.a
b()
// 結果:window,f(){...},undefined,undefined
obj.a()
// 結果:{a:..,b:2,c:3},2,3,f(){...}
/**
* 解析:
* b()調用,此時b函數所處的執行環境是全局環境,this指向window
* obj.a()調用,此時a是作爲對象方法進行調用,this指向調用對象obj
*/
ES6中的this
「在ES6中新增了一種箭頭函數,箭頭函數的this始終指向它定義時的this,而非執行時」
箭頭函數沒有自己的this,它的this是繼承來的,默認指向它定義時所在的對象,即 箭頭函數中的this指向外層代碼的this
不可以當作構造函數,也就是說,不可以用 new
命令調用,否則會拋出一個錯誤箭頭函數內沒有 arguments
對象,可以用rest
參數代替不可以使用 yield
命令,因此箭頭函數不能用作generator
函數箭頭函數沒有自己的 this
,所以不能用call
,apply
,bind
這些方法改變this指向
var obj = {
hi: function(){
console.log(this);
return ()=>{
console.log(this);
}
},
sayHi: function(){
return function() {
console.log(this);
return ()=>{
console.log(this);
}
}
},
say: ()=>{
console.log(this);
}
}
const hi = obj.hi() //this->obj對象
hi() // this->obj對象
const sayHi = obj.sayHi()
const sayHiBack = sayHi() //this->window
sayHiBack() //this->window
obj.say() //this->window
「輸出結果依次爲obj對象,obj對象,window,window,window」
❝解析:
1.第一個obj.hi()很好理解,hi爲普通函數,this指向調用它的那個對象,即obj
2.第二個執行hi(),它其實是上一個執行後返回的函數,並且是箭頭函數,箭頭函數本身沒有this,我們往他的上一級去查找,我們剛剛得出上一級的this爲obj,所以這裏的this也指向obj
3.第三個執行obj.sayHi(),這裏沒有打印this,而是返回了一個普通函數
4.第四個執行sayHi(),其實執行的是剛剛返回的那個普通函數,這裏的this則指向調用它的那個對象,沒有則指向window
5.第五個執行sa yHiBack(),指向的是剛剛第四次執行返回的箭頭函數,OK,箭頭函數我們往上一層找,也是window
5.第六個執行obj.say(),這裏這個say()是一個箭頭函數,當前代碼塊obj不存在this,只能往上一層查找,指向window
❞
call, apply,bind的區別
我們都知道call,apply,bind
都可以用來改變this
指向,但這三個函數稍稍有些不同。
call與apply唯一的區別就是它們的傳參方式不同,call從第二個參數開始都是傳給函數的,apply只有兩個參數,第二個參數是一個數組,傳給函數的參數都寫在這個數組裏面 call與apply改變了函數的this指向後會立即執行,而bind是改變函數的this指向並返回這個這個函數,不會立即執行 call與apply的返回值是函數的執行結果,bind的返回值是改變了this指向的函數的拷貝
call
❝❞
call()
方法使用一個指定的this
值和單獨給出的一個或多個參數來調用一個函數。(來自MDN)
「語法:」 fun.call(thisArg,arg1[,arg2,arg3...])
「解釋:」 call方法用來爲一個函數指定this對象,第一個參數是你想要指定的那個對象,後面都是傳給該函數的參數,之間用逗號隔開
var person = {
name:'南玖',
gender: 'boy',
}
var speak = function(age,hobbit){
console.log(`我是${this.name},今年${age}歲,愛好${hobbit},歡迎優秀的你關注~`)
}
speak.call(person,18,'前端開發') // 我是南玖,今年18歲,愛好前端開發,歡迎優秀的你關注~
apply
❝「
❞apply()
」 方法調用一個具有給定this
值的函數,以及以一個數組(或類數組對象)的形式提供的參數。(來自MDN)
「語法:」 fun.apply(thisArg,[arg1,arg2,arg3...])
「解釋:」 apply方法與call方法基本類似,不同的是,兩者的參數形式,apply方法傳遞的是一個由若干個參數組成的數組。
var person = {
name:'南玖',
gender: 'boy',
}
function speak(age,hobbit){
console.log(`我是${this.name},今年${age}歲,愛好${hobbit},歡迎優秀的你關注~`)
}
speak.apply(person,[18,'打籃球⛹️']) //我是南玖,今年18歲,愛好打籃球⛹️,歡迎優秀的你關注~
bind
❝bind() 方法會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作爲它運行時的 this,之後的一序列參數將會在傳遞的實參前傳入作爲它的參數。(來自於 MDN )
❞
「語法:」 fun.bind(thisArg,arg1[,arg2,arg3...])()
「解釋:」 bind方法用來爲方法指定this對象並返回一個新的函數,它的參數與call函數一樣。「它本身是不會調用的,需要自己手動調用。」
var person = {
name:'南玖',
gender: 'boy',
}
function speak(age,hobbit){
console.log(`我是${this.name},今年${age}歲,愛好${hobbit},歡迎優秀的你關注~`)
}
speak.bind(person,18,'旅遊⛱️')() //我是南玖,今年18歲,愛好旅遊⛱️,歡迎優秀的你關注~
「注意這裏需要自己再調用一次,因爲bind只會返回這個改變了this指向的函數,並不會自己執行」
call,apply該用哪個?
參數數量,順序確定就用call,參數數量,順序不確定就用apply 參數數量少用call,參數數量多用apply 參數集合已經是一個數組的情況,最好用apply
bind的應用場景
1.保存參數
我們先來看一道經典面試題
for(var i=1;i<6;i++){
setTimeout(()=>{
console.log(i) // 6,6,6,6,6
},i*1000)
}
相信大家都知道這裏會打印出五個6,因爲在執行settimeout回調函數時,i已經變成了6
❝那麼如何讓它打印出1,2,3,4,5呢?
❞
當然方法有很多,比如閉包、將var改成let使它形成塊級作用域,這裏先不講,後面單獨講閉包會提出來
我們也可以用bind來解決
for(var i=1;i<6;i++){
setTimeout(function(i){
console.log(i) // 1,2,3,4,5
}.bind(null,i),i*1000)
}
2.回調函數this丟失問題
var student = {
subject:['JS','VUE','REACT'],
study: function(){
setTimeout(function(){
console.log(`我是南玖,我在學習${this.subject.join('、')}`)
}.bind(this),0)
}
}
student.study() //我是南玖,我在學習JS、VUE、REACT
❝想一想這裏settimeout的回調如果不用bind綁定this,結果會怎樣?
❞
「結果是報錯,因爲不給settimeout回調函數綁定this的話,那它的this應該指向的是全局window,全局沒有subject,調用join會報錯」
模擬call
「思路:」
根據call的規則設置上下文對象,也就是 this
的指向。通過設置 context
的屬性,將函數的this指向到context上通過隱式綁定執行函數並傳遞參數。 刪除臨時屬性,返回函數執行結果
Function.prototype.myCall = function(context){
// context指的是那個想要借方法的對象,併爲它指定默認值,沒傳就是window
var context = context || window
// 將要借用的那個方法綁定在當前要使用該方法的對象的fn屬性上
context.fn = this
// 這裏的this指向你想要借用的那個方法也就是.myCall前面的調用者(這裏的this指的是一個函數)
console.log(this)
//獲取參數,也就是相當於call的參數列表
var args = [...arguments].slice(1)
// 將參數傳給該函數並執行
var res = context.fn(...args)
// 刪除該方法
delete context.fn
// 返回執行結果
return res
}
模擬apply
「思路:」
與call類似,主要區別是參數的處理
/*
實現原理與call類似,主要是參數不同,apply接受一個參數數組
*/
Function.prototype.myApply = function (context){
var context = context || window
context.fn = this
// 判斷第二個參數是否爲數組,不爲數組需提示用戶(報錯提示)
console.log(arguments.length)
if(arguments.length > 2){
throw new Error('只能傳遞兩個參數')
}else if(!(arguments[1] instanceof Array)){
throw new Error('第二個參數需要是數組類型')
}
var res = context.fn(...arguments[1])
delete context.fn
return res
}
模擬bind
Function.prototype.myBind = function(context){
var context = context || window
var _this = this
var args = [...arguments].slice(1)
// 這裏返回的是一個函數
var res = function(){
return _this.apply(context,...args)
}
return res
}