《你不知道的JavaScript》-- 精讀(七)

知識點

1.this全面解析

1.1 調用位置

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

一般來講,尋找調用位置就是尋找「函數被調用的位置」,最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。咱們關心的調用位置就在當前正在執行的函數的前一個調用中。java

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的調用位置
複製代碼

注意,咱們是如何(從調用棧中)分析出真正的調用位置的,由於它決定了this的綁定。數組

1.2 綁定規則

咱們來看看在函數的執行過程當中調用位置如何決定this的綁定對象。安全

必須找到調用位置,而後判斷須要應用下面規則中的哪一條。app

1.2.1 默認綁定

首先要介紹的是最經常使用的函數調用類型:獨立函數調用。能夠把這條規則看做是沒法應用其餘規則時的默認規則。函數

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

咱們知道,聲明在全局做用域中的變量(好比var a = 2;)就是全局對象的一個同名屬性。當調用foo()時,this.a被解析成了全局變量a。由於在本例中,函數調用時應用了this的默認綁定,所以this指向全局對象。oop

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

若是使用嚴格模式(strict mode),則不能將全局對象用於默認綁定,所以this會綁定到undefined:測試

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

雖然this的綁定規則徹底取決於調用位置,可是隻有foo()運行在非strict mode下時,默認綁定才能綁定到全局對象;在嚴格模式下,調用foo()則不影響默認綁定:ui

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

1.2.2 隱式綁定

另外一條須要考慮的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或包含,不過這種說法可能會形成一些誤導。

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

當foo()被調用時,它的前面確實加上了對obj的引用。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象。由於調用foo()時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上,取決因而否是嚴格模式。

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()實際上是一個不帶任何修飾的函數調用,所以應用了默認綁定。

一種更微妙、更常見而且更出乎意料的狀況發生在傳入回調函數時:

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

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

若是把函數傳入語言內置的函數而不是傳入你本身聲明的函數,會發生什麼呢?結果是同樣的,沒有區別:

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

JavaScript環境中內置的setTimeout()函數實現和下面的僞代碼相似:

function setTimeout(fn,delay){
    // 等待delay毫秒
    fn(); // 調用位置
}
複製代碼

如上,回調函數丟失this綁定是很是常見的。除此以外,調用回調函數的函數也可能會修改this。

不管哪一種狀況,this的改變都是意想不到的,可是咱們能夠經過固定this來修復這個問題。

1.2.3 顯式綁定

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

那麼若是咱們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎麼作呢?

可使用函數的call(..)和apply(..)方法。它們的第一個參數是一個對象,是給this準備的,接着在調用函數時將其綁定到this。由於你能夠直接指定this的綁定對象,所以咱們稱之爲顯式綁定。

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

經過foo.call(..),咱們能夠在調用foo時強制把它的this綁定到obj上。

若是你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來當作this的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。這一般被稱爲「裝箱」。

惋惜,顯式綁定仍然沒法解決咱們以前提出的丟失綁定問題。

硬綁定

可是顯式綁定的一個變種能夠解決這個問題。

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

咱們建立了函數bar(),並在它的內部手動調用了foo.call(obj),所以強制把foo的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的上下文並調用原始函數。

API調用的「上下文」

第三方庫的許多函數,以及JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」(context),其做用和bind(..)同樣,確保你的回調函數使用指定的this。

function foo(el) {
    console.log(el,this.id)
}
var obj = {
    id: "awesome"
}
// 調用foo(..)時把this綁定到obj
[1,2,3].forEach(foo,obj);
// 1 awesome 2 awesome 3 awesome
複製代碼

這些函數實際上就是經過call(..)或者apply(..)實現了顯式綁定。

1.2.4 new綁定

包括內置對象函數在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用。實際上,並不存在所謂的「構造函數」,只有對於函數的「構造調用」。

使用new來調用函數,或者說,發生構造函數調用時,會自動執行如下操做:

  • 1.建立(或者說構造)一個全新的對象
  • 2.這個新對象會被執行[[Prototype]]鏈接
  • 3.這個新對象會綁定到函數調用的this
  • 4.若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
複製代碼

使用new來調用foo(..)時,咱們會構造一個新對象並把它綁定到foo(..)調用中的this上。new是最後一種能夠影響函數調用時this綁定行爲的方法,咱們稱之爲new綁定。

1.3 優先級

如今咱們知道了函數調用中this綁定的四條規則,接下來介紹的是這些規則的優先級。

毫無疑問,默認綁定的優先級是四條規則中最低的,隱式綁定和顯式綁定哪一個優先級更高?咱們來測試一下:

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); 
console.log(obj2.a); // 3

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

能夠看到,new綁定比隱式綁定優先級高,可是new綁定和顯式綁定誰的優先級更高呢?

function foo(p1,p2) {
    this.val = p1 + p2
}
var bar = foo.bind(null,"p1");
var baz = new bar("p2");
baz.val; // p1p2
複製代碼

能夠看到,new綁定比顯式綁定的優先級高。

判斷this

如今咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。

  • 1.函數是否在new中調用(new綁定)?若是是的話this綁定的是新建立的對象。

    var bar = new foo()

  • 2.函數是否經過call、apply(顯式綁定)或者硬綁定調用?若是是的話,this綁定的是指定的對象。

    var bar = foo.call(obj2)

  • 3.函數是否在某個上下文對象中調用(隱式綁定)?若是是的話,this綁定的是那個上下文對象

    var bar = obj1.foo()

  • 4.若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined,不然綁定到全局對象。

    var bar = foo()

1.4 綁定例外

1.4.1 被忽略的this

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

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

通常傳入null是爲了使用apply(..)來「展開」一個數組,並當作參數傳入一個函數。相似地,bind(..)能夠對參數進行柯里化(預先設置一些參數)

function foo(a,b) {
    console.log("a:" + a + ", b:" + b);
}
// 把數組「展開」成參數
foo.apply(null,[2,3]); // a:2, b:3
// 使用bind(..)進行柯里化
var bar = foo.bind(null,2);
bar(3); // a:2, b:3
複製代碼

這兩種方法都須要傳入一個參數當作this的綁定對象。若是函數並不關心this的話,你仍然須要傳入一個佔位值,這時null多是一個不錯的選擇。

然而老是使用null來忽略this綁定可能產生一些反作用。由於默認綁定規則會將this綁定到全局對象。

更安全的this

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

function foo(a,b) {
    console.log("a:" + a + ", b:" + b);
}

// 咱們的DMZ空對象
var ø = Object.create(null);

// 把數組展開成參數
foo.apply(ø,[2,3]); // a:2, b:3

// 使用bind(..)進行柯里化
var bar = foo.bind(ø,2);
bar(3); // a:2,b:3
複製代碼

1.4.2 間接引用

另外一個須要注意的是,你有可能(有意或者無心地)建立一個函數的「間接引用」,在這種狀況下,調用這個函數會應用默認綁定規則。

間接引用最容易在賦值時發生:

function foo() {
    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綁定對象的並非調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式。若是函數體處於嚴格模式,this會被綁定到undefined,不然this會被綁定到全局對象。

1.5 this詞法

箭頭函數並非使用function關鍵字定義的,而是使用被稱爲「胖箭頭」的操做符=>定義的。箭頭函數不使用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() {
    setTimeout(() => {
        // 這裏的this在詞法上繼承自foo()
        console.log(this.a);
    },100)
}

var obj = {
    a: 2
}

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

箭頭函數能夠像bind(...)同樣確保函數的this被綁定到指定對象,此外,其重要性還體如今它用更常見的詞法做用域取代了傳統的this機制。實際上,在ES6以前咱們就已經在使用一種幾乎和箭頭函數徹底同樣的模式。

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機制,那你或許應當:

  • 1.只使用詞法做用域並徹底拋棄錯誤this風格的代碼;
  • 2.徹底採用this風格,在必要時使用bind(..),儘可能避免使用self = this和箭頭函數。

總結

若是要判斷一個運行中函數的this綁定,就須要找到這個函數的直接調用位置。找到以後,就能夠順序應用下面這四條規則來判斷this的綁定對象。

1.由new調用?綁定到新建立的對象。

2.由call或者apply(或者bind)調用?綁定到指定的對象。

3.由上下文對象調用?綁定到那個上下文對象。

4.默認:在嚴格模式下綁定到undefined,不然綁定到全局對象。

ES6中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定this,具體來講,箭頭函數會繼承外層函數調用的this綁定(不管this綁定到什麼)。

巴拉巴拉

好像有近一個月沒有更新了,感受本身是懶癌犯了,不想推脫說工做忙,或者太累,由於老是有時間刷豆瓣,刷知乎,最近發現,不少時候,被指責,被批評,第一反應是找藉口開脫,不知道是否是隻有我一我的這樣,因此開始學會從自身找緣由,你作的好,確定不會被批,固然,也許會有特地找茬的狀況,但是,我身邊都是很好的人,不存在這樣的狀況,須要學習和提高的地方好多,只能慢慢來啦,我看這本書的時候,有不少地方會理不順,就隨手去翻了一下《JavaScript權威指南》,發現寫的真好啊,值得細讀,推薦給你們~。

相關文章
相關標籤/搜索