06月16, 2017

JavaScript: 为初学者介绍 this 关键字

原文:https://hackernoon.com/javascript-the-keyword-this-for-beginners-fb5238d99f85

alt

理解 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 对象。

为帮助更好地理解为什么 this 指向 window 对象,下面我们更深入来了解一下。在控制台中,创建一个新变量 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 实际上就是引用 person 对象,请将如下代码复制到浏览器控制台中。它与上面的代码大致相同,只是用 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 Call、Bind 和 Apply

最后一个要点,我们实际上是可以用 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 都是 undefined,二者都是不存在的。而我们不能把数字与未定义的东西相加。

下面我们给等式引入一个对象。我们可以用 call()apply() 在我们的对象上调用 add 函数:

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() 不是马上被调用,而是返回一个 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是词法绑定的(即,基于原始上下文)

本文链接:http://www.xiaojichao.com/post/this.html

-- EOF --

Comments