天天看點

一文了解 this、call、apply、bind

文章首發于個人部落格

導讀

一文了解 this、call、apply、bind

導圖

this

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

  1. 普通函數指向函數的調用者:有個簡便的方法就是看函數前面有沒有點,如果有點,那麼就指向點前面的那個值;
  2. 箭頭函數指向函數所在的所用域: 注意了解作用域,隻有函數的

    {}

    構成作用域,對象的

    {}

    以及

    if(){}

    都不構成作用域;

    當時對this的内部原理什麼的都了解的不是很深刻,就隻能憑借遇到很多坑之後,總結了出了那時候自己用來判斷的标準。這裡會再次略微深入的說一下。思路還是圍繞上面總結的那兩句話。

普通函數調用

  1. 預設綁定var a = 'luckyStar';

    function foo() {

    console.log(this.a);

    }

    foo();

    // luckyStarfoo()直接調用非嚴格模式下是this是指向 window上的,嚴格模式 this 指向的是undefined;

  2. 隐式綁定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的指向,下面會詳細介紹該部分。

  2. new 綁定function Foo(name) {

    this.name = name;

    }

    var luckyStar = new Foo('luckyStar');

    luckyStar.name;

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

  3. 建立一個新的空對象 obj
  4. 将新對象的的原型指向目前函數的原型
  5. 新建立的對象綁定到目前this上
  6. 如果沒有傳回其他對象,就傳回 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.apply

Function.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           

複制

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

重要參考

  • 你不知道的JavaScript(上卷)
  • 不能使用call,apply,bind,如何用js實作call或者apply的功能?
  • JavaScript深入之bind的模拟實作
  • 「中進階前端面試」JavaScript手寫代碼無敵秘籍
  • 22 道高頻 JavaScript 手寫面試題及答案
  • MDN上bind函數的Polyfill