一文理解 this、call、apply、bind

文章首發於 我的博客

導讀

導圖

this

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

  1. 普通函數指向函數的調用者:有個簡便的方法就是看函數前面有沒有點,若是有點,那麼就指向點前面的那個值;
  2. 箭頭函數指向函數所在的所用域: 注意理解做用域,只有函數的{}構成做用域,對象的{}以及 if(){}都不構成做用域;

當時對this的內部原理什麼的都理解的不是很深入,就只能憑藉遇到不少坑以後,總結了出了那時候本身用來判斷的標準。這裏會再次略微深刻的說一下。思路仍是圍繞上面總結的那兩句話。git

普通函數調用

  1. 默認綁定
var a = 'luckyStar';
function foo() {
    console.log(this.a);
}
foo();
// luckyStar

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

  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函數,調用的時候,沒有調用者,因此使用的是默認綁定規則。面試

位置②和位置 位置③ 的必定要注意。c#

  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指向函數所在的所用域。箭頭函數不能做爲構造函數app

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是誰的話,能夠直接設置爲 nullpost

不一樣點:

  • 函數調用 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天。

相關文章
相關標籤/搜索