this全面解析

最近在拜讀《你不知道的js》,而此篇是對於《你不知道的js》中this部分的筆記整理,但願能有效的梳理,而且鞏固關於this的知識點瀏覽器

1、調用位置

一、什麼調用位置?

調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)安全

二、如何尋找函數被調用的位置?

關鍵:分析調用棧,即爲了到達當前執行位置所調用的全部函數。而咱們關心的調用位置就在當前正在執行的函數的前一個調用中bash

先來看一段代碼:app

function baz() {
    //當前調用棧是:baz
    // 所以,當前調用位置是全局做用域
    console.log("baz");
    bar(); // bar的調用位置
}
function bar() {
    // 當前調用棧是baz -> bar
    // 所以,當前調用位置在baz中
    console.log("bar");
    foo(); // foo的調用位置
}
function foo() {
    // 當前調用棧是baz -> bar -> foo
    // 所以,當前調用位置在bar中
    console.log("foo");
}
baz(); // <-- baz的調用位置
複製代碼

咱們能夠把調用棧想象成一個函數調用鏈,但這種方法麻煩且易出錯。函數

但咱們可使用另外一種方式:使用瀏覽器的調試工具,設立斷點,或直接在代碼中插入debugger。運行代碼時,調試器會在那個位置暫停,同時會展現當前位置的函數調用列表,這就是你的調用棧。真正的調用位置是棧中的第二個元素工具

2、綁定規則

一、默認綁定

最經常使用的函數調用類型是獨立函數調用。可把這規則看作是沒法應用其餘規則時的默認規則。oop

先看一段代碼:測試

function foo() {
    //當前調用棧是:baz
    // 所以,當前調用位置是全局做用域
    console.log(this.a);
}
var a = 2;
foo(); // 2
複製代碼

從代碼中發現this指向了全局對象,並且函數調用時應用了this的默認綁定。ui

如何判斷是默認綁定?this

可從分析調用位置來看看foo()是如何調用的。在代碼中,foo()是直接使用不帶任何修飾的函數引用進行調用的,所以只能是默認綁定,沒法應用其餘規則

但若是是在嚴格模式下,又會有怎樣的結果呢?請看以下代碼:

function foo() {
    "use strict"
    console.log(this.a);
}
var a = 2;
foo(); // TypeError:this is undefined
複製代碼

這段代碼表示:雖然this的綁定規則徹底取決於調用位置,但只有在非嚴格模式下,默認綁定才綁定全局對象;在嚴格模式下則會綁定到undefined。

可是在嚴格模式下調用則不影響默認綁定:

function foo() {
    console.log(this.a);
}
var a = 2;
(function() {
    "use strict"
    foo(); // 2
})();
複製代碼

注意:一般來講不該該在代碼中混合使用strict模式與非strict模式

二、隱式綁定

這條規則是指調用位置是否有上下文對象,或者是否被某個對象擁有或包含

先看如下代碼:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo();// 2
複製代碼

該調用位置使用了obj上下文來引用函數,或者說函數被調用時obj對象「擁有」或「包含」它。

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

上述代碼調用foo()時,this被綁定到obj,所以this指向了obj,this.a 與 obj.a 是同樣的。

另外對象屬性引用鏈中只有上一層或最後一層在調用位置中起做用。例如:

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

隱式丟失

被隱式綁定的函數會丟失綁定對象這是一個常見的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函數的自己,此時的bar()是不帶任何修飾的函數調用,所以使用了默認綁定

例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"
複製代碼

這裏使用了參數傳遞,也是隱式賦值,因此結果和例1同樣

例3:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global";// a是全局對象的屬性
setTimeout(obj.foo, 100);// oops, global
複製代碼

回調函數丟失this綁定是常見的,調用回調函數的函數可能會修改this

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

三、顯式綁定

方法:可使用call或apply直接指定this的綁定對象

缺點:沒法解決丟失綁定的問題

例:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
foo.call(obj); // 2
複製代碼

若是你傳入了一個原始值做爲this綁定對象,這個原始值會被轉換成它的對象形式(new xxx()),這叫裝箱

(1)、硬綁定

此爲顯式綁定的一個變種,能夠解決丟失綁定問題 缺點:會大大下降函數的靈活性,使用綁定以後就沒法使用隱式綁定或者顯式綁定來修改this

例:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
var bar = function() {
    foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
// 硬綁定的bar不可能再修改它的this
bar.call(window); // 2
複製代碼

foo.call(obj)強制把this綁定到了obj,以後調用函數bar,它總會在obj上調用foo,這是顯式的強制綁定,叫作硬綁定

典型應用場景一:建立一個包裹函數,負責接收參數並返回值

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = function() {
    return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3
console.log(b); // 5
複製代碼

典型應用場景二:建立一個能夠重複使用的輔助函數

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
// 簡單的輔助綁定函數
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments)
    }
}
var obj = {
    a: 2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5
複製代碼

因爲硬綁定是一種經常使用模式,因此ES5提供了內置方法Function.prototype.bind:

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
複製代碼

bind會返回一個硬編碼的新函數,會把你指定的參數位置爲this的上下文並調用原始函數

(2)、API調用「上下文」

經過 call() 或 apply() 實現

四、new綁定

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面操做 a、建立一個全新對象 b、新對象會被執行[[Prototype]]連接 c、新對象被綁定到函數調用的this d、若是函數沒有返回其餘對象,則自動返回新對象 代碼:

var obj = {};
obj.__proto__ = Base.prototype;
var result = Base.call(obj);
return typeof result === 'obj' ? result : obj;
複製代碼

3、優先級

一、隱式綁定與顯式綁定

function foo() {
    console.log(this.a);
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
複製代碼

顯然:顯式綁定 > 隱式綁定

二、new綁定與隱式綁定

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

obj1.foo.call(obj2, 3); // 3
console.log(obj2.a);// 3

var bar = new obj1.foo(4);
console.log(obj1.a);// 2
console.log(bar.a);// 4
複製代碼

new綁定 > 隱式綁定

三、new綁定與顯式綁定

new和call/apply沒法一塊兒使用,所以沒法經過new foo.call(obj1) 來直接測試,但咱們可使用硬綁定來測試

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);// 2

var baz = new bar(3);
console.log(obj1.a);// 2
console.log(bar.a);// 3
複製代碼

這裏bar被硬綁定在了obj1上,但new bar(3)並無把obj1.a修改成3。相反,new修改了硬綁定(到obj1的)調用bar()中的this。由於使用了new綁定,咱們獲得了一個名爲baz的新對象,而且baz.a的值爲3 new綁定 > 硬綁定(顯式綁定)

四、判斷this

(1)、由new調用? 綁定到新建立的對象(new綁定)

var bar = new foo();
複製代碼

(2)、由call或apply或bind調用?綁定到指定對象(顯式綁定)

var bar = foo.call(obj2);
複製代碼

(3)、由上下文對象調用?綁定到那個上下文對象(隱式綁定)

var bar = obj1.foo();
複製代碼

(4)、默認綁定:嚴格模式下綁定到undefined,不然爲全局對象

var bar = foo();
複製代碼

4、綁定例外

一、被忽略的this

若是你把null貨undefined做爲this的綁定對象傳入call、apply、bind,這些值在調用時會被忽略,實際應用默認綁定規則:

function foo() {
    console.log(this.a);
}
var a = 2;
foo.call(null); // 2
複製代碼
function foo(a, b) {
    console.log("a:"+ a + ", b:" + b);
}
foo.apply(null, [2, 3]);// a:2, b:3
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3
複製代碼

老是用null來忽略this綁定可能會產生一些反作用。若是某個函數使用了this(如第三方庫中的一個函數),那默認綁定規則會把this綁定到全局對象(瀏覽器中爲window),這會致使不可預計的後果(如修改全局對象),或者致使更多難以分析和追蹤的bug

更安全的this

一種更安全的作法是傳入一個特殊對象,把this綁定到這個對象不會對你的程序產生任何反作用。

可建立一個"DMZ"非軍事區對象,一個空的非委託的對象,任何對於this的使用都會被限制在這個空對象中,不會對全局對象產生任何影響

function foo(a, b) {
    console.log("a:"+ a + ", b:" + b);
}
// 咱們的DMZ空對象
var __null = Object.create(null);
foo.apply(__null, [2, 3]);// a:2, b:3
var bar = foo.bind(__null, 2);
bar(3); // a:2, b:3
複製代碼

二、間接引用

間接引用的狀況下,調用這個函數會應用默認綁定規則,而且最容易在賦值時發生:

function foo(a, b) {
    console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo();// 3
(p.foo = o.foo)(); // 2
複製代碼

賦值表達式p.foo = o.foo的返回值是目標函數的引用,所以調用位置是foo() 而不是p.foo()或o.foo(),這裏會使用默認綁定

對於默認綁定來講,決定this綁定對象的並非調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式

三、軟綁定

給默認綁定指定一個全局對象和undefined之外的值,可實現和硬綁定相同的效果,同時保留隱式綁定或顯式綁定修改this的能力

if(!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕獲全部curried參數
        var curried = [].slice.call(arguments, 1);
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?
                    obj : this,
                curried.concat.apply(curried, arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    }
}
複製代碼
function foo(a, b) {
    console.log("name: " + this.name);
}
var obj = { name: 'obj' },
    obj2 = { name: 'obj2' },
    obj3 = { name: 'obj3' };
var fooOBJ = foo.softBind(obj);
fooOBJ(); // name: obj

obj2.foo = softBind(obj);
obj2.foo(); // name: obj2

fooOBJ.call(obj3); // name: obj3

setTimeout(obj2.foo, 100); // name: obj 使用了軟綁定
複製代碼

從上述代碼中能夠看到軟綁定版本的foo()能夠手動將this綁定到obj2或obj3上,但若是應用默認綁定,則會將this綁定到obj

5、箭頭函數

箭頭函數不使用this的四種標準規則,而是根據外層(函數或全局)做用域來決定this

function foo() {
    // 返回一個箭頭函數
    return (a) => {
        // this繼承自foo();
        console.log(this.a);
    };
}
var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.call(obj1);
bar.call(obj2);// 2, 不是3
複製代碼

foo()內部建立的箭頭函數會捕獲調用時foo()的this。因爲foo()的this綁定到obj1,bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定沒法被修改(new也不行)

function foo() {
    var self = this; 
    setTimeout(function(){
        console.log(self.a);
    }, 100);
}
var obj = {
    a: 2
};
foo.call(obj);// 2
複製代碼

self=this與箭頭函數均可以取代bind,但本質上是替代了this機制

常常編寫this風格代碼,但絕大部分時候會使用self=this或箭頭函數來否認this機制,應當注意如下兩點:

a、只是用詞法做用域並徹底拋棄錯誤this風格的代碼

b、徹底採用this風格,在必要時使用bind(),儘可能避免使用self=this和箭頭函數

兩種風格混用一般會使代碼更難維護,而且可能也會更難編寫

相關文章
相關標籤/搜索