「前端料包」一文完全搞懂JavaScript中的this、call、apply和bind

前言

關於JavaScript中this又是一個老生常談的話題,也是面試繞不開的經典考題。「前端料包」 系列第三篇就聊聊關於this的話題。寫的很小白,若有不對,歡迎各路大佬指正~ javascript

在講this以前,先得說說環境 這個概念。一門語言在運行的時候,須要一個環境,叫作宿主環境。對於JavaScript,宿主環境最多見的是web瀏覽器,另外一個最爲常見的就是 Node 了,一樣做爲宿主環境,node 也有本身的 JavaScript 引擎:V8(目前最快JavaScript引擎、Google生產)。關於node中的this本文不作展開。往後更新~html

this的初衷

this設計的初衷是在函數內部使用,用來指代當前的運行環境。爲何這麼說呢?前端

JavaScript中的對象的賦值行爲是將地址賦給一個變量,引擎在讀取變量的時候其實就是要了個地址而後再從原始地址中讀取對象。而JavaScript 容許函數體內部引用當前環境的其餘變量,而這個變量是由運行環境提供的。因爲函數又能夠在不一樣的運行環境執行(如全局做用域內執行,對象內執行...),因此須要一個機制來代表代碼到底在哪裏執行!因而this出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境。java

global this

在瀏覽器裏,在全局範圍內:node

  1. this等價於window對象;
  2. 用var聲明一個變量和給this或者window添加屬性是等價的;
  3. 若是你在聲明一個變量的時候沒有使用var或者let、const(es6),你就是在給全局的this添加或者改變屬性值。
// 1
console.log(this === window); //true
//2
var name = "Jake";
console.log(this.name ); // "Jake"
console.log(window.name ); // "Jake"

//3
 age = 23;
 function testThis() {
   age = 18;
 }
 console.log(this.age ); // 23
 testThis();
 console.log(this.age ); // 18

複製代碼

總結起來就是:在全局範圍內this是大哥大,它等價於window對象(即指向window),若是你聲明一些全局變量(無論在任何地方),這些變量都會做爲this的屬性。es6

function this

對於函數中的this的指向問題,有一句話很好用:運行時this永遠指向最後調用它的那個對象web

舉一個栗子面試

var name = "windowsName";
function sayName() {
var name = "Jake";
console.log(this.name);   // windowsName
console.log(this);    // Window
}
sayName();
console.log(this) // Window
複製代碼

咱們看最後調用 sayName的地方 sayName();,前面沒有調用的對象那麼就是全局對象 window,這就至關因而 window.sayName()segmentfault

須要注意的是,對於嚴格模式來講,默認綁定全局對象是不合法的,this被置爲undefined。會報錯 Uncaught TypeError: Cannot read property 'name' of undefined。windows

再看下面這個栗子

function foo() {
    console.log( this.age );
}

var obj1 = {
    age : 23,
    foo: foo
};

var obj2 = {
    age : 18,
    obj1: obj1
};

obj2.obj1.foo(); // 23

複製代碼

仍是開頭的那句話,最後調用foo()的是obj1,因此this指向obj1,輸出23。

構造函數中的this

所謂構造函數,就是經過這個函數生成一個新對象(object)。當一個函數做爲構造器使用時(經過 new 關鍵字), 它的 this 值綁定到新建立的那個對象。若是沒使用 new 關鍵字, 那麼他就只是一個普通的函數, this 將指向 window 對象。

這又是另外一個經典話題:new 的過程

var a = new Foo("zhang","jake");

new Foo{
    var obj = {};
    obj.__proto__ = Foo.prototype;
    var result = Foo.call(obj,"zhang","jake");
    return typeof result === 'obj'? result : obj;
}
複製代碼

若執行 new Foo(),過程以下: 1)建立新對象 obj;

2)給新對象的內部屬性賦值,構造原型鏈(將新對象的隱式原型指向其構造函數的顯示原型);

3)執行函數 Foo,執行過程當中內部 this 指向新建立的對象 obj(這裏使用了call改變this指向);

4)若是 Foo 內部顯式返回對象類型數據,則返回該數據,執行結束;不然返回新建立的對象 obj。

var name = "Jake";jiuzhixiang
function testThis(){
  this.name = 'jakezhang';
  this.sayName = function () {
		return this.name;
	}
}
console.log(this.name ); // Jake

new testThis(); 
console.log(this.name ); // Jake

var result = new testThis();
console.log(result.name ); // jakezhang
console.log(result.sayName()); // jakezhang

testThis();  
console.log(this.name ); // jakezhang
複製代碼

很顯然,誰被new了,this就指向誰。

class中的this

本小節摘自阿里大神Nealyang的文章---->【THE LAST TIME】this:call、apply、bind

在es6中,類,是 JavaScript 應用程序中很是重要的一個部分。類一般包含一個 constructor , this能夠指向任何新建立的對象。 不過在做爲方法時,若是該方法做爲普通函數被調用, this也能夠指向任何其餘值。與方法同樣,類也可能失去對接收器的跟蹤。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();

複製代碼

構造函數裏的 this指向新建立的 類實例。當咱們調用 batman.dialogue()時, dialogue()做爲方法被調用, batman是它的接收器。 可是若是咱們將 dialogue()方法的引用存儲起來,並稍後將其做爲函數調用,咱們會丟失該方法的接收器,此時 this參數指向 undefined 。

const say = batman.dialogue;
say();

複製代碼

出現錯誤的緣由是JavaScript 類是隱式的運行在嚴格模式下的。咱們是在沒有任何自動綁定的狀況下調用 say()函數的。要解決這個問題,咱們須要手動使用 bind()將 dialogue()函數與 batman綁定在一塊兒。

const say = batman.dialogue.bind(batman);
say();

複製代碼

call、apply和bind中的this

call、apply、bind 被稱之爲 this 的強綁定,用來改變函數執行時的this指向,目前全部關於它們的運用,都是基於這一點來進行的。

var name = 'zjk';
  function fun() {
  console.log (this.name);
}

var obj= {
  name: 'jake'
};
fun(); // zjk
fun.call(obj); //Jake

複製代碼

上面的fun.ccall(obj)等價於fun.capply(obj)fun.cbind(obj)()

箭頭函數中的this

es5中的this要看函數在什麼地方調用(即要看運行時),經過誰是最後調用它該函數的對象來判斷this指向。但es6的箭頭函數中沒有 this 綁定,必須經過查找做用域鏈來決定其值,若是箭頭函數被非箭頭函數包含,則 this 綁定的是最近一層非箭頭函數的 this,不然,this 爲 undefined。箭頭函數的 this 始終指向函數定義時的 this,而非執行時。

let name = "zjk";

    let o = {
        name : "Jake",

        sayName: function () {
            console.log(this.name)     
        },

        func: function () {
            setTimeout( () => {
                this.sayName()
            },100);
        }

    };

    o.func()     // Jake
複製代碼

使用 call 、 apply或 bind等方法給 this傳值,箭頭函數會忽略。箭頭函數引用的是箭頭函數在建立時設置的 this值。

let obj = {
  name: "Jake",
  func: (a,b) => {
      console.log(this.name,a,b);
  }
};
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);// 1 2

複製代碼

最後放一道常見的this面試題

var number = 1;

var obj = {

	number:2,

	showNumber:function(){

	this.number = 3;

	(function(){

	console.log(this.number);

})();

	console.log(this.number);

}

};

obj.showNumber();// 答案就歡迎留在評論區囖~

複製代碼

call & apply

每一個函數都包含兩個非繼承而來的方法:apply()和 call()。這兩個方法的用途都是在特定的做用域中調用函數,實際上等於設置函數體內 this 對象的值。

apply()

apply()方法接收兩個參數:一個是在其中運行函數的做用域,另外一個是參數數組。其中,第二個參數能夠是 Array 的實例,也能夠是arguments 對象。

function sum(num1, num2){ 
 return num1 + num2; 
} 
function callSum1(num1, num2){ 
 return sum.apply(this, arguments); // 傳入 arguments 對象
} 
function callSum2(num1, num2){ 
 return sum.apply(this, [num1, num2]); // 傳入數組
} 
console.log(callSum1(10,10)); //20
console.log(callSum2(10,10)); //20
複製代碼

在嚴格模式下,未指定環境對象而調用函數,則 this 值不會轉型爲 window。除非明確把函數添加到某個對象或者調用 apply()或 call(),不然 this 值將是undefined。

call()

call()方法與 apply()方法的做用相同,它們的惟一區別在於接收參數的方式不一樣。在使用call()方法時,傳遞給函數的參數必須逐個列舉出來。

function sum(num1, num2){ 
 return num1 + num2; 
}
function callSum(num1, num2){ 
 return sum.call(this, num1, num2); 
} 
console.log(callSum(10,10)); //20
複製代碼

call()方法與 apply()方法返回的結果是徹底相同的,至因而使用 apply()仍是 call(),徹底取決於你採起哪一種給函數傳遞參數的方式最方便。

  • 參數數量/順序肯定就用call,參數數量/順序不肯定的話就用apply。
  • 考慮可讀性:參數數量很少就用call,參數數量比較多的話,把參數整合成數組,使用apply。

bind()

bind()方法會建立一個函數的實例,其 this 值會被綁定到傳給 bind()函數的值。意思就是 bind() 會返回一個新函數。例如:

window.color = "red"; 
var o = { color: "blue" }; 
function sayColor(){ 
 alert(this.color); 
} 
var objectSayColor = sayColor.bind(o); 
objectSayColor(); //blue
複製代碼

call/apply與bind的區別

執行:

  • call/apply改變了函數的this上下文後立刻執行該函數
  • bind則是返回改變了上下文後的函數,不執行該函數
function add (a, b) {
    return a + b;
}

function sub (a, b) {
    return a - b;
}

add.bind(sub, 5, 3); // 這時,並不會返回 8
add.bind(sub, 5, 3)(); // 調用後,返回 8

複製代碼

返回值:

  • call/apply 返回fun的執行結果
  • bind返回fun的拷貝,並指定了fun的this指向,保存了fun的參數。

call/apply/bind的核心理念

從上面幾個簡單的例子能夠看出call/apply/bind是在向其餘對象借用方法,這也符合咱們的正常思惟,舉個簡單的栗子。 我和我高中一個同窗玩的超級好,衣服鞋子都是共穿的,去買衣服的時候,他買衣服,我買鞋子;回來後某天我想穿他買的衣服了,可是我沒有,因而我就借用他的穿。這樣我就既達到了穿新衣服的目的,又節省了money~ A對象有個方法,B對象由於某種緣由也須要用到一樣的方法,這時候就可讓B借用 A 對象的方法啦,既達到了目的,又節省了內存。

這就是call/apply/bind的核心理念:借。

call/apply/bind的應用場景

關於call/apply/bind的用法因篇幅有限就不作展開了,能夠看看下面這篇,我的以爲寫得超級棒!

「乾貨」細說 call、apply 以及 bind 的區別和用法

手寫實現apply、call、bind

apply

一、先給Function原型上擴展個方法並接收2個參數,

Function.prototype.myApply = function (context, args) {}
複製代碼

二、由於不傳context的話,this會指向window,因此這裏將context和args作一下容錯處理。

Function.prototype.myApply = function (context, args) { 
    // 處理容錯
    context = (typeof context === 'object' ? context : window)
    args = args ? args : []
}
複製代碼

三、使用隱式綁定去實現顯式綁定

Function.prototype.myApply = function (context, args) {
    // 處理容錯
   context = (typeof context === 'object' ? context : window)
   args = args ? args : []
    //給context新增一個獨一無二的屬性以避免覆蓋原有屬性
    const key = Symbol()
    context[key] = this
    //經過隱式綁定的方式調用函數
    context[key](...args)
}
複製代碼

四、最後一步要返回函數調用的返回值,而且把context上的屬性刪了纔不會形成影響

Function.prototype.myApply = function (context, args) {
   // 處理容錯
    context = (typeof context === 'object' ? context : window)
    args = args ? args : []
    //給context新增一個獨一無二的屬性以避免覆蓋原有屬性
    const key = Symbol();
    context[key] = this;
    //經過隱式綁定的方式調用函數
    const result = context[key](...args);
    //刪除添加的屬性
    delete context[key]
    //返回函數調用的返回值
    return result
}
複製代碼

這樣一個乞丐版的apply就實現了,至於優化,網上有不少大牛寫的很好,能夠去找找,這裏就不作繼續優化了。

驗證走一波~

function fun(...args) {
  console.log(this.name,...args)
}
const result = { 
name: 'Jake' 
}
// 參數爲數組;方法當即執行
fun.myApply (result, [1, 2])

複製代碼

結果以下,說明已經實現了apply方法。

在這裏插入圖片描述

call

call的實現幾乎和apply如出一轍,就直接上代碼了。

//傳遞參數從一個數組變成逐個傳參了,不用...擴展運算符的也能夠用arguments代替
Function.prototype.NealCall = function (context, ...args) {
    //這裏默認不傳就是給window,也能夠用es6給參數設置默認參數
     context = (typeof context === 'object' ? context : window)
    args = args ? args : []
    //給context新增一個獨一無二的屬性以避免覆蓋原有屬性
    const key = Symbol();
    context[key] = this;
    //經過隱式綁定的方式調用函數
    const result = context[key](...args);
    //刪除添加的屬性
    delete context[key];
    //返回函數調用的返回值
    return result;
}

複製代碼

bind

bind的實現要稍微麻煩一點,由於bind是返回一個綁定好的函數,apply是直接調用.但其實簡單來講就是返回一個函數,裏面執行了apply上述的操做而已.不過有一個須要判斷的點,由於返回新的函數,要考慮到使用new去調用,而且new的優先級比較高,因此須要判斷new的調用,還有一個特色就是bind調用的時候能夠傳參,調用以後生成的新的函數也能夠傳參,效果是同樣的,因此這一塊也要作處理。

Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存儲源函數以及上方的params(函數參數)
    // 對返回的函數 secondParams 二次傳參
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是不是fToBind的實例 也就是返回的fToBind是否經過new調用
        const context = isNew ? this : Object(objThis) // new調用就綁定到this上,不然就綁定到傳入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call調用源函數綁定this的指向並傳遞參數,返回執行結果
    };
    if (thisFn.prototype) {
        // 複製源函數的prototype給fToBind 一些狀況下函數沒有prototype,好比箭頭函數
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷貝的函數
};

複製代碼

總結

  1. 在瀏覽器裏,在全局範圍內this 指向window對象;
  2. 在函數中,this永遠指向最後調用他的那個對象;
  3. 構造函數中,this指向new出來的那個新的對象;
  4. call、apply、bind中的this被強綁定在指定的那個對象上;
  5. 箭頭函數中this比較特殊,箭頭函數this爲父做用域的this,不是調用時的this.要知道前四種方式,都是調用時肯定,也就是動態的,而箭頭函數的this指向是靜態的,聲明的時候就肯定了下來;
  6. apply、call、bind都是js給函數內置的一些API,調用他們能夠爲函數指定this的執行,同時也能夠傳參。

最後放一張圖來幫助記憶

後話

說來慚愧啊,剛學JS的時候我寫過一篇關於this的學習筆記,好像2個小時就寫完了,本覺得一天就能寫完這篇,結果前先後後寫了好幾天,寫以前也看了幾篇各路大佬寫的,我下面都貼了連接,我只能感嘆寫的是真的好啊!不過這幾天下來仍是對this這個知識點有了新的的認識。另外,小生乃前端小白一枚,寫文章的最初衷是爲了讓本身對該知識點有更深入的印象和理解,寫的東西也很小白,文中若有不對,歡迎指正~ 而後就是但願看完的朋友能夠點個喜歡,也能夠關注一波~ 我會持續輸出!

我的博客連接

CSDN我的主頁

掘金我的主頁

簡書我的主頁

參考文章

紅寶書第五章

JavaScript 的 this 原理

【THE LAST TIME】this:call、apply、bind

JavaScript中的this陷阱的最全收集--沒有之一

詳解 JS 中 new 調用函數原理

js基礎-面試官想知道你有多理解call,apply,bind?[不看後悔系列]

相關文章
相關標籤/搜索