JavaScript之你不知道的this

很重要的一句話

只有深諳了this,你纔有可能用 JavaScript 建立相似谷歌地圖這樣大型的複雜應用bash


1、這篇文章出現的背景

1. this在咱們開發過程當中的重要性(開發場景) -- 經過一段代碼簡單瞭解this

提供了一種更優雅的方式來隱式」傳遞」一個對象引用, 讓API設計更加簡潔和清晰app

首先來看一段代碼, 此處不使用this, 須要給identify()和speak()顯示的傳入一個上下文對象:ide

// 定義 you & me對象
var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting );
}

identify( you ); // READER
speak( me ); //hello, 我是 KYLE
複製代碼

使用this解決: 能夠在不一樣的上下文對象(me 和 you)中重複使用函數identify()和speak()函數

function identify() {
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I'm " + identify.call( this );
    console.log( greeting );
}

identify.call( me ); // KYLE
identify.call( you ); // READER

speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
複製代碼

顯然, 隨着你的使用模式愈來愈複雜, 顯式傳遞上下文對象會讓代碼變得愈來愈混亂, this可讓你的代碼變得更優雅。特別是當你使用對象(關聯)和原型時, 利用this使得函數能夠自動引用合適的上下文對象顯的尤其重要oop

2.兩種錯誤的理解

  • this指向函數自身
  • this指向函數的做用域, 這個在某些狀況下是正確的, 可是在其餘狀況下確實錯誤的

事實上, 一部分人認爲"this既不指向函數自身也不指向函數的詞法做用域", 可是也是不對的, 在某種狀況下, this就指向函數自身, 也可能指向詞法做用域ui

3.本質

this是在運行(函數被調用)時發生綁定的,並非在編寫時綁定, 它的上下文取決於函數調用時的各類條件,它指向什麼徹底取決於函數在哪裏被調用this


2、this 綁定規則 & 優先級

簡單來講, 有這大體四種spa

  1. 由new調用(new綁定)
  2. 函數是否經過call、apply(顯式綁定)或者硬綁定調用
  3. 函數是否在某個上下文對象中調用(隱式綁定)
  4. 默認綁定

1. 默認綁定

沒法應用其餘規則時的默認規則, 嚴格模式下綁定到undefined, 不然綁定到全局對象設計

最經常使用的函數調用類型:獨立函數調用code

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

var a = 2;
foo(); // 2
複製代碼

代碼中, foo()是直接使用不帶任何修飾的函數引用進行調用的,只能適用於this的默認綁定,沒法應用其餘規則,所以this指向全局對象

// 嚴格模式下
function foo() {
    "use strict";
    console.log( this.a );
}

var a = 2;
foo(); // TypeError: this is undefined
複製代碼

因此, 不推薦這種寫法。

2. 隱式綁定

考慮調用位置是否有上下文對象,或者說是否被某個對象或者包含

當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象

必須在一個對象內部包含一個指向函數的屬性,並經過這個屬性間接引用函數,從而把 this 間接(隱式)綁定到這個對象上

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2
複製代碼

對象屬性引用鏈中只有最後一層會影響調用位置, 即調用棧的末端

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42
複製代碼

3. 顯示綁定

強制指定某些對象對函數進行調用,this則強制指向調用函數的對象

  • call(thisObj, arg1, arg2, arg3...)
  • apply(thisObj, argArr)
  • ES5 中提供了內置的方法 硬綁定bind(thisObj)

顯示綁定場景

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};

foo.call( obj ); // 2
複製代碼

硬綁定經常使用場景

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a:2
};

var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
複製代碼

4. new綁定

new方式優先級最高,只要是使用new方式來調用一個構造函數,this必定會指向new調用函數新建立的對象

function foo(a) {
    this.a = a;
}
var bar = new foo(2);

console.log( bar.a ); // 2
複製代碼

3、綁定例外

1. 箭頭函數

,實際緣由是箭頭函數根本沒有本身的this

this指向的固定化,並非由於箭頭函數內部有綁定this的機制, 實際緣由箭頭函數沒有本身的this,它的this是繼承而來,默認指向在定義它時所處的對象(宿主對象)。 捕獲其所在(即定義的位置)上下文的this值,做爲本身的this值, 若是在當前的箭頭函數做用域中找不到變量,就像上一級做用域裏去找, 致使內部的this就是外層代碼塊的this

// demo 1
function foo() {
	 setTimeout(() => {
	    console.log('id:', this.id);
	  }, 100);
}
var id = 21;
foo.call({ id: 42 }) // id: 42

//demo 2
function Person() {
    this.name = 'dog';
    this.age = '18';
    setTimeout( () => {
        console.log(this);
        console.log('my name:' + this.name + '& my age:' + this.age)
    }, 1000)
}
var p = Person();

複製代碼

2. 被忽略的this

當被綁定的是null,則使用的是默認綁定規則

// 若是你把 null 或者 undefined 做爲 this 的綁定對象傳入 call 、 apply 或者 bind ,這些值在調用時會被忽略,
實際應用的是默認綁定規則
function foo() {
	console.log( this.a );
}
var a = 2222;
foo.call( null ); // 2222
複製代碼

4、(隱式)綁定丟失

最多見的this綁定問題就是被隱式綁定的函數會丟失綁定對象,也是就說它會應用默認綁定,從而把this綁定到全局對象或者undefined上,取決因而否是嚴格模式

1. 引用賦值丟失

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a是全局對象的屬性

bar(); // "oops, global"
複製代碼

雖然 bar 是 obj.foo 的一個引用,可是實際上,它引用的是 foo 函數自己,這就至關於:var bar = foo, obj對象只是一箇中間橋樑, obj.foo只起到傳遞函數的做用,因此bar跟obj對象沒有任何關係,此時的 bar() 實際上是一個不帶任何修飾的函數調用. 而bar自己又不帶a屬性,所以應用了默認綁定,最後a只能指向window.

2. 傳參丟失

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    // fn其實引用的是foo
    fn(); // <-- 調用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局對象的屬性

doFoo( obj.foo ); // "oops, global"
複製代碼

參數傳遞其實就是一種隱式賦值,所以咱們傳入函數時也會被隱式賦值,因此結果和上一 個例子同樣

3. 回調函數丟失

function thisTo(){
   console.log(this.a);
}
var data={
    a:2,
    foo:thisTo //經過屬性引用this所在函數
};
var a=3;//全局屬性

setTimeout(data.foo,100);// 3
複製代碼

所謂傳參丟失,就是在將包含this的函數做爲參數在函數中傳遞時,this指向改變

setTimeout函數的原本寫法應該是setTimeout(function(){......},100); 100ms後執行的函數都在「......」中, 能夠將要執行函數定義成var fun = function(){......}, 即:setTimeout(fun,100),100ms後就有:fun();因此此時此刻是data.foo做爲一個參數,是這樣的:setTimeout(thisTo,100);100ms事後執行thisTo(), 實際道理還跟1.1差很少,沒有調用thisTo的對象,this只能指向window 實際上你沒辦法控制回調函數的執行方式,沒有辦法控制會影響綁定的調用位置. 所以, 回調函數丟失this綁定是很是常見的,甚至更加出乎意料的是,調用回調函數的函數可能會修改this,特別 是在一些流行的JavaScript庫中時間處理器會把回調函數的this強制綁定到觸發事件的DOM元素上


總結

四種規則:

  • 由new調用(new綁定)
  • 經過call、apply(顯式綁定)或者硬綁定調用
  • 函數是否在某個上下文對象中調用(隱式綁定)
  • 默認綁定

特殊狀況特殊處理:

  • 箭頭函數
  • 被忽略的this
  • 綁定丟失
相關文章
相關標籤/搜索