Javascript進階2--this

系列文章:javascript

this 機制

誤解

this 機制在 javascript 中是動態綁定,或稱爲運行期綁定的。這就致使 JS 中的 this 關鍵字會有多重含義,因此會給咱們形成一誤解。學習 this 的第一步是明白this既不指向函數自身也不指向函數的詞法做用域java

指向自身

人們容易把 this 理解成指向函數自身,看下面的代碼bash

function foo(num) {
    console.log("foo: " + num
    this.count++;
}
// 這裏爲 foo 添加了一個屬性 count,初始化爲 0
foo.count = 0;
for(var i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log(foo.count); // 0
// foo 確實被調用了4次,可是 foo.count 仍是 0,
// 是由於函數內部的 this 並非指向那個函數對象,
// 因此雖然屬性名相同,可是根對象卻不相同
複製代碼

指向它的做用域

this指向函數的做用域,這個問題比較複雜,由於有時它是正確的,有時它的錯誤的。但能夠明確的是:this在任何狀況下,都不指向函數的詞法做用域閉包

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

結果是 undefined ,由於這裏經過 this.bar() 來引用 bar 函數,這裏能調用成功是由於:app

  1. foo()被調用時,this === window,因此能夠調用 bar 函數。
  2. bar執行時,this === window,使用對象屬性訪問規則,沒有找到 a,因此返回undefined。

⚠️注意:這裏有人會認爲結果是 Reference Error 報錯,其實和 RHS(Javascript進階1--做用域和閉包 內有解釋)混淆了。函數

this 解析

javascript中,this 是在運行時進行綁定的,它的上下文取決於函數調用時的各類條件。oop

綁定規則

先根據把綁定的優先級拋出結論,按照如下順序進行判斷:post

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

    var bar = new Foo()ui

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

    var bar = foo.call(obj2)

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

    var bar = obj1.foo()

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

    var bar = foo()

new 綁定

在傳統的面向類的語言中,構造函數是類的一些特殊方法,使用 new 初始化類時,會調用類中的構造函數。

首先,咱們須要澄清的一個誤解是:在JS中,是沒有構造函數的,在普通的函數調用前面加 new 關鍵字以後,就會把這個函數調用變成一個「構造函數調用」。實際上,new 會劫持全部普通函數並用構造函數的形式來調用它

那麼在JS中,使用 new 來調用函數,var obj = new Foo(),會執行如下操做:

  1. 首先建立一個全新的空對象。

    var obj = {}

  2. 將空對象的原型 [[prototype]] 賦值爲構造函數的原型.

    obj.__proto__ = Foo.prototype

  3. 更改 this 的指向。

    Foo.call(obj)

  4. 若 return 有值,而且值是對象,則直接返回此對象,不然,返回新建立的對象 obj。

如今看一段簡單的代碼檢測一下學習成果

function foo1(a) {
    this.a = a;
}
function foo2(name) {
    this.name = name;
    return {
        w:1
    };
}
foo2.prototype.getName = function() {
    return this.name;
};
var bar = new foo1(2);
console.log(bar.a) // 2
var a = new foo2('hh');
a.getName();
// Uncaught TypeError: a.getName is not a function
// 這是由於,foo2 有返回值,而且是一個對象,因此a值是 {w: 1}
複製代碼

顯式綁定和硬綁定

若是想調用函數時,強制把函數的 this 綁定到 obj 上,可使用 call(...) 和 apply(...) 方法,它們的第一個參數一個對象,是給 this 準備的,這稱之爲顯式綁定

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

⚠️注意:若是傳入了一個原始值(布爾、數字、字符串等類型)來看成 this 的綁定對象,這個原始值會被轉換成它的對象形式(也就是 new Boolean(...)、new Number(...)、new String(...))。

硬綁定就是顯式綁定的一個變種,用於咱們將 this 被永久綁定到 obj 的 foo 函數,這樣咱們就沒必要每次調用 foo 都在尾巴上加上 call 那麼麻煩。

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

call/apply 與 bind 的區別是:call、apply將當即執行該函數,bind 不執行函數,只返回一個可供執行的函數。

隱式綁定

當一個函數被一個對象調用時,會把函數調用中的 this 綁定到這個上下文對象中。

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

⚠️注意:當咱們使用隱式綁定規則時,要注意下面兩點:

  • 在參數傳遞、賦值時,隱式綁定的函數可能會丟失綁定對象,也就是會應用默認綁定規則。
    function foo() {
       console.log(this.a);
    }
    function doFoo(fn) {
       fn();
    }
    var obj = {
       a: 2,
       foo: foo
    };
    var a = 'oops, global';
    // obj.foo 引用 foo 函數自己,應用默認綁定
    doFoo(obj.foo); // oops, global
    
    var bar = obj.foo;
     // bar 引用 foo 函數自己,應用默認綁定
    bar(); // oops, global
    複製代碼
  • 對象屬性引用鏈中只有最後一層在調用位置中起做用。

默認綁定

當直接使用不帶任何修飾的函數引用進行函數調用時,只能使用默認綁定,沒法應用其餘規則。在嚴格模式下,this 會綁定到 undefined,在非嚴格模式下, this會綁定到全局對象。

若是把 null 或者 undefined 做爲 this 的綁定對象傳入 call、apply或者 bind,會應用默認綁定規則。

箭頭函數

箭頭函數根據外層(函數或全局)做用域來決定 this。

function foo() {
    return (a) => {
        //  繼承自 foo()
        console.log(this.a)
    };
}
var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.call(obj1);
bar.call(obj2) // 2
複製代碼

⚠️注意:箭頭函數的綁定沒法被修改

好題練習

練習題1:

function Foo() {
    getName = function() {
        console.log(1);
    };
    return this;
}
// 靜態方法賦值
Foo.getName = function() {
    console.log(2);
};
Foo.prototype.getName = function() {
    console.log(3);
};
var getName = function() {
    console.log(4);
};
function getName() {
    console.log(5);
}
Foo.getName(); //2

// 變量聲明提高,賦值語句留在原地,因此結果是4
getName(); //4

// 在Foo函數中,重寫了全局做用域下的 getName 函數
Foo().getName(); //1 
getName(); //1

// 等價於new (Foo.getName)(), 運算符優先級:成員訪問 > new(不帶括號) > 函數調用
new Foo.getName(); //2 

// 等價於 (new Foo()).getName(),運算符優先級:成員訪問、new(帶括號)的優先級同樣,因此從左到右執行。
// 使用 new 實例 Foo 後生成的實例上,只有原型方法,沒有靜態方法
new Foo().getName();
複製代碼

練習題2:

var title = 'world';
var a = {
    title: 'hello',
    alias: this.title,
    show() {
        console.log(this.title, this.alias)
    },
    ashow: () => {
        console.log(this.title, this.alias)
    }
}

a.show(); // hello world
var b = a.show; 
b(); // world undefined
a.ashow(); // world undefined
複製代碼
相關文章
相關標籤/搜索