1 定义
this关键字是一个非常重要的语法点,不理解它的含义,大部分开发任务都很难完成。
无论什么场合,this总是返回一个对象。简单说,this就是属性或方法“当前”所在的对象。
var person = {
name: 'mark',
say: function() {
return 'name: ' + this.name;
}
};
person.say() // "name: mark"
上面代码中,由于this.name是在say方法中调用的,而say方法所在的当前对象是person,因此this指向person,而this. name 就是 person. name。
由于对象的属性可以赋给另一个对象,因此this的指向是可变的。
function f() {
return 'hello: ' + this.name;
}
var A = {
name: 'AAA',
desc: f
};
var B = {
name: 'BBB',
desc: f
};
A.desc() // "hello: AAA"
B.desc() // "hello: BBB"
上面代码中,函数f内部使用了this,随着f所在对象的不同,this的指向也不同。
总结一下,JavaScript语言中一切皆对象,运行环境是对象,函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。这本来是很清晰的概念,但是因为JavaScript支持运行环境的动态切换,所以没有办法事先确定this到底指向哪个对象,这才是最容易让人迷惑的地方。
2 实质
由于函数可以在不同的运行环境执行,需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指向函数当前的运行环境。
var f = function() {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
f() // 1
obj.f() // 2
上面代码中,函数f在全局环境里执行,this.x指向全局环境的x,输出1;在obj环境里执行,this.x指向obj.x,输出2。
3 使用注意点
3.1 避免多层this
切勿在函数中包含多层this,因为this的指向不确定。
var obj = {
f1: function() {
console.log(this);
var f2 = function() {
console.log(this);
}();
}
}
obj.f1()
// Object
// Window
上面代码包含两层this,实际运行后,第一层this指向对象obj,第二层this指向全局对象。因为实际执行的类似于下面代码:
var temp = function() {
console.log(this);
};
var obj = {
f1: function() {
console.log(this);
var f2 = temp();
}
}
简单的解决方法是用一个指向外层对象的变量来代替第二层this。
var obj = {
f1: function() {
console.log(this);
var self = this; // 增加一个变量
var f2 = function() {
console.log(self);
}();
}
}
obj.f1()
// Object
// Object
上面代码定义了变量self,固定指向外层的this,就不会发生指向的改变了。
通过一个变量固定this的值,然后内层函数使用这个变量,是非常常见的做法,请务必掌握。
3.2 避免数组处理方法中的this
数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。
var o = {
v: 'hello',
p: ['a1', 'a2'],
f: function f() {
this.p.forEach(function(item) {
console.log(this.v + ' ' + item);
});
}
}
o.f()
// undefined a1
// undefined a2
上面forEach方法的回调函数中的this,跟上一段的多层this一样,内层this不指向外部,而是指向顶层对象。
解决这个问题的一个简单方法,就是前面提到的,使用中间变量固定this。
3.3 避免回调函数中的this
回调函数中的this往往会改变指向,最好避免使用。
为了解决这个问题,可以采用下面的一些方法对this进行绑定,使this固定指向某个对象,减小不确定性。
4 绑定this的方法
JavaScript提供了call、apply、bind三个方法,来切换/固定this的指向。
4.1 Function.prototype.call()
函数实例的call方法,可以指定this的运行环境(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
call方法的参数,是一个对象。
var obj = {};
var f = function() {
return this;
};
f() === window // true
f.call(obj) === obj // true
上面代码中,直接运行函数f时,this指向全局环境(window对象)。而call方法可以改变this指向对象obj,然后在对象obj的作用域中运行函数f。
如果call方法的参数为空、null和undefined,则默认传入全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
call方法还可以接受多个参数,后面的参数是函数调用时所需的参数。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
4.2 Function.prototype.apply()
apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别是,它接收一个数组作为函数执行的参数,使用格式如下:
func.apply(thisValue, [arg1, arg2, ...])
原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。
function f(x, y) {
console.log(x + y);
}
f.call(null, 1, 2) // 3
f.apply(null, [1, 2]) // 3
5.3 Function.prototype.bind()
bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。
var d = new Date();
d.getTime()
var print = d.getTime;
print() // Error
上面代码中,print报错是因为getTime方法内部的this,绑定了Date对象的实例,赋值给变量print后,内部的this已经不指向Date对象的实例了。
bind方法可以解决这个问题
var print = d.getTime.bind(d);
print() // ok
上面代码中,bind将getTime方法内部的this绑定到d对象,因此就可以安全地将getTime赋值给其它变量了。
使用bind还可以将this绑定到其它对象。
var counter = {
count: 0,
inc: function() {
this.count++;
}
};
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
上面代码中,bind将inc内部的this绑定到obj对象,因此调用func后,递增的就是obj内部的count属性。
使用bind方法有一些注意事项。
(1)每一次返回一个新函数
bind方法每运行一次,就会返回一个新函数,这可能产生一些问题。
node.addEventListener('click', obj.m.bind(obj));
node.removeEventListener('click', obj.m.bind(obj)); // Error
上面代码无法取消绑定,因为后一个bind返回的是一个匿名函数,和前一个add事件并不是同一个函数。
正确的定法是下面这样:
var listener = obj.m.bind(obj);
node.addEventListener('click', listener);
node.removeEventListener('click', listener);
(2)结合回调函数使用
回调函数是JavaScript最常用的模式之一,我们应用使用bind方法将目标函数固定到目标对象。
(3)结合call方法使用
以数组的slice方法为例。
[1, 2, 3].slice(0, 1) // [1]
等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
上面代码的两种调用方法,得到同样的结果。
注:本文适用于ES5规范,原始内容来自 JavaScript 教程,有修改。