0%

十五分钟搞定JavaScript的this

this有什么问题?

JavaScript有三大坑:闭包,this和异步。在十五分钟搞定JavaScript闭包中我们简单看了看闭包,今天就来说一说这个this。那么this有什么问题呢?JavaScript中到处都是惊喜,先来看一个:

1
2
3
4
5
6
7
8
9
const obj = {
msg: "obj's message",
sendMessage() {
console.log('excuting function foo...');
console.log(this.msg);
}
};

obj.sendMessage();

执行这段代码的结果是啥?这个没有问题,运行结果就是输出两行内容:excuting function foo...obj's message。如果想让它执行结束过一秒再执行一次的话,大家可能会认为代码应该是这个样子的:

1
2
3
4
5
6
7
8
9
10
const obj = {
msg: "obj's message",
sendMessage() {
console.log('excuting function foo...');
console.log(this.msg);
}
};

obj.sendMessage();
setTimeout(obj.sendMessage, 1000);

猜一猜结果?

然而在浏览器和nodejs中执行结果都是一样的:
excuting function foo...
obj's message
一秒后:
excuting function foo...
undefined

立即执行的没有问题,过一秒再执行的就成了undefined了?
如果我们在代码中再加一行,然后在浏览器中运行的话就更奇怪了:

1
2
3
4
5
6
7
8
9
10
11
var msg = 'Global Message!';
const obj = {
msg: "obj's message",
sendMessage() {
console.log('excuting function foo...');
console.log(this.msg);
}
};

obj.sendMessage();
setTimeout(obj.sendMessage, 1000);

结果是:
excuting function foo...
obj's message
一秒后:
excuting function foo...
Global Message!
不知道你是什么反应,我的表情如下:黑人问号

我们就来看看这个this为什么这么古怪,它究竟是什么。

this是什么?

this代表的是执行函数时自动生成的一个内部对象,只存在于函数体中。函数在调用的时候,这个函数就会有一个执行环境,这个执行环境描述了包括函数在哪里被调用,调用的方法,变量对象(作用域链)以及this的值等等信息。也就是说,这个this的值是在函数调用时才有的,也只有在函数调用时才存在,跟声明函数没有什么关系。
我们通过不同的函数调用情况来看this的值。

普通的函数调用

1
2
3
4
function sendMessage() {
console.log(this);
}
sendMessage();

这个结果在浏览器中就是Window对象,nodejs中的是global对象,在非严格模式下是这样的,严格模式下返回的都是undefined
为了简单起见,我们只分析非严格模式下this的行为举止。还有我实在不喜欢var,所以的话用var声明的情况也就不考虑了,这个var是一个设计失误,我们没有必要去花很多心思去理解一个错误的东西。

通对象的方法去调用函数

1
2
3
4
5
6
7
8
9
function sendMessage() {
console.log(this.msg);
}

const obj = {
msg: "obj's message",
sendMessage
};
obj.sendMessage();

这段代码的返回值是obj's message,因为这个函数被调用时的执行环境就是obj对象,所以函数调用的this会绑定到这个对象上。
假如我们想给objsendMessage方法再起一个名字叫anotherFuncName,然后通过anotherFuncName来调用这个方法话,可能会这样写代码:

1
2
3
4
5
6
7
8
9
10
11
12
function sendMessage() {
console.log(this.msg);
}

const obj = {
msg: "obj's message",
sendMessage
};

const anotherFuncName = obj.sendMessage;

anotherFuncName();

这样行不行呢?不行,返回的结果是undefined。为什么呢?原因在于函数的名字它真的就只是个名字,就像一个盒子上面贴的标签一样。复制函数就像又给函数贴了一个标签一样,实际上anotherFuncName引用的依然是sendMessage函数本身。在调用的时候就相当于普通的函数调用,并没有额外的信息,所以this绑定的Window对象,而Window对象的属性不能这么访问(ES2015之前的非严格模式下在全局作用域下使用var申明变量会自动添加到全局对象上,作为其属性,这个可以说是一种错误或者巨大的缺陷,这里就不做错误的示范了)。
这里的例子想要说明什么?把函数赋值给或当做参数传递的时候也会意外的发生同样的事情,只是会给函数再加一个名称,并不会真正的把函数执行的相关信息也一同复制过来。所以在调用的时候就如同普通的函数调用一样,而函数中的如果有this的话,自然就绑定的是undefined。这也就是我们文章开头看到的延时一秒执行出问题的原因。问题怎么解决呢?

call、apply和bind

call和apply的作用就是将函数在调用时的this绑定传入的第一个参数上。所以上面的问题可以这样来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
function sendMessage() {
console.log(this.msg);
}

const obj = {
msg: "obj's message",
sendMessage
};

const anotherFuncName = function() {
sendMessage.call(obj);
};
anotherFuncName();

这样一来,通过anotherFuncName来调用函数,会在执行的时候将sendMessage函数的this绑定到obj对象上了。
bind是干什么的呢?bind可以将函数的this永久地绑定到传入的第一个参数上。上面的代码可以改写成:

1
2
3
4
5
6
7
8
9
10
11
function sendMessage() {
console.log(this.msg);
}

const obj = {
msg: "obj's message",
sendMessage
};

anotherFuncName = sendMessage.bind(obj);
anotherFuncName();

bind()接受的参数就是想要将函数的this绑定的对象,返回原函数。

new调用

对任何函数都可以使用new操作符来new一下,这个过程发生了什么事?

  • 新建一个对象
  • 将新对象的[[prototype]]属性连接到new后面的函数的原型对象上
  • new后面的函数调用时的this绑定到新对象上
  • 没有返回其他对象的话,会返回新建的对象
1
2
3
4
5
6
function say(msg) {
this.msg = msg;
}

const obj = new say('Hello');
console.log(obj.msg);

在执行new时,会将this绑定到新建的obj上,所以obj会获得一个msg属性。