JavaScript中this關鍵字

Shenfq發表於2017-10-17

this一直是js中一個老生常談的東西,但是我們究竟該如何來理解它呢?
在《JavaScript高階程式設計》中,對this的解釋是:

this物件是在執行時基於函式的執行環境繫結的。

我們來逐字解讀這句話:

  • this是一個物件
  • this的產生與函式有關
  • this與執行環境繫結

說通俗一點就是,“誰呼叫的這個函式,this就是誰”。


一、函式直接呼叫中的this

舉個栗子:

var x = 1;

function testThis() {
    console.log(this.x);
}

testThis();  //1複製程式碼

js中有一個全域性物件window,直接呼叫函式testThis時,就相當於呼叫window下的testThis方法,包括直接宣告的變數也都是掛載在window物件下的。

var x = 1;
function testThis() {
    this.innerX = 10;
    return 1;
}
testThis() === window.testThis();  // true
innerX === window.innerX;  // true
x === window.x;  // true複製程式碼

同理,在匿名函式中使用this也是指向的window,因為匿名函式的執行環境具有全域性性。

(function () {
    console.log(this); //window
})();複製程式碼

但是呢,凡事都有例外,js的例外就是嚴格模式。在嚴格模式中,禁止this關鍵字指向全域性物件。

(function () {
    'use strict';
    console.log(this); //undefined
})();複製程式碼

二、物件方法呼叫中的this

再舉個栗子:

var person = {
    "name": "shenfq",
    "showName": function () {
        console.log(this.name);
    } 
};

person.showName(); // 'shenfq'複製程式碼

此時,showName方法中的this指向的是物件person,因為呼叫showName的是person物件,所以showName方法中的 this.name 其實就是 person.name。

但是如果我們換個思路,把showName方法賦值給一個全域性變數,然後在全域性環境下呼叫。

var name = 'global',
    person = {
        "name": "shenfq",
        "showName": function () {
            console.log(this.name);
        } 
    },
    showGlobalName = person.showName;
showGlobalName(); // 'global'複製程式碼

可以看到,在全域性環境中呼叫showName方法時,this就會指向window。

再換個思路,如果showName方法被其他物件呼叫呢?

var person = {
        "name": "shenfq",
        "showName": function () {
            console.log(this.name);
        } 
    },
    animal = {
        "name": "dog",
        "showName": person.showName
    };

animal.showName(); // 'dog'複製程式碼

此時的name又變成了animal物件下的name,再複雜一點,如果呼叫方法的是物件下的一個屬性,而這個屬性是另個物件。

function showName () {
    console.log(this.name);
}
var person = {
    "name": "shenfq",
    "bodyParts": {
        "name": "hand",
        "showName": showName
    },
    "showName": showName
};

person.showName(); // 'shenfq'
person.bodyParts.showName(); // 'hand'複製程式碼

雖然呼叫showName方法的最源頭是person物件,但是最終呼叫的是person下的bodyParts,所以方法寫在哪個物件下其實不重要,重要的是這個方法最後被誰呼叫了,this指向的永遠是最終呼叫它的那個物件。講來講去,this也就那麼回事,只要知道函式體的執行上下文就能知道this指向哪兒,這個規則在大多數情況下都適用,注意是大多數情況,少部分情況後面會講。

最後一個思考題,當方法返回一個匿名函式,這個匿名函式裡面的this指向哪裡?

var name = 'global',
    person = {
        "name": "shenfq",
        "returnShowName": function () {
            return function () {
                console.log(this.name);
            }
        } 
    };

person.returnShowName()(); // 'global'複製程式碼

答案一目瞭然,匿名函式不管寫在哪裡,只要是被直接呼叫,它的this都是指向window,因為匿名函式的執行環境具有全域性性。


三、new建構函式中的this

還是先舉個栗子:

function Person (name) {
    this.name = name;
}
var global = Peson('global'),
    xiaoming = new Person('xiaoming');

console.log(window.name); // 'global'
console.log(xiaoming.name); // 'xiaoming'複製程式碼

首先不使用new操作符,直接呼叫Person函式,這時的this任然指向window。當使用了new操作符時,這個函式就被稱為建構函式。

所謂建構函式,就是用來構造一個物件的函式。建構函式總是與new操作符一起出現的,當沒有new操作符時,該函式與普通函式無區別。

對建構函式進行new操作的過程被稱為例項化。new操作會返回一個被例項化的物件,而建構函式中的this指向的就是那個被例項化的物件,比如上面例子中的xiaoming。

關於建構函式有幾點需要注意:

  1. 例項化物件預設會有constructor屬性,指向建構函式;

function Person (name) {
    this.name = name;
}
var xiaoming = new Person('xiaoming');

console.log(xiaoming.constructor); // Person複製程式碼
  1. 例項化物件會繼承建構函式的原型,可以呼叫建構函式原型上的所有方法;

function Person (name) {
    this.name = name;
}
Person.prototype = {
    showName: function () {
        console.log(this.name);
    }
};
var xiaoming = new Person('xiaoming');

xiaoming.showName(); // 'xiaoming'複製程式碼
  1. 如果建構函式返回了一個物件,那麼例項物件就是返回的物件,所有通過this賦值的屬性都將不存在

function Person (name, age) {
    this.name = name;
    this.age  = age;
    return {
        name: 'innerName'
    };
}
Person.prototype = {
    showName: function () {
        console.log(this.name);
    }
};
var xiaoming = new Person('xiaoming', 18);

console.log(xiaoming); // {name: 'innerName'}複製程式碼

四、通過call、apply間接呼叫函式時的this

又一次舉個栗子:

var obj = {
    "name": "object"
}

function test () {
    console.log(this.name);
}

test.call(obj);   // 'object'
test.apply(obj);  // 'object'複製程式碼

callapply方法都是掛載在Function原型下的方法,所有的函式都能使用。

這兩個函式既有相同之處也有不同之處:

  • 相同的地方就是它們的第一個引數會繫結到函式體的this上,如果不傳引數,this預設還是繫結到window上。
  • 不同之處在於,call的後續引數會傳遞給呼叫函式作為引數,而apply的第二個引數為一個陣列,陣列裡的元素就是呼叫函式的引數。

語言很蒼白,我只好寫段程式碼:

var person = {
    "name": "shenfq"
};
function changeJob(company, work) {
    this.company = company;
    this.work    = work;
};

changeJob.call(person, 'NASA', 'spaceman');
console.log(person.work); // 'spaceman'

changeJob.apply(person, ['Temple', 'monk']);
console.log(person.work); // 'monk'複製程式碼

有一點值得注意,這兩個方法會把傳入的引數轉成物件型別,不管傳入的字串還是數字。

var number = 1, string = 'string';
function getThisType () {
    console.log(typeof this);
}

getThisType.call(number); //object
getThisType.apply(string); //object複製程式碼

五、通過bind改變函式的this指向

最後舉個栗子:

var name = 'global',
    person = {
        "name": "shenfq"
    };
function test () {
    console.log(this.name);
}

test(); // global

var newTest = test.bind(person);
newTest(); // shenfq複製程式碼

bind方法是ES5中新增的,和call、apply一樣都是Function物件原型下的方法-- Function.prototype.bind ,所以每個函式都能直接呼叫。bind方法會返回一個與呼叫函式一樣的函式,只是返回的函式內的this被永久繫結為bind方法的第一個引數,並且被bind繫結後的函式不能再被重新繫結。

function showName () {
    console.log(this.name);
}
var person = {"name": "shenfq"},
    animal = {"name": "dog"};

var showPersonName = showName.bind(person),
    showAnimalName = showPersonName.bind(animal);

showPersonName(); //'shenfq'
showAnimalName(); //'shenfq'複製程式碼

可以看到showPersonName方法先是對showName繫結了person物件,然後再對showPersonName重新繫結animal物件並沒有生效。

六、箭頭函式中的this

真的是最後一個栗子:

var person = {
    "name": "shenfq",
    "returnArrow": function () {
        return () => {
            console.log(this.name);
        }
    }
};

person.returnArrow()(); // 'shenfq'複製程式碼

箭頭函式是ES6中新增的一種語法糖,簡單說就是匿名函式的簡寫,但是與匿名函式不同的是箭頭函式中的this表示的是外層執行上下文,也就是說箭頭函式的this就是外層函式的this。

var person = {
    "name": "shenfq",
    "returnArrow": function () {
        let that = this;
        return () => {
            console.log(this == that);
        }
    }
};

person.returnArrow()(); // true複製程式碼

補充:

事件處理函式中的this:

var $btn = document.getElementById('btn');
function showThis () {
    console.log(this);
}
$btn.addEventListener('click', showThis, false);複製程式碼

點選按鈕可以看到控制檯列印出了元素節點。

事件結果
事件結果

其實事件函式中的this預設就是繫結事件的元素,呼叫事件函式時可以簡單理解為

$btn.showThis()

只要單擊了按鈕就會已這種方式來觸發事件函式,所以事件函式中的this表示元素節點,這也與之前定義的“誰呼叫的這個函式,this就是誰”相吻合。

eval中的this:

eval('console.log(this)'); //window
var obj = {
    name: 'object',
    showThis: function () {
        eval('console.log(this)');
    }
}
obj.showThis(); // obj複製程式碼

eval是一個可以動態執行js程式碼的函式,能將傳入其中的字串當作js程式碼執行。這個方法一般用得比較少,因為很危險,想想動態執行程式碼,什麼字串都能執行,但是如果用得好也能帶來很大的便利。

eval中的this與箭頭函式比較類似,與外層函式的this一致。

當然這隻針對現代瀏覽器,在一些低版本的瀏覽器上,比如ie7、低版本webkit,eval的this指向會有些不同。

eval也可以在一些特殊情況下用來獲取全域性物件(window、global),使用 (1,eval)('this')


先寫這麼多,有需要再補充 ^ _ ^

參考:

  1. this - JavaScript | MDN
  2. Javascript的this用法
  3. (1,eval)('this') vs eval('this') in JavaScript?

原文連結

相關文章