smartsrh

为初学者介绍 JavaScript 中的 this 关键字

smartsrh · 2017-06-04翻译 · 379阅读 原文链接

JavaScript 中的 this 关键字在不同情况下指的是什么,很多时候会有点复杂。幸运的是,有五个规则可以用来确定 this。尽管有些情况下这些规则不适用,但应该可以在绝大多数情况下解决我们的困惑。所以,让我们开始吧!

  1. this 通常由函数的 执行上下文 决定。执行上下文是指一个函数是 如何 被调用的。

  2. 很重要的是一个函数每次被调用时,函数中的 this 可能会指向不同的东西。

  3. 如果现在还不能理解上面两点的含义,没有关系,本文结尾时在总结里还会提到。

#1 全局对象

现在让我们在实践中学习。 打开 Chrome 开发者控制台(Windows:Ctrl + Shift + J)(Mac:Cmd + Option + J)并键入以下内容:

console.log(this);

我们得到了什么

// Window {...}

window 对象!这是因为在全局范围内,this 是指全局对象。在浏览器中,全局对象是 window 对象。

为了帮助我们更好地理解为什么 thiswindow 对象,我们来深入了解一下。在控制台中,创建一个新变量并命名为 myName

var myName = 'Brandon';

我们可以通过调用 myName 再次访问这个变量:

myName
// returns -> 'Brandon'

但是你知道在全局范围内声明的每个变量都附加在“window”对象上吗?让我们测试一下:

window.myName
// returns -> 'Brandon'
window.myName === myName
// returns -> true

所以当我们在全局环境中运行 console.log(this) 的时候,我们知道这个 this 指向全局对象。 由于浏览器中的全局对象是“window”对象,所以这样的输出结果是有道理的:

console.log(this)
// returns -> window{...}

现在让我们把这个 this 放在一个函数里。回想一下我们以前的定义:this 的值通常由函数 如何 被调用而确定。考虑到这一点,你期望这个函数返回什么?在浏览器控制台中,复制下面的代码,然后按回车键。

function test() {
  return this;
}
test()

关键字 this 再次指向了全局(window)对象。这是因为关键字 this 不在一个已声明的对象内,因此默认指向全局(window)对象。这个概念现在可能有点难以理解,但随着进一步阅读理解会更加深入。有一件事要注意,如果使用严格模式,在上面的例子中,这个 this 将是 undefined

#2 已声明的对象

当关键字 this 在已声明的对象内部使用时,this 指向离被调用的方法 最近的父对象。 看看下面的代码,声明了对象 person,并在方法 full 中使用 this

var person = {
  first: 'John',
  last: 'Smith',  
  full: function() {
    console.log(this.first + ' ' + this.last);
  }
};
person.full();
// logs => 'John Smith'

为了更好地说明 this 实际上是引用该对象的,请将以下代码复制到浏览器控制台中。它与上面的代码大致相同,只是执行了 console.log(this) ,所以我们可以看它到底返回什么。

var person = {
  first: 'John',
  last: 'Smith',  
  full: function() {
    console.log(this);
  }
};
person.full();
// logs => Object {first: "John", last: "Smith", full: function}

正如所看到的,控制台返回了 person 对象,证明 this 已经指向了 person

还记得刚刚说过 this 指向离被调用方法 最近的父对象 吗?在我们有嵌套对象的情况下会发生什么?看下面的示例代码。我们有一个 person 对象,包含与之前相同的 firstlastfull 键。这一次,我们还嵌套了一个 personTwo 对象。personTwo 包含相同的三个键。

var person = {
  first: 'John',
  last: 'Smith',
  full: function() {
    console.log(this.first + ' ' + this.last);
  },
  personTwo: {
    first: 'Allison',
    last: 'Jones',
    full: function() {
      console.log(this.first + ' ' + this.last);
    }
  }
};

当我们分别调用两个 full 方法时会发生什么?我们来看看吧。

person.full();
// logs => 'John Smith'
person.personTwo.full();
// logs => 'Allison Jones'

this 指向离被调用方法 最近的父对象 。当 person.full() 被调用时,函数里的 this 被绑定到 person 对象。同时,当 person.personTwo.full() 被调用时,在 full 函数内,this 被绑定到 personTwo 对象!

#3 New 关键字

当使用 new 关键字(一个构造函数)时,this 绑定到正在创建的那个新对象。

让我们来看一个例子:

function Car(make, model) {
  this.make = make;
  this.model = model;
};

你可能会猜测 this 应该绑定到全局对象 - 如果我们不用关键字 new 的话,你就是正确的。 当我们使用 new 时,this 的值会指向一个空对象,在本例中是 myCar

var myCar = new Car('Ford', 'Escape');
console.log(myCar);
// logs => Car {make: "Ford", model: "Escape"}

为了理解以上的例子,你需要了解什么是 new 关键字,但这其实是另一个全新的话题。 所以现在,如果你不确定,就看到关键字 new,只要知道 this 指向的是一个全新的空白对象就可以了。

#4 CallBindApply

最后,但同样很重要的是,我们可以使用 call()bind()apply() 来明确地设置 this 的值。 这三个是非常相似的,但重要的是了解其中的小区别。

call()apply() 都会立即被调用。call() 可以接受任意数量的参数:this 和之后其他参数。apply() 只需要两个参数:this 和一个包含附加参数的数组。

有木有跟上我的思路?下面这个例子应该会解释得更清楚。看下面的代码,我们正在尝试对几个数求和。将其复制到浏览器控制台并调用该函数。

function add(c, d) {
  console.log(this.a + this.b + c + d);
}
add(3,4);
// logs => NaN

add 函数输出 NaN(不是一个数字)。这是因为 this.athis.b 未定义。他们不存在 而且不能将数字喝未定义的东西求和。

让我们添加一个新的对象。我们可以使用 call()apply() 在我们的对象的作用域中调用该函数:

function add(c, d) {
  console.log(this.a + this.b + c + d);
}
var ten = {a: 1, b: 2};
add.call(ten, 3, 4);
// logs => 10
add.apply(ten, [3,4]);
// logs => 10

当我们使用 add.call() 时,第一个参数表示 this 被绑定到该参数。随后的参数被传递到我们调用的函数中。因此,在 add() 中,this.a 指的是 ten.athis.b 指的是 ten.b,我们得到 1 + 2 + 3 + 4 的返回结果,也就是 10。

add.apply() 是类似的。第一个参数表示 this 被绑定到该参数。 后续的参数是要在函数中使用的参数组成的数组。

bind() 又是什么呢?bind() 中的参数与 call() 相同,但是 bind() 不会立即被调用。相反, bind() 返回一个上下文 this 已经绑定为第一个参数的函数。因此,当我们最开始不知道运行时需要的所有参数时,就可以用 bind()。下面这个例子应该会解释得更清楚:

var small = {
  a: 1,
  go: function(b,c,d){
    console.log(this.a+b+c+d);
  }
}
var large = {
  a: 100
}

将上述内容复制到控制台。 然后调用:

small.go(2,3,4);
// logs 1+2+3+4 => 10

这里没什么新鲜的。但是,如果我们想要直接使用 large.a 的值呢?我们可以使用 call()/apply()

small.go.call(large,2,3,4);
// logs 100+2+3+4 => 109

现在,如果我们不知道所有 3 个参数呢?我们可以使用 bind()

var bindTest = small.go.bind(large,2);

如果我们在控制台输出我们上面的变量 bindTest,我们可以看到真相

console.log(bindTest);
// logs => function (b,c,d){console.log(this.a+b+c+d);}

记住,bind 会返回一个已经 this 已经被绑定的函数!所以我们的 this 已经成功地绑定到我们的 large 对象上。我们也已经传递了第二个参数 2。 后来,当知道其它参数后时,我们可以接着将它们传递进函数:

bindTest(3,4);
// logs 100+2+3+4 => 109

为了更加清晰,这里将所有的代码放在一个代码块中。通看一遍,并将其复制到控制台,去真正了解发生了什么!

var small = {
  a: 1,
  go: function(b,c,d){
    console.log(this.a+b+c+d);
  }
}

var large = {
  a: 100
}

small.go(2,3,4);
// logs 1+2+3+4 => 10

var bindTest = small.go.bind(large,2);

console.log(bindTest);
// logs => function (b,c,d){console.log(this.a+b+c+d);}

bindTest(3,4);
// logs 100+2+3+4 => 109

#5 箭头函数

这本身就是一个很大的话题,因此我写了一篇文章: 为初学者介绍箭头函数

总结

为自己鼓掌吧!在大多数情况下,你现在应该可以推断出 this 是指向什么了!记住以下几件事:

  1. this 通常由函数的 执行上下文 决定。

  2. 在全局范围内,this 指的是全局对象(window 对象)。

  3. 当使用 new 关键字(一个构造函数)时,this 绑定到正在创建的新对象。

  4. 我们可以用 call()bind()apply() 来明确设置 this 的值。

  5. 箭头函数不影响 this —— this 的绑定由函数的 执行上下文 决定(即基于原始上下文)。

相关文章