一文理解 this、call、apply、bind

木子星兮發表於2020-03-17

文章首發於個人部落格

導讀

導圖

this

記得差不多在兩年多之前寫過一篇文章 兩句話理解js中的this,當時總結的兩句話原話是這樣的:

  1. 普通函式指向函式的呼叫者:有個簡便的方法就是看函式前面有沒有點,如果有點,那麼就指向點前面的那個值;
  2. 箭頭函式指向函式所在的所用域: 注意理解作用域,只有函式的{}構成作用域,物件的{}以及 if(){}都不構成作用域; 當時對this的內部原理什麼的都理解的不是很深刻,就只能憑藉遇到很多坑之後,總結了出了那時候自己用來判斷的標準。這裡會再次略微深入的說一下。思路還是圍繞上面總結的那兩句話。

普通函式呼叫

  1. 預設繫結
var a = 'luckyStar';
function foo() {
    console.log(this.a);
}
foo();
// luckyStar
複製程式碼

foo()直接呼叫非嚴格模式下是this是指向 window上的,嚴格模式 this 指向的是undefined;

  1. 隱式繫結
var a = 'luckyStar';
var obj = {
    a: 'litterStar',
    foo() {
        console.log(this.a);
    }
}
obj.foo(); // ①
// litterStar

var bar = obj.foo; 
bar(); // ②
// luckyStar 

setTimeout(obj.foo, 100); // ③
// luckyStar 
複製程式碼

位置①,obj.foo(),是obj通過.運算子呼叫了 foo(),所以指向的值 obj。

位置②,是把 obj.foo賦值給了 bar,實際上是把 foo函式賦值給了bar, bar() 呼叫的時候,沒有呼叫者,所以使用的是預設繫結規則。

位置③,是把 obj.foo賦值給了 setTimeout,實際上呼叫的還是 foo函式,呼叫的時候,沒有呼叫者,所以使用的是預設繫結規則。

位置②和位置 位置③ 的一定要注意。

  1. 顯式繫結
function foo() {
    console.log(this.name);
}
const obj = {
    name: 'litterStar'
}
const bar = function() {
    foo.call(obj);
}
bar();
// litterStar
複製程式碼

使用 call,apply可以顯式修改 this的指向,下面會詳細介紹該部分。

  1. new 繫結
function Foo(name) {
    this.name = name;
}
var luckyStar = new Foo('luckyStar');
luckyStar.name; 
// luckyStar
複製程式碼

要解釋上面的結果就要從 new 的過程說起了

  1. 建立一個新的空物件 obj
  2. 將新物件的的原型指向當前函式的原型
  3. 新建立的物件繫結到當前this上
  4. 如果沒有返回其他物件,就返回 obj,否則返回其他物件
function _new(constructor, ...arg) {
    // ① 建立一個新的空物件 obj
    const obj = {};
    // ② 將新物件的的原型指向當前函式的原型
    obj.__proto__ = constructor.prototype;
    // ③ 新建立的物件繫結到當前this上
    const result = constructor.apply(obj, arg); 
    // ④ 如果沒有返回其他物件,就返回 obj,否則返回其他物件
    return typeof result === 'object' ? result : obj;
}
function Foo(name) {
    this.name = name;
}
var luckyStar = _new(Foo, 'luckyStar');
luckyStar.name; //luckyStar
複製程式碼

箭頭函式呼叫

箭頭函式中其實沒有 this 繫結,因為箭頭函式中this指向函式所在的所用域。箭頭函式不能作為建構函式

const obj = {
    name: 'litterStar',
    say() {
        console.log(this.name);
    },
    read: () => {
        console.log(this.name);
    }
}
obj.say(); // litterStar
obj.read(); // undefined
複製程式碼

call,apply,bind

call,apply,bind 這三個函式是 Function原型上的方法 Function.prototype.call()Function.prototype.applyFunction.prototype.bind(),所有的函式都是 Funciton 的例項,因此所有的函式可以呼叫call,apply,bind 這三個方法。

call,apply,bind 在用法上的異同

相同點:

call,apply,bind 這三個方法的第一個引數,都是this。如果你使用的時候不關心 this是誰的話,可以直接設定為 null

不同點:

  • 函式呼叫 call,apply方法時,返回的是呼叫函式的返回值。
  • 而bind是返回一個新的函式,你需要再加一個小括號來呼叫。
  • call和apply的區別就是,call接受的是一系列引數,而apply接受的是一個陣列。

但是有了 ES6引入的 ...展開運算子,其實很多情況下使用 call和apply沒有什麼太大的區別。

舉個例子,找到陣列中最大的值

const arr = [1, 2, 3, 5];
Math.max.call(null, ...arr);
Math.max.apply(null, arr);
複製程式碼

Math.max 是數字的方法,陣列上並沒有,但是我們可以通過 call, apply 來使用 Math.max 方法來計算當前陣列的最大值。

手寫 call,apply,bind

實現一個call:

  • 如果不指定this,則預設指向window
  • 將函式設定為物件的屬性
  • 指定this到函式並傳入給定引數執行函式
  • 執行&刪除這個函式,返回函式執行結果
Function.prototype.myCall = function(thisArg = window) {
    // thisArg.fn 指向當前函式 fn (fn.myCall)
    thisArg.fn = this;
    // 第一個引數為 this,所以要取剩下的引數
    const args = [...arguments].slice(1);
    // 執行函式
    const result = thisArg.fn(...args);
    // thisArg上並不存在fn,所以需要移除
    delete thisArg.fn;
    return result;
}

function foo() {
    console.log(this.name);
}
const obj = {
    name: 'litterStar'
}
const bar = function() {
    foo.myCall(obj);
}
bar();
// litterStar
複製程式碼

實現一個apply 過程很call類似,只是引數不同,不再贅述

Function.prototype.myApply = function(thisArg = window) {
    thisArg.fn = this;
    let result;
    // 判斷是否有第二個引數
    if(arguments[1]) {
        // apply方法呼叫的時候第二個引數是陣列,所以要展開arguments[1]之後再傳入函式
        result = thisArg.fn(...arguments[1]);
    } else {
        result = thisArg.fn();
    }
    delete thisArg.fn;
    return result;
}

function foo() {
    console.log(this.name);
}
const obj = {
    name: 'litterStar'
}
const bar = function() {
    foo.myApply(obj);
}
bar();
// litterStar
複製程式碼

實現一個bind

MDN上的解釋:bind() 方法建立一個新的函式,在 bind() 被呼叫時,這個新函式的 this 被指定為 bind() 的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。

Function.prototype.myBind = function(thisArg) {
    // 儲存當前函式的this
    const fn = this;
    // 儲存原先的引數
    const args = [...arguments].slice(1);
    // 返回一個新的函式
    return function() {
        // 再次獲取新的引數
        const newArgs = [...arguments];
        /**
         * 1.修改當前函式的this為thisArg
         * 2.將多次傳入的引數一次性傳入函式中
        */
        return fn.apply(thisArg, args.concat(newArgs))
    }
}

const obj1 = {
    name: 'litterStar',
    getName() {
        console.log(this.name)
    }
}
const obj2 = {
    name: 'luckyStar'
}

const fn = obj1.getName.myBind(obj2)
fn(); // luckyStar
複製程式碼

手寫部分的程式碼大部分參考了網上比較多的一些寫法。手寫程式碼的前提是一定要搞清楚這個函式是什麼,怎麼用,幹了什麼。

重要參考

其他

最近發起了一個100天前端進階計劃,主要是深挖每個知識點背後的原理,歡迎關注 微信公眾號「牧碼的星星」,我們一起學習,打卡100天。

一文理解 this、call、apply、bind

相關文章