JavaScript之你不知道的this

曜靈SUN發表於2018-11-14

很重要的一句話

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


一、這篇文章出現的背景

1. this在我們開發過程中的重要性(開發場景) -- 通過一段程式碼簡單瞭解this

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

首先來看一段程式碼, 此處不使用this, 需要給identify()和speak()顯示的傳入一個上下文物件:

// 定義 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使得函式可以自動引用合適的上下文物件顯的尤為重要

2.兩種錯誤的理解

  • this指向函式自身
  • this指向函式的作用域, 這個在某些情況下是正確的, 但是在其他情況下確實錯誤的

事實上, 一部分人認為"this既不指向函式自身也不指向函式的詞法作用域", 但是也是不對的, 在某種情況下, this就指向函式自身, 也可能指向詞法作用域

3.本質

this是在執行(函式被呼叫)時發生繫結的,並不是在編寫時繫結, 它的上下文取決於函式呼叫時的各種條件,它指向什麼完全取決於函式在哪裡被呼叫


二、this 繫結規則 & 優先順序

簡單來說, 有這大致四種

  1. 由new呼叫(new繫結)
  2. 函式是否通過call、apply(顯式繫結)或者硬繫結呼叫
  3. 函式是否在某個上下文物件中呼叫(隱式繫結)
  4. 預設繫結

1. 預設繫結

無法應用其他規則時的預設規則, 嚴格模式下繫結到undefined, 否則繫結到全域性物件

最常用的函式呼叫型別:獨立函式呼叫

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
複製程式碼

三、繫結例外

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
複製程式碼

四、(隱式)繫結丟失

最常見的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
  • 繫結丟失

相關文章