this作为JavaScript中最难理解的知识点之一,掌握了this的用法就相当于JavaScript入门,所以掌握this的指向对我们的开发很有好处。

this的指向

  this允许在复用不同的函数时调用不同的函数上下文,所以我们在判断this的指向时首先应该看得就是函数在哪儿被调用,在ES5中,this的指向始终坚持一个原理:this永远指向最后调用函数的那个对象,其实当你理解了这句话,基本上就已经理解了this的使用。
  在《JavaScript权威指南》一书中,对this有明确的定义

this是一个关键字,不是变量,也不是属性名,JavaScript的语法不允许给this赋值,和变量不同的是,this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this,如果嵌套函数作为方法调用,其this的值指向调用它的对象。

为了能更好的理解this的指向,下面我们来看几个例子:
例1:

1
2
3
4
5
6
7
var a = 1;
function b () {
var a = 2;
console.log(this.a); // 1
}

b();

在上面的例子中,我们看到最后调用函数b()的前面没有调用对象,那么调用对象就是全局对象(此时为window), 相当于window.b(), 而this又指向最后调用它的那个对象,所以最后输出1;
注意: 非严格模式下全局对象为window, 在严格模式下全局对象就是undefined, 此时调用控制台报错: Uncaught TypeError: Cannot read property ‘name’ of undefined

例2:

1
2
3
4
5
6
7
8
9
var name = java;
var b = {
name: javaScript,
fn: function () {
console.log(this.name)
}
}

b.fn(); // javaScript

通过以上两个例子的对比, 相信你很快就能理解this的指向问题了, 上面的例子中最后调用函数fn()的是对象b, 所以对象理所当然的指向b, 最后输出name的值即为JavaScript。

下面我们在做一个小小的改动
例3:

1
2
3
4
5
6
7
8
9
var name = java;
var b = {
name: javaScript,
fn: function () {
console.log(this.name)
}
}

window.b.fn(); // javaScript

这里打印的结果不变的原因还是因为this永远指向最后调用它的那个对象,所以this还是指向b。

下面我们再来看一个比较容易出错的例子
例4:

1
2
3
4
5
6
7
8
9
var name = java;
var b = {
name: javaScript,
fn: function () {
console.log(this.name) // javaScript
}
}
var c = b.fn();
c();

看到这儿你也许会疑惑, 为什么此时输出不是java? 这里涉及到另外一个知识点了–this的指向永远是在运行时确定的。所以我们再看一下,由于刚定义的时候函数并没有被调用,当调用函数的时候,函数还是被window对象调用,所以this的指向也就是window。
相信看到这儿,你应该已经知道如何来判断this的指向了。下面我们来看看如何改变this的指向。

怎么改变this的指向

改变this的指向总结了下有以下几种方法:

  • 在函数内部将this的值保存在一个变量中
  • 使用es6 箭头函数
  • 使用new 关键字
  • 使用call、apply、bind

在函数内部将this的值保存在一个变量中

这种方法应该是最容易理解的了,我们只需要将this的值保存在一个变量_this中,然后在函数中都是用这个_this, 这样不管怎么调用函数_this都不会变
例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var o = {
m: function () {
var _this = this;
console.log(this === o); // true
f();

function f () {
console.log(this === o); // false
console.log(_this === o); // true
}
}
}

o.m();

在这个例子中,首先函数内部的this指向对象o,并且我们将this的值保存在变量_this中,所以console.log(this === o)打印true,但是在函数f()内部,并没有明确的对象调用函数f(),按照我们上面的说法,此时调用f()的就是全局对象window(this指向window), 所以console.log(this === o) 打印结果为 false, 在函数f()内部, _this的值却还是外部函数this的值并没有发生改变仍为对象o, 所以console.log(_this === o) 的结果为true

使用es6的箭头函数

关于es6的箭头函数, 所涉及到的知识点太多, 另开一篇博客讲解…

使用new 关键字

JavaScript的new关键字是众多运算符其中的一个, 让我们来看看下面一个例子:
例6:

1
2
3
4
5
6
7
8
function Person () {
this.name = 'Jeremy';
this.age = 24;
}

var a = new Person();
console.log(a.name);
console.log(a.age)

每当用new调用函数时, JavaScript解释器都会在底层创建一个全新的对象并把这个对象当做this。如果用new调用一个函数, this会自然地引用解释器创建的新对象。

使用call、apply、bind

下面先来看一个例子
例7:

1
2
3
4
5
6
7
function fn () {
console.log(`My name is ${this.name}`)
}
const user = {
name: 'Jeremy',
age: 24
}

使用call

如果fn()不是user的函数,知识一个独立的函数,那我们怎么才能fn()函数调用的this指向user对象呢?当然我们不能像以前那样简单的使用user.fn(),因为此时fn()不是user的函数。这个时候call出现了

“call” 是每个函数都有的一个方法,它允许你在调用函数时为函数指定上下文。

对于例7,我们可以这样调用

1
fn.call(user)

强调一下,call是每个函数都有的一个属性。并且call通常可以传递多个参数,默认第一个参数会作为函数调用时的上下文, 换句话说, this将会指向传递给call的第一个参数。
例8:

1
2
3
4
5
6
7
8
9
10
function fn (age, language) {
console.log(`My name is ${this.name} and My age is ${age}`);
// My name is Jeremy and My age is 24
console.log(`My favorite language is ${language}`)
// My favorite language is JavaScript
}
const user = {
name: 'Jeremy'
}
fn.call(user, 24, 'JavaScript')

虽然call()能够有效的改变函数执行上下文,但你可能也注意到了, 如果有多个参数,就需要一个一个的传递, 这样显得比较麻烦。所以这个时候apply()方法就出来了。

使用apply

call方法和apply方法作用是一样的, 都是在特定的作用域中调用函数, 等于设置了函数体内this对象的值,以扩充函数赖以运行的作用域。
下面还是来看一个例子:
例9:

1
2
3
4
5
6
7
8
9
10
function fn (age, language) {
console.log(`My name is ${this.name} and My age is ${age}`);
// My name is Jeremy and My age is 24
console.log(`My favorite language is ${language}`)
// My favorite language is JavaScript
}
const user = {
name: 'Jeremy'
}
fn.apply(user, [24, 'JavaScript'])

可以看到,例9就是简单的将例8进行了一个改造,但是两个函数的输出结果却完全一样。
所以我们可以总结下call和apply两个方法:

  • 每个函数都有这两个非继承而来的方法。
  • 都可以改变函数的运行作用域
  • call 可以接受多个参数,其中第一个参数是函数的运行作用域,而后每一个需要传入的参数都必须列举出来; apply方法接受两个参数,第一个同call方法,第二个参数接受参数数组,如果第二个参数不是一个有效数组或不是arguments对象,那么将导致一个TypeError,如果没有提供任何一个参数,那么全局对象将用作this的指向。
    下面我们再来看看bind方法

使用bind

bind和call、apply类似,都可以改变函数的运行作用域,下面我们先来看一个例子
例10:

1
2
3
4
5
6
7
8
9
10
11
12
var user = {
name: 'Jeremy',
age: 24,
fn: function () {
f.bind(user);
function f () {
console.log(`My name is ${this.name}`)
console.log(`My age is ${this.age}`)
}
}
}
user.fn()

当运行上面这个例子,我们会发现什么都没有打印,这时就开始疑惑了,难道代码写错了???其实不是,因为bind方法返回的是一个修改后的函数,所以我们来稍作修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var user = {
name: 'Jeremy',
age: 24,
fn: function () {
f.bind(user)();
function f () {
console.log(`My name is ${this.name}`)
// My name is Jeremy
console.log(`My age is ${this.age}`)
// My age is 24
}
}
}
user.fn()

将bind方法稍作修改,代码结果就和我们预想的一样。同样bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。这里就不做具体的举例了。
再说说call、apply和bind的区别
call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这就是他们之间的区别。

本文完

PS: 好久没写博客了,反思了下自己,好习惯不能落下,以后每周写一篇博客