搞清楚this這種玄學的東西的機制,作用一自然是應付面試官,作用二就是可以維護別人的爛代碼啦~
1 前置知識
1.1 對this的一個大誤解
很多人對this有一個潛意識裏的誤解——認為this的值取決於其所在函數是在哪裏聲明的
let obj = {
a: function () {
console.log(this);
},
b: function () {
let f = obj.a;
f();
}
}
obj.b(); // window
很多人在遇到上面這個面試題時,看到函數是在對象內部聲明,都會誤認為this指向obj
1.2 函數名即指針
函數名就是指向函數對象的指針
this作為一個函數內部的對象,自然與函數緊密相連
然而,很多朋友都沒搞清楚函數名竟然是個指針
看下面的代碼,思考問題:
- fn1是否能夠直接找到其函數體的位置(ans:能)
- fn1和obj還有關係嗎(ans:無關係)
function fn() {
console.log(this);
}
let obj = {
a() {
return fn;
}
}
let fn1 = obj.a();
fn1(); // window
如果上面兩個問題你能想明白,或許你對this能夠指向window已經有一定的感覺
2 this機制詳解
2.1 this本質上就是指向它的調用者
javascript作為一門解釋型語言,this的值到底是什麼,必須到函數被調用時才能確定。
什麼意思呢,比如下面這段代碼
function fn() {
console.log(this);
}
請問:你現在知道fn裏的this是什麼嗎?
不可能知道的,因為fn沒有被調用!
那麼什麼又叫做必須到函數被調用時才能確定呢?
我們來看看對上面的fn進行不同的調用,結果是什麼
function fn() {
console.log(this);
}
let obj = {fn};
fn(); // window
obj.fn(); // obj
可以很明顯地發現,因為調用方式的不同,this的值也不同
那麼this本質上就是指向它的調用者這句話該怎麼理解呢?
先別急,整個文章都是圍繞這句話展開的
首先,我們分析一下一個函數到底有幾種調用方式,再分別進行闡述:
全局調用方法調用new調用
2.1 全局調用(獨立調用)
只要遇到獨立調用,基本可以無腦推斷該函數內部的this為windows
function foo() {
console.log(this);
}
foo(); // window
對於foo()而言,foo是函數名,而在js中,函數名只是一個指針
類似這種函數名()形式,孤零零地單獨出現,《你不知道的javascript》作者把這種方式稱之為獨立調用,而這種調用會使得被調用函數內部的this默認綁定為window
結合this本質上就是指向它的調用者這句話,全局調用的本質其實就是window調用了foo這個函數
foo();
// 等價於下面
window.foo();
2.2 方法調用
方法(method)指的是某個對象的屬性是一個函數,如obj = {fn:function(){}},而obj.fn()叫做方法調用結合
this本質上就是指向它的調用者這句話:經過方法調用後,方法內的this指向擁有該方法的對象
let obj = {
fn() {
console.log(this);
}
}
obj.fn(); // obj
2.2.1 方法調用的就近原則
在多個對象嵌套的情況下,this指向調用它的距離最近的那一個對象
let obj = {
a: {
b: {
fn() {
console.log(this);
}
}
}
}
obj.a.b.fn(); // obj.a.b
2.2.2 和全局調用進行對比
下面這段代碼,在不運行的情況下,很多人都會猜錯
let obj = {
a() {
console.log(this);
}
}
let fn = obj.a;
obj.a(); // obj
fn(); // window
相信大家對obj.a();沒有疑問,關鍵在於fn()為什麼是window
其實結合1.2小節 函數名即指針一起來看,fn只是一個指針,現在fn指向了obj.a這個函數,而fn()是一個全局調用,因此this自然指向了window
2.3 new
關鍵在於記住
new做了哪些事情:
- 創立一個臨時對象,
this指向該臨時對象- 把實例的
__proto__指向類的prototype- return臨時對象
function fn() {
console.log(this);
}
new fn(); // 結果看下面的截圖
3 其它場景下的this解惑
3.1 嚴格模式下的this
嚴格模式下只需要注意一點就行,其它情況下與非嚴格模式相同
全局作用域裏函數中的this是undefined
function test() {
"use strict"
console.log(this)
}
test() // undefined
所以,在使用構造函數時,如果忘了加new,this不再指向全局對象,而是報錯,因為這就是函數的全局調用
let People = function (name) {
"use strict"
this.name = name
}
People() // Cannot set property 'name' of undefined
3.2 數組中的this
function fn() {
console.log(this)
}
arr[fn, fn2, fn3]
arr[0]() // ??
// answer:arr
// 解析
// 數組也是對象的一種
// arr[0]() 可以看做 arr.0().call(arr)
3.3 嵌套函數裏的this
要注意的是:不論函數名()這種形式出現在哪,都是獨立調用
// 例子一
function fn0() {
function fn() {
console.log(this);
}
fn();
}
fn0(); // fn中this是全局變量
// 例子二
let a = {
b: function () {
console.log(this) // {b:fn}
function xx() {
console.log(this) // window
}
xx()
}
}
a.b()
3.4 setTimeout、setInterval中的this
this指向全局變量
document.addEventListener('click', function (e) {
console.log(this);
setTimeout(function () {
console.log(this); // 這裏的this是全局變量
}, 200);
}, false);
3.5 事件中的this
事件裏的this指向的是觸發事件的DOM節點
document.querySelector('div').addEventListener('click',function (e) {
console.log(this) // <div></div>
})
4 自己指定this
我個人認為,對待this,應該儘量使用call/apply/bind去強制綁定,這樣才是上策
4.1 call/apply和bind概覽
- 我們要將
call/apply歸為一類,bind單獨歸為一類 - 三者的共同點是都可以指定this
- call/apply和bind都是綁定在Function的原型上的,所以Function的實例都可以調用這三個方法
Function.prototype.call(this,arg1,arg2)
Function.prototype.apply(this,[arg1,arg2])
Function.prototype.bind(this,arg1,arg2)
4.2 call/apply —— 第一個參數是this
4.2.1 call/apply的作用
call和apply只有一個區別:call()方法接受的是若干個參數,apply()方法接受的是一個包含若干個參數的數組
作用:
- 調用函數
- 改變該函數的this指向
- 給函數傳遞參數
返回值
返回值是你調用的函數的返回值
window.a = 1
function print(b, c) {
console.log(this.a, b, c)
}
// 獨立調用
print(2, 3) // 1 2 3
// 使用call和apply
print.call({a: -1}, -2, -3) // -1 -2 -3
print.apply({a: 0}, [-2, -3]) // 0 -2 -3
4.2.2 apply傳遞數組參數
雖然apply傳遞的參數為數組,但實際上apply會將這個數組拆開再傳遞,因此函數接受到的參數是數組內的元素,而非一個數組
let fn = function () {
console.log(arguments)
}
fn.apply(null, [1, 2, [3, 4]]);
因此,apply經常性的作用之一就是將數組元素迭代為函數參數
例子一
Math.max()不接收數組的傳遞,因此如果想要找到一個長度很長的數組的最大值會非常麻煩
我們可以使用apply方法將數組傳遞給Math.max()
其本質還是將參數數組拆開再傳遞給Math.max()
let answer = Math.max.apply(null, [2, 4, 3])
console.log(answer) // 4
// 注意下面三個等價
Math.max.apply(null, [2, 4, 3])
Math.max.call(null, 2, 4, 3)
Math.max(2, 4, 3)
例子二:合併兩個數組
非常值得注意的就是arr2數組被拆開了,成了一個一個的參數
// 將第二個數組融合進第一個數組
// 相當於 arr1.push('celery', 'beetroot');
let arr1 = ['parsnip', 'potato']
let arr2 = ['celery', 'beetroot']
arr1.push.apply(arr1, arr2)
// 注意!!!this的意思是要指定調用了push這個方法
// 所以當 this = arr1 後
// 就成了 arr1 調用了 push方法
// 上述表達式等價於 arr1.push('celery', 'beetroot')
console.log(arr1)
// ['parsnip', 'potato', 'celery', 'beetroot']
當然,在使用apply的時候,也一定要注意是否要對this的指向進行綁定,否則可能會報錯
Math.max.apply(null, [2, 4, 3]) // 完美運行
arr1.push.apply(null, arr2) // 報錯 Uncaught TypeError: Array.prototype.push called on null or undefined
// 説明
Math = {
max: function (values) {
// 沒用到this值
}
}
Array.prototype.push = function (items) {
// this -> 調用push方法的數組本身
// this為null的話,就是向null裏push,會報錯
// Array.prototype.push called on null or undefined
}
// 下面三個值是完全等價的,因為this值已經是arr1
Array.prototype.push.apply(arr1, arr2)
arr1.push.apply(arr1, arr2)
arr2.push.apply(arr1, arr2)
4.2.3 小測試
function xx() {
console.log(this)
}
xx.call('1') // ??
xx() // ??
4.3 bind
fun.bind(thisArg[, arg1[, arg2[, ...]]])
4.3.1 作用
- 改變該函數的this指向
- 返回一個新函數
4.3.2 綁定函數與目標函數
函數使用bind()方法後會返回一個新函數【綁定函數】
而原函數為【目標函數】
我個人更喜歡用新函數和原函數來區分,因為新名詞越多,理解上的困難越大
那麼新函數被調用時會發生什麼呢?
下面一句話務必記住
其實就是把原函數call/apply一下,並指定你傳遞的this
function xx() {
console.log(this)
}
let foo = xx.bind({'name':'jason'})
// foo —— 新函數【綁定函數】
// xx —— 原函數【目標函數】
foo()
// 新函數調用時對原函數的操作如下(偽代碼)
function foo(){
xx.call({'name':'jason'})
}
也就是説,實際上,在foo()這一句執行時做了如下事情:
1.給xx(原函數)指定this
2.調用xx(原函數)
一定要注意這兩步是在新函數被調用時才發生,不調用不發生
你也可以總結為一句話:給原函數 call/apply 了一下
4.3.3 bind()傳參
- bind(this,arg1,arg2...)會將
arg1,arg2...插入到新函數【綁定函數】的arguments的開始位置 - 調用新函數時,再傳遞的參數也只會跟在
arg1,arg2...後面
function list() {
// 原函數【目標函數】
return Array.prototype.slice.call(arguments);
}
// 新函數【綁定函數】
let leadingThirtysevenList = list.bind(null, 37, 38);
let newList1 = leadingThirtysevenList();
let newList2 = leadingThirtysevenList(1, 2, 3);
let newList3 = leadingThirtysevenList(-1, -2);
console.log(newList1) // [ 37, 38 ]
console.log(newList2) // [ 37, 38, 1, 2, 3 ]
console.log(newList3) // [ 37, 38, -1, -2 ]
4.3.4 使用 this + call/apply原生實現一個bind【重點】
思考過程
實現bind其實就是實現bind的特點
- bind的第一個參數是this
- bind可以return一個新函數,這個新函數可以調用原函數並且可以指定其this,還可以接受參數
- 新函數傳遞的參數要在bind傳遞的參數的後面
代碼
Function.prototype._bind = function () {
// bind指定的this
let bindThis = arguments[0]
// bind傳遞的參數
let bindArgs = Array.prototype.slice.call(arguments, 1)
// this指向調用_bind的原函數,即舊函數
let oldFunction = this
// 返回的新函數
return function () {
// 所謂的新函數和舊函數,函數體一樣,實際上只是用apply調用了舊函數,再return舊函數的返回值
// 截獲調用新函數時傳遞的參數
let newArgs = Array.prototype.slice.call(arguments)
// 合併bindArgs和newArgs,並且newArgs要在bindArgs後面,再傳遞給舊函數
return oldFunction.apply(bindThis, bindArgs.concat(newArgs))
}
}
// 測試
function fn() {
console.log(arguments)
}
let newFn = fn._bind(null, 1, 2);
newFn(4, 6)