很多人當談到 JavaScript 中的 this
的時候會感到頭疼,因為在 JavaScript 中,this
是動態綁定,或稱為運行期綁定的,這就導致 JavaScript 中的 this
關鍵字有能力具備多重含義,帶來靈活性的同時,也為初學者帶來不少困惑。
每個函數調用都有與之相關的作用域和上下文。首先需要澄清的問題是上下文和作用域是不同的概念。很多人經常將這兩個術語混淆。
作用域(scope) 是在運行時代碼中的某些特定部分中變量,函數和對象的可訪問性。換句話說,作用域決定了代碼區塊中變量和其他資源的可見性。而上下文(context)是用來指定代碼某些特定部分中 this
的值。
從根本上說,作用域是基於函數(function-based)的,而上下文是基於對象(object-based)的。換句話說,作用域是和每次函數調用時變量的訪問有關,並且每次調用都是獨立的。上下文總是被調用函數中關鍵字 this
的值,是調用當前可執行代碼的對象的引用。說的通俗一點就是:this
取值,是在函數真正被調用執行的時候確定的,而不是在函數定義的時候確定的。
無論是否在嚴格模式下,在全局執行上下文中(在任何函數體外部)this
都指向全局對象。當然具體的全局對象和宿主環境有關。
在瀏覽器中, window
對象同時也是全局對象:
JavaScript 代碼:
- console.log(this === window); // true
NodeJS 中,則是 global
對象:
JavaScript 代碼:
- console.log(this); // global
由於其運行期綁定的特性,JavaScript 中的 this
含義要豐富得多,它可以是全局對象、當前對象或者任意對象,這完全取決於函數的調用方式。JavaScript 中函數的調用有以下幾種方式:作為函數調用,作為對象方法調用,作為構造函數調用,和使用 apply
或 call
調用。下面我們將按照調用方式的不同,分別討論 this
的含義。
作為函數直接調用時,要注意 2 種情況:
在非嚴格模式下執行函數調用,此時 this
默認指向全局對象。
JavaScript 代碼:
- function f1(){
- return this;
- }
- //在瀏覽器中:
- f1() === window; //在瀏覽器中,全局對象是window
- //在Node中:
- f1() === global;
在嚴格模式下,this
將保持他進入執行上下文時的值,所以下面的 this
並不會指向全局對象,而是默認為 undefined 。
JavaScript 代碼:
- 'use strict'; // 這裡是嚴格模式
- function test() {
- return this;
- };
- test() === undefined; // true
在 JavaScript 中,函數也是對象,因此函數可以作為一個對象的屬性,此時該函數被稱為該對象的方法,在使用這種調用方式時,內部的 this
指向該對象。
JavaScript 代碼:
- var Obj = {
- prop: 37,
- getProp: function() {
- return this.prop;
- }
- };
- console.log(Obj.getProp()); // 37
上面的例子中,當 Obj.getProp()
被調用時,方法內的 this
將指向 Obj
對象。值得注意的是,這種行為根本不受函數定義方式或定義位置的影響。在前面的例子中,我們在定義對象 Obj
的同時,將成員 getProp
定義了一個匿名函數。但是,我們也可以首先定義函數,然後再將其附加到 Obj.getProp
。所以,下面的代碼和上面的例子是等價的:
JavaScript 代碼:
- var Obj = {
- prop: 37
- };
- function independent() {
- return this.prop;
- }
- Obj.getProp = independent;
- console.log(Obj.getProp()); // logs 37
JavaScript 非常靈活,現在我們把對象的方法賦值給一個變量,然後直接調用這個函數變量又會發生什麼呢?
JavaScript 代碼:
- var Obj = {
- prop: 37,
- getProp: function() {
- return this.prop;
- }
- };
- var test = Obj.getProp
- console.log(test()); // undefined
可以看到,這時候 this
指向全局對象,這個例子 test
只是引用了 Obj.getProp
函數,也就是說這個函數並不作為 Obj
對象的方法調用,所以,它是被當作一個普通函數來直接調用。因此,this
指向全局對象。
我們來看看下面這個例子:
JavaScript 代碼:
- var prop = 0;
- var Obj = {
- prop: 37,
- getProp: function() {
- setTimeout(function() {
- console.log(this.prop) // 結果是 0 ,不是37!
- },1000)
- }
- };
- Obj.getProp();
正如你所見, setTimeout
中的 this
向了全局對象,這裡不是把它當作函數的方法使用嗎?這一點經常讓很多初學者疑惑;這種問題是很多異步回調函數中也會普遍會碰到,通常有個土辦法解決這個問題,比如,我們可以利用 閉包 的特性來處理:
JavaScript 代碼:
- var Obj = {
- prop: 37,
- getProp: function() {
- var self = this;
- setTimeout(function() {
- console.log(self.prop) // 37
- },1000)
- }
- };
- Obj.getProp();
其實,setTimeout
和 setInterval
都只是在全局上下文中執行一個函數而已,即使是在嚴格模式下:
JavaScript 代碼:
- 'use strict';
- function foo() {
- console.log(this); // Window
- }
- setTimeout(foo, 1);
記住 setTimeout
和 setInterval
都只是在全局上下文中執行一個函數而已,因此 this 指向全局對象。 除非你實用箭頭函數,Function.prototype.bind
方法等辦法修復。至於解決方案會在後續的文章中繼續討論。
JavaScript 支持面向對象式編程,與主流的面向對象式編程語言不同,JavaScript 並沒有類(class)的概念,而是使用基於原型(prototype)的繼承方式。作為又一項約定通用的準則,構造函數以大寫字母開頭,提醒調用者使用正確的方式調用。
當一個函數用作構造函數時(使用 new
關鍵字),它的 this
被綁定到正在構造的新對象,也就是我們常說的實例化出來的對象。
JavaScript 代碼:
- function Person(name) {
- this.name = name;
- }
- var p = new Person('愚人碼頭');
- console.log(p.name); // "愚人碼頭"
如果構造函數具有返回對象的 return
語句,則該返回對象將是 new
表達式的結果。
JavaScript 代碼:
- function Person(name) {
- this.name = name;
- return { title : "前端開發" };
- }
- var p = new Person('愚人碼頭');
- console.log(p.name); // undefined
- console.log(p.title); // "前端開發"
相應的,JavaScript 中的構造函數也很特殊,如果不使用 new
調用,則和普通函數一樣, this 仍然執行全局:
JavaScript 代碼:
- function Person(name) {
- this.name = name;
- console.log(this); // Window
- }
- var p = Person('愚人碼頭');
在箭頭函數中,this
與封閉詞法上下文的 this
保持一致,也就是說由上下文確定。
JavaScript 代碼:
- var obj = {
- x: 10,
- foo: function() {
- var fn = () => {
- return () => {
- return () => {
- console.log(this); //{x: 10, foo: ƒ} 即 obj
- console.log(this.x); //10
- }
- }
- }
- fn()()();
- }
- }
- obj.foo();
obj.foo
是一個匿名函數,無論如何, 這個函數中的 this 指向它被創建時的上下文(在上面的例子中,就是 obj
對象)。這同樣適用於在其他函數中創建的箭頭函數:這些箭頭函數的this 被設置為外層執行上下文。
JavaScript 代碼:
- // 創建一個含有bar方法的obj對象,bar返回一個函數,這個函數返回它自己的this,
- // 這個返回的函數是以箭頭函數創建的,所以它的this被永久綁定到了它外層函數的this。
- // bar的值可以在調用中設置,它反過來又設置返回函數的值。
- var obj = {
- bar: function() {
- var x = (() => this);
- return x;
- }
- };
- // 作為obj對象的一個方法來調用bar,把它的this綁定到obj。
- // x所指向的匿名函數賦值給fn。
- var fn = obj.bar();
- // 直接調用fn而不設置this,通常(即不使用箭頭函數的情況)默認為全局對象,若在嚴格模式則為undefined
- console.log(fn() === obj); // true
- // 但是注意,如果你只是引用obj的方法,而沒有調用它(this是在函數調用過程中設置的)
- var fn2 = obj.bar;
- // 那麼調用箭頭函數後,this指向window,因為它從 bar 繼承了this。
- console.log(fn2()() == window); // true
在上面的例子中,一個賦值給了 obj.bar
的函數(稱為匿名函數 A),返回了另一個箭頭函數(稱為匿名函數 B)。因此,函數B的this被永久設置為 obj.bar
(函數A)被調用時的 this
。當返回的函數(函數B)被調用時,它this始終是最初設置的。在上面的代碼示例中,函數B的 this
被設置為函數A的 this
,即 obj
,所以它仍然設置為 obj
,即使以通常將 this
設置為 undefined
或全局對象(或者如前面示例中全局執行上下文中的任何其他方法)進行調用。
我們回到上面 setTimeout 的坑:
JavaScript 代碼:
- var prop = 0;
- var Obj = {
- prop: 37,
- getProp: function() {
- setTimeout(function() {
- console.log(this.prop) // 結果是 0 ,不是37!
- },1000)
- }
- };
- Obj.getProp();
通常情況我,我們在這裡期望輸出的結果是 37
,用箭頭函數解決這個問題相當簡單:
JavaScript 代碼:
- var Obj = {
- prop: 37,
- getProp: function() {
- setTimeout(() => {
- console.log(this.prop) // 37
- },1000)
- }
- };
- Obj.getProp();
相同的概念在定義在原型鏈中的方法也是一致的。如果該方法存在於一個對象的原型鏈上,那麼 this
指向的是調用這個方法的對象,就好像該方法本來就存在於這個對象上。
JavaScript 代碼:
- var o = {
- f : function(){
- return this.a + this.b;
- }
- };
- var p = Object.create(o);
- p.a = 1;
- p.b = 4;
- console.log(p.f()); // 5
在這個例子中,對象 p
沒有屬於它自己的f屬性,它的f屬性繼承自它的原型。但是這對於最終在 o
中找到 f
屬性的查找過程來說沒有關係;查找過程首先從 p.f
的引用開始,所以函數中的 this
指向 p
。也就是說,因為f是作為p的方法調用的,所以它的this
指向了 p
。這是 JavaScript 的原型繼承中的一個有趣的特性。
你也會看到下面這種形式的老代碼,道理是一樣的:
JavaScript 代碼:
- function Person(name) {
- this.name = name;
- }
- Person.prototype = {
- getName:function () {
- return this.name
- }
- };
- var p = new Person('愚人碼頭');
- console.log(p.getName()); // "愚人碼頭"
再次,相同的概念也適用時的函數作為一個 getter
或者 一個 setter
調用。用作 getter
或 setter
的函數都會把 this
綁定到正在設置或獲取屬性的對象。
JavaScript 代碼:
- function sum() {
- return this.a + this.b + this.c;
- }
- var o = {
- a: 1,
- b: 2,
- c: 3,
- get average() {
- return (this.a + this.b + this.c) / 3;
- }
- };
- Object.defineProperty(o, 'sum', {
- get: sum, enumerable: true, configurable: true});
- console.log(o.average, o.sum); // logs 2, 6
當函數被用作事件處理函數時,它的 this
指向觸發事件的元素(一些瀏覽器在使用非addEventListener
的函數動態添加監聽函數時不遵守這個約定)。
JavaScript 代碼:
- // 被調用時,將關聯的元素變成藍色
- function bluify(e){
- console.log(this === e.currentTarget); // 總是 true
- // 當 currentTarget 和 target 是同一個對象是為 true
- console.log(this === e.target);
- this.style.backgroundColor = '#A5D9F3';
- }
- // 獲取文檔中的所有元素的列表
- var elements = document.getElementsByTagName('*');
- // 將bluify作為元素的點擊監聽函數,當元素被點擊時,就會變成藍色
- for(var i=0 ; i < elements.length; i++){
- elements[i].addEventListener('click', bluify, false);
- }
當代碼被內聯on-event 處理函數調用時,它的this指向監聽器所在的DOM元素:
HTML 代碼:
- <button onclick="alert(this.tagName.toLowerCase());">
- Show this
- </button>
上面的 alert 會顯示 button
。注意只有外層代碼中的 this
是這樣設置的:
HTML 代碼:
- <button onclick="alert((function(){return this})());">
- Show inner this
- </button>
在這種情況下,沒有設置內部函數的 this
,所以它指向 global
/window
對象(即非嚴格模式下調用的函數未設置 this
時指向的默認對象)。
JavaScript 中函數也是對象,對象則有方法,apply
和 call
就是函數對象的方法。這兩個方法異常強大,他們允許切換函數執行的上下文環境(context),即 this
綁定的對象。很多 JavaScript 中的技巧以及類庫都用到了該方法。讓我們看一個具體的例子:
JavaScript 代碼:
- function Point(x, y){
- this.x = x;
- this.y = y;
- this.moveTo = function(x, y){
- this.x = x;
- this.y = y;
- }
- }
- var p1 = new Point(0, 0);
- p1.moveTo(1, 1);
- console.log(p1.x,p1.y); //1 1
- var p2 = {x: 0, y: 0};
- p1.moveTo.apply(p2, [10, 10]);
- console.log(p2.x,p2.y); //10 10
在上面的例子中,我們使用構造函數生成了一個對象 p1
,該對象同時具有 moveTo
方法;使用對象字面量創建了另一個對象 p2
,我們看到使用 apply
可以將 p1
的方法 apply 到 p2
上,這時候 this
也被綁定到對象 p2
上。另一個方法 call
也具備同樣功能,不同的是最後的參數不是作為一個數組統一傳入,而是分開傳入的:
JavaScript 代碼:
- function Point(x, y){
- this.x = x;
- this.y = y;
- this.moveTo = function(x, y){
- this.x = x;
- this.y = y;
- }
- }
- var p1 = new Point(0, 0);
- p1.moveTo(1, 1);
- console.log(p1.x,p1.y); //1 1
- var p2 = {x: 0, y: 0};
- p1.moveTo.call(p2, 10, 10); // 只是參數不同
- console.log(p2.x,p2.y); //10 10
ECMAScript 5 引入了 Function.prototype.bind
。調用 f.bind(someObject)
會創建一個與 f
具有相同函數體和作用域的函數,但是在這個新函數中,this
將永久地被綁定到了 bind
的第一個參數,無論這個函數是如何被調用的。
JavaScript 代碼:
- function f(){
- return this.a;
- }
- //this被固定到了傳入的對象上
- var g = f.bind({a:"azerty"});
- console.log(g()); // azerty
- var h = g.bind({a:'yoo'}); //bind只生效一次!
- console.log(h()); // azerty
- var o = {a:37, f:f, g:g, h:h};
- console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty
上面我們已經講了使用箭頭函數填 setTimeout 的坑,這次我們使用 bind
方法來試試:
JavaScript 代碼:
- var prop = 0;
- var Obj = {
- prop: 37,
- getProp: function() {
- setTimeout(function() {
- console.log(this.prop) // 37
- }.bind(Obj),1000)
- }
- };
- Obj.getProp();
同樣可以填坑,但是看上去沒有使用箭頭函數來的優雅。
本文介紹了 JavaScript 中的 this 關鍵字在各種情況下的含義,雖然這只是 JavaScript 中一個很小的概念,但借此我們可以深入瞭解 JavaScript 中函數的執行環境,而這是理解閉包等其他概念的基礎。掌握了這些概念,才能充分發揮 JavaScript 的特點,才會發現 JavaScript 語言特性的強大。
联系客服