打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
全面理解 JavaScript 中的 this – WEB前端開發

很多人當談到 JavaScript 中的 this 的時候會感到頭疼,因為在 JavaScript 中,this 是動態綁定,或稱為運行期綁定的,這就導致 JavaScript 中的 this 關鍵字有能力具備多重含義,帶來靈活性的同時,也為初學者帶來不少困惑。

上下文 vs 作用域

每個函數調用都有與之相關的作用域和上下文。首先需要澄清的問題是上下文和作用域是不同的概念。很多人經常將這兩個術語混淆。

作用域(scope) 是在運行時代碼中的某些特定部分中變量,函數和對象的可訪問性。換句話說,作用域決定了代碼區塊中變量和其他資源的可見性。而上下文(context)是用來指定代碼某些特定部分中 this 的值。

從根本上說,作用域是基於函數(function-based)的,而上下文是基於對象(object-based)的。換句話說,作用域是和每次函數調用時變量的訪問有關,並且每次調用都是獨立的。上下文總是被調用函數中關鍵字 this 的值,是調用當前可執行代碼的對象的引用。說的通俗一點就是:this 取值,是在函數真正被調用執行的時候確定的,而不是在函數定義的時候確定的。

全局上下文

無論是否在嚴格模式下,在全局執行上下文中(在任何函數體外部)this 都指向全局對象。當然具體的全局對象和宿主環境有關。

在瀏覽器中, window 對象同時也是全局對象:

JavaScript 代碼:
  1. console.log(this === window); // true

NodeJS 中,則是 global 對象:

JavaScript 代碼:
  1. console.log(this); // global

函數上下文

由於其運行期綁定的特性,JavaScript 中的 this 含義要豐富得多,它可以是全局對象、當前對象或者任意對象,這完全取決於函數的調用方式。JavaScript 中函數的調用有以下幾種方式:作為函數調用,作為對象方法調用,作為構造函數調用,和使用 applycall 調用。下面我們將按照調用方式的不同,分別討論 this 的含義。

作為函數直接調用

作為函數直接調用時,要注意 2 種情況:

非嚴格模式

在非嚴格模式下執行函數調用,此時 this 默認指向全局對象。

JavaScript 代碼:
  1. function f1(){
  2. return this;
  3. }
  4. //在瀏覽器中:
  5. f1() === window; //在瀏覽器中,全局對象是window
  6.  
  7. //在Node中:
  8. f1() === global;

嚴格模式 『use strict';

在嚴格模式下,this 將保持他進入執行上下文時的值,所以下面的 this 並不會指向全局對象,而是默認為 undefined 。

JavaScript 代碼:
  1. 'use strict'; // 這裡是嚴格模式
  2. function test() {
  3. return this;
  4. };
  5.  
  6. test() === undefined; // true

作為對象的方法調用

在 JavaScript 中,函數也是對象,因此函數可以作為一個對象的屬性,此時該函數被稱為該對象的方法,在使用這種調用方式時,內部的 this 指向該對象。

JavaScript 代碼:
  1. var Obj = {
  2. prop: 37,
  3. getProp: function() {
  4. return this.prop;
  5. }
  6. };
  7.  
  8. console.log(Obj.getProp()); // 37

上面的例子中,當 Obj.getProp() 被調用時,方法內的 this 將指向 Obj 對象。值得注意的是,這種行為根本不受函數定義方式或定義位置的影響。在前面的例子中,我們在定義對象 Obj 的同時,將成員 getProp 定義了一個匿名函數。但是,我們也可以首先定義函數,然後再將其附加到 Obj.getProp 。所以,下面的代碼和上面的例子是等價的:

JavaScript 代碼:
  1. var Obj = {
  2. prop: 37
  3. };
  4.  
  5. function independent() {
  6. return this.prop;
  7. }
  8.  
  9. Obj.getProp = independent;
  10.  
  11. console.log(Obj.getProp()); // logs 37

JavaScript 非常靈活,現在我們把對象的方法賦值給一個變量,然後直接調用這個函數變量又會發生什麼呢?

JavaScript 代碼:
  1. var Obj = {
  2. prop: 37,
  3. getProp: function() {
  4. return this.prop;
  5. }
  6. };
  7.  
  8. var test = Obj.getProp
  9. console.log(test()); // undefined

可以看到,這時候 this 指向全局對象,這個例子 test 只是引用了 Obj.getProp 函數,也就是說這個函數並不作為 Obj 對象的方法調用,所以,它是被當作一個普通函數來直接調用。因此,this 指向全局對象。

一些坑

我們來看看下面這個例子:

JavaScript 代碼:
  1. var prop = 0;
  2. var Obj = {
  3. prop: 37,
  4. getProp: function() {
  5. setTimeout(function() {
  6. console.log(this.prop) // 結果是 0 ,不是37!
  7. },1000)
  8. }
  9. };
  10.  
  11. Obj.getProp();

正如你所見, setTimeout 中的 this 向了全局對象,這裡不是把它當作函數的方法使用嗎?這一點經常讓很多初學者疑惑;這種問題是很多異步回調函數中也會普遍會碰到,通常有個土辦法解決這個問題,比如,我們可以利用 閉包 的特性來處理:

JavaScript 代碼:
  1. var Obj = {
  2. prop: 37,
  3. getProp: function() {
  4. var self = this;
  5. setTimeout(function() {
  6. console.log(self.prop) // 37
  7. },1000)
  8. }
  9. };
  10.  
  11. Obj.getProp();

其實,setTimeoutsetInterval 都只是在全局上下文中執行一個函數而已,即使是在嚴格模式下:

JavaScript 代碼:
  1. 'use strict';
  2.  
  3. function foo() {
  4. console.log(this); // Window
  5. }
  6.  
  7. setTimeout(foo, 1);

記住 setTimeoutsetInterval 都只是在全局上下文中執行一個函數而已,因此 this 指向全局對象。 除非你實用箭頭函數,Function.prototype.bind 方法等辦法修復。至於解決方案會在後續的文章中繼續討論。

作為構造函數調用

JavaScript 支持面向對象式編程,與主流的面向對象式編程語言不同,JavaScript 並沒有類(class)的概念,而是使用基於原型(prototype)的繼承方式。作為又一項約定通用的準則,構造函數以大寫字母開頭,提醒調用者使用正確的方式調用。

當一個函數用作構造函數時(使用 new 關鍵字),它的 this 被綁定到正在構造的新對象,也就是我們常說的實例化出來的對象。

JavaScript 代碼:
  1. function Person(name) {
  2. this.name = name;
  3. }
  4.  
  5. var p = new Person('愚人碼頭');
  6. console.log(p.name); // "愚人碼頭"

幾個陷阱

如果構造函數具有返回對象的 return 語句,則該返回對象將是 new 表達式的結果。

JavaScript 代碼:
  1. function Person(name) {
  2. this.name = name;
  3. return { title : "前端開發" };
  4. }
  5.  
  6. var p = new Person('愚人碼頭');
  7. console.log(p.name); // undefined
  8. console.log(p.title); // "前端開發"

相應的,JavaScript 中的構造函數也很特殊,如果不使用 new 調用,則和普通函數一樣, this 仍然執行全局:

JavaScript 代碼:
  1. function Person(name) {
  2. this.name = name;
  3. console.log(this); // Window
  4. }
  5.  
  6. var p = Person('愚人碼頭');

箭頭函數中的 this

在箭頭函數中,this 與封閉詞法上下文的 this 保持一致,也就是說由上下文確定。

JavaScript 代碼:
  1. var obj = {
  2. x: 10,
  3. foo: function() {
  4. var fn = () => {
  5. return () => {
  6. return () => {
  7. console.log(this); //{x: 10, foo: ƒ} 即 obj
  8. console.log(this.x); //10
  9. }
  10. }
  11. }
  12. fn()()();
  13. }
  14. }
  15. obj.foo();

obj.foo 是一個匿名函數,無論如何, 這個函數中的 this 指向它被創建時的上下文(在上面的例子中,就是 obj 對象)。這同樣適用於在其他函數中創建的箭頭函數:這些箭頭函數的this 被設置為外層執行上下文。

JavaScript 代碼:
  1. // 創建一個含有bar方法的obj對象,bar返回一個函數,這個函數返回它自己的this,
  2. // 這個返回的函數是以箭頭函數創建的,所以它的this被永久綁定到了它外層函數的this。
  3. // bar的值可以在調用中設置,它反過來又設置返回函數的值。
  4. var obj = {
  5. bar: function() {
  6. var x = (() => this);
  7. return x;
  8. }
  9. };
  10.  
  11. // 作為obj對象的一個方法來調用bar,把它的this綁定到obj。
  12. // x所指向的匿名函數賦值給fn。
  13. var fn = obj.bar();
  14.  
  15. // 直接調用fn而不設置this,通常(即不使用箭頭函數的情況)默認為全局對象,若在嚴格模式則為undefined
  16. console.log(fn() === obj); // true
  17.  
  18. // 但是注意,如果你只是引用obj的方法,而沒有調用它(this是在函數調用過程中設置的)
  19. var fn2 = obj.bar;
  20. // 那麼調用箭頭函數後,this指向window,因為它從 bar 繼承了this。
  21. 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 代碼:
  1. var prop = 0;
  2. var Obj = {
  3. prop: 37,
  4. getProp: function() {
  5. setTimeout(function() {
  6. console.log(this.prop) // 結果是 0 ,不是37!
  7. },1000)
  8. }
  9. };
  10.  
  11. Obj.getProp();

通常情況我,我們在這裡期望輸出的結果是 37 ,用箭頭函數解決這個問題相當簡單:

JavaScript 代碼:
  1. var Obj = {
  2. prop: 37,
  3. getProp: function() {
  4. setTimeout(() => {
  5. console.log(this.prop) // 37
  6. },1000)
  7. }
  8. };
  9.  
  10. Obj.getProp();

原型鏈中的 this

相同的概念在定義在原型鏈中的方法也是一致的。如果該方法存在於一個對象的原型鏈上,那麼 this 指向的是調用這個方法的對象,就好像該方法本來就存在於這個對象上。

JavaScript 代碼:
  1. var o = {
  2. f : function(){
  3. return this.a + this.b;
  4. }
  5. };
  6. var p = Object.create(o);
  7. p.a = 1;
  8. p.b = 4;
  9.  
  10. console.log(p.f()); // 5

在這個例子中,對象 p 沒有屬於它自己的f屬性,它的f屬性繼承自它的原型。但是這對於最終在 o 中找到 f 屬性的查找過程來說沒有關係;查找過程首先從 p.f 的引用開始,所以函數中的 this 指向 p 。也就是說,因為f是作為p的方法調用的,所以它的this 指向了 p 。這是 JavaScript 的原型繼承中的一個有趣的特性。

你也會看到下面這種形式的老代碼,道理是一樣的:

JavaScript 代碼:
  1. function Person(name) {
  2. this.name = name;
  3. }
  4. Person.prototype = {
  5. getName:function () {
  6. return this.name
  7. }
  8. };
  9. var p = new Person('愚人碼頭');
  10. console.log(p.getName()); // "愚人碼頭"

getter 與 setter 中的 this

再次,相同的概念也適用時的函數作為一個 getter 或者 一個 setter 調用。用作 gettersetter 的函數都會把 this 綁定到正在設置或獲取屬性的對象。

JavaScript 代碼:
  1. function sum() {
  2. return this.a + this.b + this.c;
  3. }
  4.  
  5. var o = {
  6. a: 1,
  7. b: 2,
  8. c: 3,
  9. get average() {
  10. return (this.a + this.b + this.c) / 3;
  11. }
  12. };
  13.  
  14. Object.defineProperty(o, 'sum', {
  15. get: sum, enumerable: true, configurable: true});
  16.  
  17. console.log(o.average, o.sum); // logs 2, 6

作為一個DOM事件處理函數

當函數被用作事件處理函數時,它的 this 指向觸發事件的元素(一些瀏覽器在使用非addEventListener 的函數動態添加監聽函數時不遵守這個約定)。

JavaScript 代碼:
  1. // 被調用時,將關聯的元素變成藍色
  2. function bluify(e){
  3. console.log(this === e.currentTarget); // 總是 true
  4.  
  5. // 當 currentTarget 和 target 是同一個對象是為 true
  6. console.log(this === e.target);
  7. this.style.backgroundColor = '#A5D9F3';
  8. }
  9.  
  10. // 獲取文檔中的所有元素的列表
  11. var elements = document.getElementsByTagName('*');
  12.  
  13. // 將bluify作為元素的點擊監聽函數,當元素被點擊時,就會變成藍色
  14. for(var i=0 ; i < elements.length; i++){
  15. elements[i].addEventListener('click', bluify, false);
  16. }

作為一個內聯事件處理函數

當代碼被內聯on-event 處理函數調用時,它的this指向監聽器所在的DOM元素:

HTML 代碼:
  1. <button onclick="alert(this.tagName.toLowerCase());">
  2. Show this
  3. </button>

上面的 alert 會顯示 button 。注意只有外層代碼中的 this 是這樣設置的:

HTML 代碼:
  1. <button onclick="alert((function(){return this})());">
  2. Show inner this
  3. </button>

在這種情況下,沒有設置內部函數的 this,所以它指向 global/window 對象(即非嚴格模式下調用的函數未設置 this 時指向的默認對象)。

使用 apply 或 call 調用

JavaScript 中函數也是對象,對象則有方法,applycall 就是函數對象的方法。這兩個方法異常強大,他們允許切換函數執行的上下文環境(context),即 this 綁定的對象。很多 JavaScript 中的技巧以及類庫都用到了該方法。讓我們看一個具體的例子:

JavaScript 代碼:
  1. function Point(x, y){
  2. this.x = x;
  3. this.y = y;
  4. this.moveTo = function(x, y){
  5. this.x = x;
  6. this.y = y;
  7. }
  8. }
  9. var p1 = new Point(0, 0);
  10. p1.moveTo(1, 1);
  11. console.log(p1.x,p1.y); //1 1
  12.  
  13. var p2 = {x: 0, y: 0};
  14. p1.moveTo.apply(p2, [10, 10]);
  15. console.log(p2.x,p2.y); //10 10

在上面的例子中,我們使用構造函數生成了一個對象 p1,該對象同時具有 moveTo 方法;使用對象字面量創建了另一個對象 p2,我們看到使用 apply 可以將 p1 的方法 apply 到 p2 上,這時候 this 也被綁定到對象 p2 上。另一個方法 call 也具備同樣功能,不同的是最後的參數不是作為一個數組統一傳入,而是分開傳入的:

JavaScript 代碼:
  1. function Point(x, y){
  2. this.x = x;
  3. this.y = y;
  4. this.moveTo = function(x, y){
  5. this.x = x;
  6. this.y = y;
  7. }
  8. }
  9. var p1 = new Point(0, 0);
  10. p1.moveTo(1, 1);
  11. console.log(p1.x,p1.y); //1 1
  12.  
  13. var p2 = {x: 0, y: 0};
  14. p1.moveTo.call(p2, 10, 10); // 只是參數不同
  15. console.log(p2.x,p2.y); //10 10

.bind() 方法

ECMAScript 5 引入了 Function.prototype.bind 。調用 f.bind(someObject) 會創建一個與 f 具有相同函數體和作用域的函數,但是在這個新函數中,this 將永久地被綁定到了 bind 的第一個參數,無論這個函數是如何被調用的。

JavaScript 代碼:
  1. function f(){
  2. return this.a;
  3. }
  4.  
  5. //this被固定到了傳入的對象上
  6. var g = f.bind({a:"azerty"});
  7. console.log(g()); // azerty
  8.  
  9. var h = g.bind({a:'yoo'}); //bind只生效一次!
  10. console.log(h()); // azerty
  11.  
  12. var o = {a:37, f:f, g:g, h:h};
  13. console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

填坑

上面我們已經講了使用箭頭函數填 setTimeout 的坑,這次我們使用 bind 方法來試試:

JavaScript 代碼:
  1. var prop = 0;
  2. var Obj = {
  3. prop: 37,
  4. getProp: function() {
  5. setTimeout(function() {
  6. console.log(this.prop) // 37
  7. }.bind(Obj),1000)
  8. }
  9. };
  10.  
  11. Obj.getProp();

同樣可以填坑,但是看上去沒有使用箭頭函數來的優雅。

小結

本文介紹了 JavaScript 中的 this 關鍵字在各種情況下的含義,雖然這只是 JavaScript 中一個很小的概念,但借此我們可以深入瞭解 JavaScript 中函數的執行環境,而這是理解閉包等其他概念的基礎。掌握了這些概念,才能充分發揮 JavaScript 的特點,才會發現 JavaScript 語言特性的強大。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
深入理解JavaScript中this到底指的是什么!
深入浅出妙用 Javascript 中 apply、call、bind
Javascript中函数调用和this的关系
一篇文章带你了解JavaScript this关键字
JavaScript 中的 this 并不难
javascript工厂模式,调用的方法
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服