首頁  >  文章  >  web前端  >  精通JavaScript的this關鍵字_javascript技巧

精通JavaScript的this關鍵字_javascript技巧

WBOY
WBOY原創
2016-05-16 15:13:181080瀏覽

JS中的this關鍵字讓許多新舊JS開發人員都感到困惑。這篇文章將對this關鍵字進行完整地闡述。讀完本文以後,您的困惑將全部消除。您將學會如何在各種不同的情形上正確運用this。

我們和在英文、法文這樣的自然語言中使用名詞一樣地使用this。比如,「John飛快地跑著,因為他想追上火車」。請注意這句話中的代指John的代名詞「他」。我們原本也可以這樣表達,「John飛快地跑著,因為John想追上火車」。按照正常的語言習慣,我們並沒有用第二種方式表達。如果我們真照第二種方式說話,我們的家人和基友一定會把我們當成怪胎。說不定不只家人,甚至連我們的酒肉朋友和同事都會遠離我們。類似地,在JS中,我們把this關鍵字當成一種快捷方式,或者說是引用(referent)。 this關鍵字指向的是目前上下文(context,下文中將會對此作專門的解釋)的主體(subject),或是目前正在執行的程式碼區塊的主體。

考慮以下程式碼:

var person = {
   firstName: "Penelope",
   lastName: "Barrymore",
   fullName: function () {
   // 正如我们在文中提到的使用“他”作为代名词一样,我们在这里使用this

   console.log(this.firstName + " " + this.lastName);
   //我们其实也可以这样写:
   console.log(person.firstName + " " + person.lastName);
   }
  }

如果我們使用person.firstName和person.lastName,程式碼會在某些情況下變得模稜兩可。例如,全域變數中有一個跟person同名的變數名稱。在這種情況下,如果我們想要讀取person.firstName的值,系統將有可能從全域變數的person變數中讀取firstName屬性(property)。這樣一來,我們調試程式碼的時候很難發現錯誤。所以this不只起到美化程式碼的作用,同時也是為了確保程式的準確性。這種做法其實和前面講到的「他」的用法一樣,使得我們的程式碼更加清晰。 「他」所引用的正是句首的「John」。

正如代名詞「他」被用來代指句中的先行詞(先行詞就是代名詞所指示的名詞),this關鍵字以同樣的方式來引用當前方法(function,也可以稱為函數)所綁定(bound)的物件。 this不只引用對象,同時包含了對象的值。跟先行詞一樣,this也可以被理解成在上下文中用來引用當前物件(又叫作「先行詞物件」)的捷徑(或者來適度減少歧義的替代品)。我們遲些會專門講解「上下文」。

this關鍵字基本理論

首先我們得知道,物件(Object)有屬性集(properties),所有的方法(function)也有屬性集。運行到某個方法的時候就有了一個this屬性—一個儲存了呼叫該方法(準確地說是使用了this關鍵字的方法)的物件的值的變數。

this關鍵字總是指向一個物件並持有這個物件的值,儘管它可以出現在全域範圍(global scope)方法(函數)以外的地方,但它通常出現在方法體中。值得注意的是,如果我們使用嚴格模式(strict mode),並在全域方法(global functions)或沒有綁定到任何物件的匿名方法中使用this關鍵字時,this將會指向undefined。

this被用在方法體中,例如方法A,它將指向呼叫方法A的物件的值。並不是任何情況我們都能找到呼叫方法A的物件名,這時候就用this來存取呼叫方法A的物件所擁有的方法和屬性。 this確實只是一個用來引用先行詞—呼叫方法的物件—的捷徑。

我們來仔細體會下面這段使用this的程式碼。

var person = {
  firstName: "Penelope",
  lastName: "Barrymore",
  //this用在showFullName方法中,而showFullName定义在person对象中,由于调用showFullName的是person这个对象,所以this拥有person的值

  showFullName: function() {
    console.log(this.firstName + " " + this.lastName);
  }
}
person.showFullName(); // 结果:Penelope Barrymore

再考慮下面這段使用了this的jQuery範例。

//这是一段很简单的jQuery代码

$("button").click(function(event) {
  // $(this) 会指向$("button")对象
  // 因为$("button")对象调用click方法
  console.log($(this).prop("name"));
});

我想详细地说一下上面这个jQuery示例:$(this)的使用,这是this的jQuery版本,它用于匿名方法中,这个匿名方法在button的单击事件里执行。这里之所以说$(this)被绑定到button对象,是因为jQuery库把$(this)绑定到调用click方法的对象上。因此,尽管$(this)被定义在一个自身无法访问“自身”变量的匿名方法里,$(this)仍会指向button对象。

请注意,button是一个HTML页面的DOM元素,它同时是一个对象:在上面的例子中,因为我们把它包装在了jQuery的$()方法里,所以它是一个jQuery对象。

this关键字的核心

下面这条规则可以帮助你彻底搞懂this关键字:如果一个方法内部使用了this关键字,仅当对象调用该方法时this关键字才会被赋值。我们估且把使用了this关键字的方法称为“this方法”。

尽管看上去this引用了它在代码中所存在于的对向,事实上在方法被调用之前它并没有被赋值,而赋给它的值又严格地依赖于实际调用“this方法”的对象。this通常会被赋予调用对象的值,下面有一些特殊情况。

全局范围里的this

在全局域中,代码在浏览器里执行,所有变量和方法都属于window对象。因而当我们在全局域中使用this关键字的时候,它会被指向(并拥有)全局变量window对象。如前所述,严格模式除外。window对象是JS一个程序或一张网页的主容器。

因而:

var firstName = "Peter",
lastName = "Ally";

function showFullName() {
  //在这个方法中,this将指向window对象。因为showFullName()出现在全局域里。

  console.log(this.firstName + " " + this.lastName);
}

var person = {
  firstName: "Penelope",
  lastName: "Barrymore",
  showFullName: function() {
    //下面这行代码,this指向person对象,因为showFullName方法会被person对象调用。
    console.log(this.firstName + " " + this.lastName);
  }
}

showFullName(); // Peter Ally

//window对象包含了所有的全局变量和方法,因而会有以下输出
window.showFullName(); // Peter Ally

//使用了this关键字的showFullName方法定义在person对象里,this关键字指向person对象,因以会有以下输出
person.showFullName(); // Penelope Barrymore

对付this有绝招

当方法内使用了this关键字时,这几种情况最容易引起误解:方法被借用;把方法赋值给某个变量;方法被用作回调函数(callback),被作为参数传递;this所在的方法是个闭包(该方法是一个内部方法)。针对这几种情况,我们将逐一攻破。在此之前,我们先简单介绍一下“上下文”(context)。

JS当中的上下文跟这句话中的主语(subject)类似:“John是赢家,他还了钱”。这句话的主语是John。我们也可以说这句话的上下文是John,因为我们在这句话中关注的是John,即使这里有一个“他”字来代指John这个先行词。正如我们可以使用分号来切换句子的主语一样,通过使用不同的对象来对方法进行调用,当前的上下文对象同样可以被切换。

类似地,以下JS代码:

var person = {
  firstName: "Penelope",
  lastName: "Barrymore",
  showFullName: function() {
    // 上下文
    console.log(this.firstName + " " + this.lastName);
  }
}

//使用person对象调用showFullName的时候,上下文是person对象
//showFullName内部的this指向person对象
person.showFullName(); // Penelope Barrymore
//如果我们用不同的对象来调用showFullName
var anotherPerson = {
  firstName: "Rohit",
  lastName: "Khan"
};

//我们可以使用apply方法来显式设置this的值—迟些我们会讲到apply方法
//this会指向任何一个调用了this方法的对象,因此会有以下输出结果
person.showFullName.apply(anotherPerson); // Rohit Khan
//所以现在的上下文是anotherPerson,因为anotherPerson通过借助使用apply方法间接调用了person的showFullName方法

现在我们开始正式讨论应付this关键字的绝招,例子里包含了this所引发的错误以及解决方案。

1.当this被用作回调函数传入其它方法

当我们把一个使用了this关键字的方法当成参数作为回函数的时候,麻烦就来了。例如:

//以下是一个简单的对象,我们定义了一个clickHandler方法。我们想让这个方法在页面上某个button被单击时执行。
var user = {
  data: [{
    name: "T. Woods",
    age: 37
  },
  {
    name: "P. Mickelson",
    age: 43
  }],
  clickHandler: function(event) {
    var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 随机返回0或1
    //下面这行代码会从数组data里随机打印姓名和年龄
    console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
  }
}

//button对象被jQuery的$方法包装,现在变成一个jQuery对象
//所以输出结果是undefined,因为button对象没有data这个属性
$("button").click(user.clickHandler); // 无法读取未定义的属性

上面的代码中,我们把user.clickHandler当成回调函数传入$(“button”)对象的click事件,user.clickHandler中的this将不再指向user对象转。谁调用了这个包含this的方法this就会指向谁。真正调用user.clickHandler的对象是button对象—user.clickHandler会在button对象的单击方法里执行。

注意,尽管我们使用user.clickHandler来调用clickHander方法(我们也只能这么做,因为clickHandler是定义在user对象上的),clickHandler方法本身会被现在被this所指向的上下文对象所调用。所以this现在指向的是$(“button”)对象。

当上下文改变时—当我们在其它对象而非原对象上执行某个方法的时候,显然this关键字不再指向定义了this关键字的原对象。

解决方案:

由于我们真的很想让this.data指向user对象的data属性,我们可以使用Bind/ Apply/ Call等方法来强制改变this所指向的对象。本系列的其它篇目将专门对Bind/ Apply/ Call进行讲解,文中介绍了如何在不同的情况强制改变this的值的方法。与其在本文大篇幅讨论,我强烈建议大家直接去读另外的篇目(译者注:晚些时候放出这里所说的“其它篇目”)。

为了解决前面代码中的问题,我们可以使用bind方法。

针对下面这行代码:

$ ("button").click (user.clickHandler);
我们可以用bind方法把clickHandler绑定的user对象上:

$("button").click (user.clickHandler.bind (user)); // P. Mickelson 43
2.闭包中的this

在内部方法中,或者说闭包中使用this,是另一个很容易被误解的例子。我们必须注意的是,内部方法不能直接通过使用this关键字来访问外部方法的this变量,因为this变量 只能被特定的方法本身使用。例如:

var user = {
  tournament: "The Masters",
  data: [{
    name: "T. Woods",
    age: 37
  },
  {
    name: "P. Mickelson",
    age: 43
  }],

  clickHandler: function() {
    //在里用this.data没有太大问题,因为this指向的是user对象,data是user的一个属性
    this.data.forEach(function(person) {
      //但是在这个匿名方法(作为参数被传进forEach方法的这个方法)里,this不再指向user对象
      //内部方法无法访问外部方法的this
      console.log("What is This referring to? " + this); //输出结果为:[object Window]
      console.log(person.name + " is playing at " + this.tournament);
      // T. Woods is playing at undefined
      // P. Mickelson is playing at undefined
    })
  }

}

user.clickHandler(); // What is "this" referring to? [object Window]

因为匿名方法中的this不能访问外部方法的this,所以在非严格模式下,this指向了全局的window对象

解决方案:

在进入forEach方法之前,额外使用一个变量来引用this。

var user = {
  tournament: "The Masters",
  data: [{
    name: "T. Woods",
    age: 37
  },
  {
    name: "P. Mickelson",
    age: 43
  }],

  clickHandler: function(event) {
    //为了捕获this指向user对象时的值,我们把它赋值给另外一个变量theUserObj,后面我们可以使用theUserObj
    var theUserObj = this;
    this.data.forEach(function(person) {
      //现在我们不用this.tournament了,我们用theUserObj.tournament
      console.log(person.name + " is playing at " + theUserObj.tournament);
    })
  }

}

user.clickHandler();
// T. Woods is playing at The Masters
// P. Mickelson is playing at The Masters

正如下面的代码,很多JS开发人员喜欢使用变量that来设置this的值。但我个人不太喜欢用that这个名字,我喜欢使用让人一眼就能看懂this到底指向谁的那种名字,所以上面的代码中我使用了theUserObj = this。

 // 这句代码对大多数JS开发人员来说再常见不过了
var that = this;

3.方法被赋值给某个变量

this关键字有时候很调皮,如果我们把一个使用了this关键字的方法赋值给一个变量,我们来看会有什么有趣的事发生:

// data变量是一个全局变量
var data = [{
    name: "Samantha",
    age: 12
},
{
    name: "Alexis",
    age: 14
}];

var user = {
    // 而这里的data是user的一个属性
    data: [{
        name: "T. Woods",
        age: 37
    },
    {
        name: "P. Mickelson",
        age: 43
    }],
    showData: function(event) {
        var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 随机生成1或0
        //这句话会从数组data里随机显示人名和岁数
        console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
    }

}

// 把user.showData方法赋值给变量 showUserData
var showUserData = user.showData;

//执行showUserData方法,结果将 来自全局的data数组而非user对象的data属性
showUserData(); // Samantha 12 (来自全局变量data)
//解决方案:通过使用bind方法来显式设置this的值
//把showData方法绑定到user对象上
var showUserData = user.showData.bind(user);

//现在结果将来自user对象,因为this关键字已经被强制绑定到user对象上了
showUserData(); // P. Mickelson 43

4.借用方法带来的问题

JS开发中,借用方法(borrowing methods)很常见。

我们来看下面的代码:

//下面代码中有两个对象。其中一个定义了avg方法,另一个不包含avg的定义。我们用另一个对象来借用前一对象的avg方法。
var gameController = {
  scores: [20, 34, 55, 46, 77],
  avgScore: null,
  players: [{
    name: "Tommy",
    playerID: 987,
    age: 23
  },
  {
    name: "Pau",
    playerID: 87,
    age: 33
  }]
}

var appController = {
  scores: [900, 845, 809, 950],
  avgScore: null,
  avg: function() {

    var sumOfScores = this.scores.reduce(function(prev, cur, index, array) {
      return prev + cur;
    });

    this.avgScore = sumOfScores / this.scores.length;
  }
}

//如果执行下面的代码,gameController.avgScore属性的实际取值将由appController的scores而来
//不要执行下面的代码,我们只是为了对这种情况进行说明。实际上我们想让appController.avgScore仍然为null。
gameController.avgScore = appController.avg();

avg方法的this关键字指向的是gameController对象,如果使用appController调用该方法,this将会指向appController(但事实上这并不是我们期望的结果,因为我们只想借用方法的实现逻辑而非具体的数据来源)。

解决方案:

为了保证gameController只借用appController的avg方法的逻辑,我们使用apply方法:

// 我们要使用apply方法,注意这里传入appController.avg方法的第二个参数
appController.avg.apply(gameController, gameController.scores);

//尽管avg方法是借来的,但是现在avgScore属性已经被成功地应用到gameController上了。
console.log(gameController.avgScore); // 46.4
//appController.avgScore仍然是null,只有gameController的avgScore被更新了
console.log(appController.avgScore); // null

gameController只借用了appController的avg方法,这时this将指向gameController,因为我们把gameController作为apply方法的第一个参数进行传递。apply方法的第一个参数将会显式设置this的取值。

结语

希望您在文中有所收获。现在你可以使用文中介绍的绝招(bind方法,apply方法,call方法,以及把this赋值给 一个变量)来对付跟this相关的任何问题。

正如已经了解到的,this在上下文改变、被作为回调函数使用、被不同的对象调用、或者方法被借用的情况下,this将一直指向调用当前方法的对象。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn