博客 / 詳情

返回

this揭秘

搞清楚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作為一個函數內部的對象,自然與函數緊密相連

然而,很多朋友都沒搞清楚函數名竟然是個指針

看下面的代碼,思考問題:

  1. fn1是否能夠直接找到其函數體的位置(ans:能)
  2. 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本質上就是指向它的調用者這句話該怎麼理解呢?

先別急,整個文章都是圍繞這句話展開的

首先,我們分析一下一個函數到底有幾種調用方式,再分別進行闡述:

  1. 全局調用
  2. 方法調用
  3. 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做了哪些事情

  1. 創立一個臨時對象,this指向該臨時對象
  2. 把實例的__proto__指向類的prototype
  3. return臨時對象
function fn() {
    console.log(this);
}

new fn(); // 結果看下面的截圖

image-20210127001039562

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概覽

  1. 我們要將call/apply歸為一類bind單獨歸為一類
  2. 三者的共同點是都可以指定this
  3. 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的作用

callapply只有一個區別:call()方法接受的是若干個參數,apply()方法接受的是一個包含若干個參數的數組
作用
  1. 調用函數
  2. 改變該函數的this指向
  3. 給函數傳遞參數

返回值
返回值是你調用的函數的返回值

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 作用

  1. 改變該函數的this指向
  2. 返回一個新函數

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()傳參

  1. bind(this,arg1,arg2...)會將arg1,arg2...插入到新函數【綁定函數】的arguments的開始位置
  2. 調用新函數時,再傳遞的參數也只會跟在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的特點
  1. bind的第一個參數是this
  2. bind可以return一個新函數,這個新函數可以調用原函數並且可以指定其this,還可以接受參數
  3. 新函數傳遞的參數要在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)
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.