js 五種綁定完全弄懂this,默認綁定、隱式綁定、顯式綁定、new綁定、箭頭函數綁定詳解

 壹 ❀ 引html

能夠說this與閉包、原型鏈同樣,屬於JavaScript開發中老生常談的問題了,百度一搜,this相關的文章鋪天蓋地。可開發好幾年,被幾道this題安排明明白白的人應該不在少數(我就是其一)。我以爲this概念抽象,變化無窮老是讓人暈頭轉向,但平心它並非有多難,今天咱們就從this綁定的五種場景(默認綁定、隱式綁定、顯式綁定、new綁定、箭頭函數綁定)出發,靜下心來好好聊聊這個 this,本文開始。面試

 貳 ❀ this默認綁定數組

this默認綁定咱們能夠理解爲函數調用時無任何調用前綴的情景,它沒法應對咱們後面要介紹的另外四種狀況,因此稱之爲默認綁定,默認綁定時this指向全局對象(非嚴格模式)閉包

function fn1() {
    let fn2 = function () {
        console.log(this); //window
        fn3();
    };
    console.log(this); //window
    fn2();
};

function fn3() {
    console.log(this); //window
};

fn1();

這個例子中不管函數聲明在哪,在哪調用,因爲函數調用時前面並未指定任何對象,這種狀況下this指向全局對象window。app

但須要注意的是,在嚴格模式環境中,默認綁定的this指向undefined,來看個對比例子:函數

function fn() {
    console.log(this); //window
    console.log(this.name);
};

function fn1() {
    "use strict";
    console.log(this); //undefined
    console.log(this.name);
};

var name = '聽風是風';

fn(); 
fn1() //TypeError: Cannot read property 'a' of undefined

再例如函數以及調用都暴露在嚴格模式中的例子:post

"use strict";
var name = '聽風是風';
function fn() {
    console.log(this); //undefined
    console.log(this.name);//報錯
};
fn();

最後一點,若是在嚴格模式下調用不在嚴格模式中的函數,並不會影響this指向,來看最後一個例子:性能

var name = '聽風是風';
function fn() {
    console.log(this); //indow
    console.log(this.name); //聽風是風
};

(function () {
    "use strict";
    fn();
}());

 叄 ❀ this隱式綁定this

 1.隱式綁定spa

什麼是隱式綁定呢,若是函數調用時,前面存在調用它的對象,那麼this就會隱式綁定到這個對象上,看個例子:

function fn() {
    console.log(this.name);
};
let obj = {
    name: '聽風是風',
    func: fn
};
obj.func() //聽風是風

若是函數調用前存在多個對象,this指向距離調用本身最近的對象,好比這樣:

function fn() {
    console.log(this.name);
};
let obj = {
    name: '行星飛行',
    func: fn,
};
let obj1 = {
    name: '聽風是風',
    o: obj
};
obj1.o.func() //行星飛行

那若是咱們將obj對象的name屬性註釋掉,如今輸出什麼呢?

function fn() {
    console.log(this.name);
};
let obj = {
    func: fn,
};
let obj1 = {
    name: '聽風是風',
    o: obj
};
obj1.o.func() //??

這裏輸出undefined,你們千萬不要將做用域鏈和原型鏈弄混淆了,obj對象雖然obj1的屬性,但它兩原型鏈並不相同,並非父子關係,因爲obj未提供name屬性,因此是undefined。

既然說到原型鏈,那咱們再來點花哨的,咱們再改寫例子,看看下面輸出多少:

function Fn() {};
Fn.prototype.name = '時間跳躍';

function fn() {
    console.log(this.name);
};

let obj = new Fn();
obj.func = fn;

let obj1 = {
    name: '聽風是風',
    o: obj
};
obj1.o.func() //?

這裏輸出時間跳躍,雖然obj對象並無name屬性,但順着原型鏈,找到了產生本身的構造函數Fn,因爲Fn原型鏈存在name屬性,因此輸出時間跳躍了。

番外------做用域鏈與原型鏈的區別:

當訪問一個變量時,解釋器會先在當前做用域查找標識符,若是沒有找到就去父做用域找,做用域鏈頂端是全局對象window,若是window都沒有這個變量則報錯。

當在對象上訪問某屬性時,首選i會查找當前對象,若是沒有就順着原型鏈往上找,原型鏈頂端是null,若是全程都沒找到則返一個undefined,而不是報錯。

 2.隱式丟失

在特定狀況下會存在隱式綁定丟失的問題,最多見的就是做爲參數傳遞以及變量賦值,先看參數傳遞:

var name = '行星飛行';
let obj = {
    name: '聽風是風',
    fn: function () {
        console.log(this.name);
    }
};

function fn1(param) {
    param();
};
fn1(obj.fn);//行星飛行

這個例子中咱們將 obj.fn 也就是一個函數傳遞進 fn1 中執行,這裏只是單純傳遞了一個函數而已,this並無跟函數綁在一塊兒,因此this丟失這裏指向了window。

第二個引發丟失的問題是變量賦值,其實本質上與傳參相同,看這個例子:

var name = '行星飛行';
let obj = {
    name: '聽風是風',
    fn: function () {
        console.log(this.name);
    }
};
let fn1 = obj.fn;
fn1(); //行星飛行

注意,隱式綁定丟失並非都會指向全局對象,好比下面的例子:

var name = '行星飛行';
let obj = {
    name: '聽風是風',
    fn: function () {
        console.log(this.name);
    }
};
let obj1 = {
    name: '時間跳躍'
}
obj1.fn = obj.fn;
obj1.fn(); //時間跳躍

雖然丟失了 obj 的隱式綁定,可是在賦值的過程當中,又創建了新的隱式綁定,這裏this就指向了對象 obj1。

 肆 ❀ this顯式綁定

顯式綁定是指咱們經過call、apply以及bind方法改變this的行爲,相比隱式綁定,咱們能清楚的感知 this 指向變化過程。來看個例子:

let obj1 = {
    name: '聽風是風'
};
let obj2 = {
    name: '時間跳躍'
};
let obj3 = {
    name: 'echo'
}
var name = '行星飛行';

function fn() {
    console.log(this.name);
};
fn(); //行星飛行
fn.call(obj1); //聽風是風
fn.apply(obj2); //時間跳躍
fn.bind(obj3)(); //echo

好比在上述代碼中,咱們分別經過call、apply、bind改變了函數fn的this指向。

在js中,當咱們調用一個函數時,咱們習慣稱之爲函數調用,函數處於一個被動的狀態;而call與apply讓函數從被動變主動,函數能主動選擇本身的上下文,因此這種寫法咱們又稱之爲函數應用

注意,若是在使用call之類的方法改變this指向時,指向參數提供的是null或者undefined,那麼 this 將指向全局對象。

let obj1 = {
    name: '聽風是風'
};
let obj2 = {
    name: '時間跳躍'
};
var name = '行星飛行';

function fn() {
    console.log(this.name);
};
fn.call(undefined); //行星飛行
fn.apply(null); //行星飛行
fn.bind(undefined)(); //行星飛行

另外,在js API中部分方法也內置了顯式綁定,以forEach爲例:

let obj = {
    name: '聽風是風'
};

[1, 2, 3].forEach(function () {
    console.log(this.name);//聽風是風*3
}, obj);

番外-----call、apply與bind有什麼區別?

1.call、apply與bind都用於改變this綁定,但call、apply在改變this指向的同時還會執行函數,而bind在改變this後是返回一個全新的boundFcuntion綁定函數,這也是爲何上方例子中bind後還加了一對括號 ()的緣由。

2.bind屬於硬綁定,返回的 boundFunction 的 this 指向沒法再次經過bind、apply或 call 修改;call與apply的綁定只適用當前調用,調用完就沒了,下次要用還得再次綁。

3.call與apply功能徹底相同,惟一不一樣的是call方法傳遞函數調用形參是以散列形式,而apply方法的形參是一個數組。在傳參的狀況下,call的性能要高於apply,由於apply在執行時還要多一步解析數組。

描述一請參照上面已有例子。

描述二請參照下方例子,咱們嘗試修改 boundFunction 的 this 指向:

let obj1 = {
    name: '聽風是風'
};
let obj2 = {
    name: '時間跳躍'
};
var name = '行星飛行';

function fn() {
    console.log(this.name);
};
fn.call(obj1); //聽風是風
fn(); //行星飛行
fn.apply(obj2); //時間跳躍
fn(); //行星飛行
let boundFn = fn.bind(obj1);//聽風是風
boundFn.call(obj2);//聽風是風
boundFn.apply(obj2);//聽風是風
boundFn.bind(obj2)();//聽風是風

描述三請參考如下例子:

let obj = {
    name: '聽風是風'
};

function fn(age,describe) {
    console.log(`我是${this.name},個人年齡是${age},我很是${describe}!`);
};
fn.call(obj,'26','帥');//我是聽風是風,個人年齡是26,我很是帥
fn.apply(obj,['26','帥']);//我是聽風是風,個人年齡是26,我很是帥

更多關於call apply bind能夠閱讀博主這篇文章 js中call、apply、bind到底有什麼區別?bind返回的方法還能修改this指向嗎?

 伍 ❀ new綁定

準確來講,js中的構造函數只是使用new 調用的普通函數,它並非一個類,最終返回的對象也不是一個實例,只是爲了便於理解習慣這麼說罷了。

那麼new一個函數究竟發生了什麼呢,大體分爲三步:

1.以構造器的prototype屬性爲原型,建立新對象;

2.將this(能夠理解爲上句建立的新對象)和調用參數傳給構造器,執行;

3.若是構造器沒有手動返回對象,則返回第一步建立的對象

這個過程咱們稱之爲構造調用,咱們來看個例子:

function Fn(){
    this.name = '聽風是風';
};
let echo = new Fn();
echo.name//聽風是風

在上方代碼中,構造調用建立了一個新對象echo,而在函數體內,this將指向新對象echo上(能夠抽象理解爲新對象就是this)。

若對於new具體過程有疑惑,或者不知道怎麼手動實現一個new 方法,能夠閱讀博主這篇文章 js new一個對象的過程,實現一個簡單的new方法

 陸 ❀ this綁定優先級

咱們先介紹前四種this綁定規則,那麼問題來了,若是一個函數調用存在多種綁定方法,this最終指向誰呢?這裏咱們直接先上答案,this綁定優先級爲:

顯式綁定 > 隱式綁定 > 默認綁定

new綁定 > 隱式綁定 > 默認綁定

爲何顯式綁定不和new綁定比較呢?由於不存在這種綁定同時生效的情景,若是同時寫這兩種代碼會直接拋錯,因此你們只用記住上面的規律便可。

function Fn(){
    this.name = '聽風是風';
};
let obj = {
    name:'行星飛行'
}
let echo = new Fn().call(obj);//報錯 call is not a function

那麼咱們結合幾個例子來驗證下上面的規律,首先是顯式大於隱式:

//顯式>隱式
let obj = {
    name:'行星飛行',
    fn:function () {
        console.log(this.name);
    }
};
obj1 = {
    name:'時間跳躍'
};
obj.fn.call(obj1);// 時間跳躍

其次是new綁定大於隱式:

//new>隱式
obj = {
    name: '時間跳躍',
    fn: function () {
        this.name = '聽風是風';
    }
};
let echo = new obj.fn();
echo.name;//聽風是風

 柒 ❀ 箭頭函數的this

ES6的箭頭函數是另類的存在,爲何要單獨說呢,這是由於箭頭函數中的this不適用上面介紹的四種綁定規則

準確來講,箭頭函數中沒有this,箭頭函數的this指向取決於外層做用域中的this,外層做用域或函數的this指向誰,箭頭函數中的this便指向誰。有點吃軟飯的嫌疑,一點都不硬朗,咱們來看個例子:

function fn() {
    return () => {
      console.log(this.name);
    };
  }
  let obj1 = {
    name: '聽風是風'
  };
  let obj2 = {
    name: '時間跳躍'
  };
  let bar = fn.call(obj1); // fn this指向obj1
  bar.call(obj2); //聽風是風

爲啥咱們第一次綁定this並返回箭頭函數後,再次改變this指向沒生效呢?

前面說了,箭頭函數的this取決於外層做用域的this,fn函數執行時this指向了obj1,因此箭頭函數的this也指向obj1。除此以外,箭頭函數this還有一個特性,那就是一旦箭頭函數的this綁定成功,也沒法被再次修改,有點硬綁定的意思。

 捌 ❀ 總

那麼到這裏,對於this的五種綁定場景就所有介紹完畢了,若是你有結合例子練習下來,我相信你如今對於this的理解必定更上一層樓了。

那麼經過本文,咱們知道默認綁定在嚴格模式與非嚴格模式下this指向會有所不一樣。

咱們知道了隱式綁定與隱式丟失的幾種狀況,並簡單複習了做用域鏈與原型鏈的區別。

相對隱式綁定改變的不可見,咱們還介紹了顯式綁定以及硬綁定,簡單科普了call、apply與bind的區別,並提到當綁定指向爲null或undefined時this會指向全局(非嚴格模式)。

咱們介紹了new綁定以及new一個函數會發生什麼。

最後咱們瞭解了不太合羣的箭頭函數中的this綁定,瞭解到箭頭函數的this由外層函數this指向決定,並有一旦綁定成功也沒法再修改的特性。

但願在面試題中遇到this的你再也不有所畏懼,到這裏,本文結束。

 參考

你不知道的js中關於this綁定機制的解析[看完還不懂算我輸]

JavaScript深刻之史上最全--5種this綁定全面解析

this、apply、call、bind

相關文章
相關標籤/搜索