JS中this的指向问题不同于其他语言,JS中的this不是指向定义它的位置,而是在哪里调用它就指向哪里。
JS中,普通的函数调用有三种:直接调用、方法调用和new调用。除此之外还有一些特殊的调用方式,比如通过bind()将函数对象之后在进行调用,通过apply()、call()进行调用等。而es6引入箭头函数之后,箭头函数调用时,其this指向又有所不同,下面来分析这些情况下的this指向。
直接调用:
首先看直接调用,就是通过 函数名(...) 这种方式调用。这时候,函数内部的 this 指向全局对象,在浏览器中全局对象是 window,在node中全局对象是 global。
// 简单兼容浏览器和node的全局对象
const _global = typeof window === "undefined" ? global : window;
function demo(){
console.log(this === _global); // true
}
demo(); // 直接调用
注意⚠️:直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...) 来对函数进行调用的方式,都称为直接调用。比如下面这个例子也是直接调用:
(function(_global){
// 通过IIFE 限定作用域
function test(){
console.log(this === _global); // true
}
test(); // 非全局作用域下的直接调用
})(typeof window === "undefined" ? global : window);
bind()对直接调用的影响:
这种情况在react和es6中经常遇到,绑定事件其实类似于之前的 var _this = this;
Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数无论以什么样的方式调用,其this始终指向绑定的对象。例:
const obj = {};
function demo(){
console.log(this === obj);
}
const obj1 = demo.bind(obj);
test(); // false
obj1(); // true
那么 bind 做了什么:
const obj = {};
function demo(){
console.log(this === obj);
}
function myBind(func, target){
return function(){
return func.apply(target, arguments);
}
}
const obj1 = myBind(demo, obj);
test(); // false
obj1(); // true
从上面的示例可以看到,首先通过闭包保持了target,即绑定的对象;然后在调用函数的时候,对原函数使用了 apply 方法来指定函数的 this。当原声的 bind() 实现可能不同,而且更高效。但这个示例说明了 bind() 的可行性。
call和apply对this的影响:
上面用到了 Function.prototype.apple(),与之类似的还有 Function.prototype.call()。他们第一个参数都是指定函数运行时其中的 this 指向。不过使用 apply 和 call 的时候仍需要注意,如果目标函数本身是一个绑定了 this 对象的函数,那么 apply 和 call 不会像预期那样执行。比如:
const obj = {};
function demo(){
console.log(this === obj);
}
// 绑定到一个新对象,而不是obj
const obj1 = demo.bind({});
demo.apply(obj); // true
obj1.apply(obj); // false
这样看来 bind 对函数的影响还是很大的,所以使用时一定要注意!
方法调用:
方法调用是指通过对象来调用其方法函数,它是 对象.函数名(...) 这样的调用形式。这种情况下,函数中的 this 指向调用该方法的对象。但是同样需要注意 bind() 的影响。
const obj = {
// 第一种方式,定义对象的时候定义其方法
test() {
console.log(this === obj)
}
}
// 第二种方式,对象定义好之后为期附加一个方法(函数表达式)
obj.test2 = function(){
console.log(this === obj)
}
// 第三种方式与第二种原理相同
// 是对象定义好之后为其附加一个方法(函数定义)
function t(){
console.log(this === obj)
}
obj.test3 = t;
// 这也是为对象附加一个方法函数
// 但这个函数绑定了一个不是 obj 的其他对象
obj.test4 = (function(){
console.log(this === obj);
}).bind({})
obj.test(); // true
obj.test2(); // true
obj.test3(); // true
obj.test4(); // false
这里需要注意的是,后三种方式都是预定定义函数,再将其附加给 obj 对象作为其方法。再次强调,函数内部的 this 指向与定义无关,受调用方式的影响。
方法中 this 指向全局对象的情况:
注意,这里说的方法中不是方法调用中。方法中的 this 指向全局对象,如果不是因为 bind(),那一定是因为不是用的方法调用方式,比如:
const obj = {
test(){
console.log(this === obj);
}
}
const t = obj.test;
t(); // false
t就是obj的test方法,但是t()调用时,其中的this指向全局。
之所以要提出这种情况,主要是因为常常将一个对象方法作为回调传给某个函数之后,却发现运行结果与预期不符—因为忽略了调用方式对 this 的影响。比如下面的例子就是在页面中对某些事情进行封装之后特别容易遇到的问题:
class Handles {
// 这里 $button 假设是一个指向某个按钮的jq对象
constructor(data, $button) {
this.data = data;
$button.on('click', this.onButtonClick);
}
onButtonClick(e) {
console.log(this.data);
}
}
const handles = new Handles("string data", $('#btn'));
// 对 $('#btn') 进行点击操作之后,输出 undefined ,但预期输出 string data
很显然 this.obButtonClick 作为一个参数传入 on() 之后,事件触发时,是对这个函数进行的直接调用,而不是方法调用。所以其中的 this 指向的是全局对象,要解决这个问题有很多办法:
// 这是es5 中的解决办法之一
var _this = this;
$button.on('click', function(){
_this.onButtonClick();
})
// 也可以通过 bind() 来解决
$button.on('click', this.onButtonClick.bind(this));
// es6 中可以通过箭头函数来处理,在jq中慎用
$button.on('click', e => this.onButtonClick(e));
不过请注意,将箭头函数用作jq的回调时,要小心函数对 this 的使用。jq大多数回调函数【非箭头函数】中的 this 都是表示调用目标,所以可以写 $(this).text() 这样的语句,但jq无法改变箭头函数的 this 指向,同样的语句语义完全不同。
new调用:
在es6之前,每一个函数都可以当作构造函数,通过 new 调用来产生新的对象(函数内无特定返回值的情况下)。而es6改变了这种状态,虽然 class 定义的类用 typeof 运算符得到的仍然是“function”,但他不能像普通函数一样直击调用;同时,class 中定义的方法函数,也不能当作构造函数 new 来调用。
而在es5中, 用 new 调用一个构造函数,会创建一个新对象而其中的 this 指向这个新对象。这没什么悬念,因为 new 本身就是设计来创建先对象的。
var data = 'hi';
function AClass(data){
this.data = data;
}
var a = new AClass('hello world');
console.log(a.data); // hello world this === a
console.log(data); // hi
var b = new AClass('hello world');
console.log(a === b); // false
箭头函数中的this:
箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含他的那个函数或函数表达式中的 this。比如:
const obj = {
test(){
const arrow = () => {
// 这里的 this 是 test() 中的 this,
// 由 test() 的调用方式决定
console.log(this === obj)
}
arrow();
},
getArrow() {
return () => {
// 这里的 this 是 getArrow() 中的 this
// 由 getArrow() 的调用方式决定
console.log(this === obj);
}
}
}
obj.test(); // true
const arrow = obj.getArrow();
arrow(); // true
示例中两个 this 都是由箭头函数的直接外层函数(方法)决定的,而方法函数中的 this 是由其调用方式决定的。上例的调用方式都是方法调用,所以 this 都指向方法调用的对象,即 obj。
箭头函数让大家在使用闭包时不需要太纠结 this,不需要通过像 _this 这样的局部变量来临时引用 this 给闭包函数使用。来看一段 Babel 对箭头函数的转译可能能加深理解:
// es6
const obj = {
getArrow(){
return () => {
console.log(this === obj)
}
}
}
// es6 babel 转译
var obj = {
getArrow: function(){
var _this = this;
return function(){
console.log(_this === obj);
}
}
}
另外需要注意的是,箭头函数不能用 new 调用,不能 bind() 到某个对象(虽然 bind() 方法调用没问题,但不会产生预期效果)。不管在什么情况下使用箭头函数,它本身是没有绑定 this 的,它用的是直接外层函数(即包含他的最近的一层函数或函数表达式)绑定的 this。
总结: [以下方法均需要注意 bind() 的影响]
直接调用[任何作用域]:函数名(...) => 函数内部 this 指向全局对象。typeof window === 'undefined' ? global : window
bind():Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,返回一个新函数,不论此新函数什么方式调用,其 this 始终指向绑定的对象,相当于=>[var _this = this;]
call()/apply():Fcuntion.prototype.call()/Function.prototype.apply()。他们的第一个参数都是指定函数运行时其中的 this 指向。
方法调用:对象.方法函数(...) => 函数内部 this 指向该方法的对象。
new调用:使用new一个构造函数会创建一个新对象,而 this 指向这个新对象。
箭头函数:没有自己的 this 绑定,箭头函数中使用的 this,其实是直接包含他的那个函数或函数表达式中的 this。
obj.demo() / var d = obj.demo(); // 均为 true
方法中 this 指向全局对象的情况:bind() / const d = obj.demo();
关于bind、call和apply之间区别的文章请看这里:bind、call和apply之间区别