当我们学习 Javascript 中的 this 时,非常容易陷入一种困境,一种似懂非懂的困境。在某些情况下,我们看了一些文章和解释,将其应用到一些简单的情况,发现,嗯,确实这么运作了。而在另一些更为复杂的情况下,我们发现又懵逼了,什么情况?这篇文章的目的,就是要完全搞懂并掌握 Javascript 中的 this。为什么我们很难完全掌握 this?在我看来,原因是 this 的解释太过抽象,在理论上是这样,到了实际应用时,却无法直接应用。同时,一些写 this 的文章可能并未覆盖全面所有 this 的情况,如果缺乏理论和实际的互相印证,以及一些深入揭示 this 原理的实例分析,那么 this 确实是很难完全掌握的。还好,有这篇文章,看完这篇文章后,你将完全掌握 Javascript 中的 this。
确定 this 的规则
this 是 Javascript 当前执行上下文中 ThisBinding 的值,所以,可以理解为,它是一个变量。现在的关键问题是,ThisBinding 是什么?ThisBinding 是 Javascript 解释器在求值(evaluate)js 代码时维护的一个变量,它是一个指向一个对象的引用,有点像一个特殊的 CPU 寄存器。解释器在建立一个执行上下文的时候会更新 ThisBinding。
当一个函数被执行时,它的 this 值被赋值。一个函数的 this 的值是由它的调用位置决定的。但是找到调用位置,需要跟踪函数的调用链,这有时候是非常复杂和困难的。有一个更简单的确定 this 的方式,就是直接找到调用该函数的对象。Arnav Aggarwal 提出了一个简单的能确定 this 值的规则,优先级按文中的先后顺序:
1. 构造函数(constructor)中的 this,通过 new 操作符来调用一个函数时,这个函数就变成为构造函数。new 操作符创建了一个新的对象,并将其通过 this 传给构造函数。
function ConstructorExample() {
console.log(this);
this.value = 10;
console.log(this);
}
new ConstructorExample();
// -> {}
// -> { value: 10 }
new 操作符在 Javascript 中的实现大致如下:
function newOperator(Constr, arrayWithArgs) {
var thisValue = Object.create(Constr.prototype);
Constr.apply(thisValue, arrayWithArgs);
return thisValue;
}
2. 如果 apply,call 或者 bind 用于调用、创建一个函数,函数中的 this 是作为参数传入这些方法的参数
function fn() {
console.log(this);
}
var obj = {
value: 5
};
var boundFn = fn.bind(obj);
boundFn(); // -> { value: 5 }
fn.call(obj); // -> { value: 5 }
fn.apply(obj); // -> { value: 5 }
3. 当函数作为对象里的方法被调用时,函数内的this是调用该函数的对象。比如当obj.method()被调用时,函数内的 this 将绑定到obj对象
var obj = {
method: function () {
console.log(this === obj); // true
}
}
obj.method();
4. 如果调用函数不符合上述规则,那么this的值指向全局对象(global object)。浏览器环境下this的值指向window对象,但是在严格模式下('use strict'),this的值为undefined
- sloppy mode:
function sloppyFunc() {
console.log(this === window); // true
}
sloppyFunc();
- strict mode:
function strictFunc() {
'use strict';
console.log(this === undefined); // true
}
strictFunc();
需要注意的是,以下情况下,默认为 strict mode:
- Module code is always strict mode code.
- All parts of a ClassDeclaration or a ClassExpression are strict mode code.
第二个需要注意的点是,在 nodejs 的 module 中,this 指向 module.exports:
// `global` (not `window`) refers to global object:
console.log(Math === global.Math); // true
// `this` doesn’t refer to the global object:
console.log(this !== global); // true
// `this` refers to a module’s exports:
console.log(this === module.exports); // true
5. 如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定this的值
6. 如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,this被设置为它被创建时的上下文
const obj = {
value: 'abc',
createArrowFn: function() {
return () => console.log(this);
}
};
const arrowFn = obj.createArrowFn();
arrowFn(); // -> { value: 'abc', createArrowFn: ƒ }
一些进阶情况下的 this
以上确定 this 的规则,能满足大部分简单情况下 this 的确定,然而,在一些进阶情况下,我们还是难以确定 this,下面将分析一些进阶情况下的 this:
闭包中的函数中的 this
看下面这个例子,用上一节的确定 this 规则,可以看到 inner 函数中的 this 符合第四条规则,在 strict 模式下,this 为 undefined。这是因为 inner 函数有自己的 this,可以这么理解:每个非箭头函数其实都有其自己的隐式的 this 参数,而这里 inner 函数并没有明确的调用其的对象,也没有被 apply、call 或 bind,其隐式的 this 在 strict 模式下则为 undefined,在宽松模式下则为 window 对象。
function outer() {
'use strict';
function inner() {
console.log(this); // undefined
}
console.log(this); // 'outer'
inner();
}
outer.call('outer');
一个类似的例子,在构造函数中调用外部函数:
'use strict';
function getThis() {
console.log(this); // undefined
}
function Dog(saying) {
this.saying = saying;
getThis();
console.log(this); // Dog {saying: "wang wang"}
}
new Dog('wang wang');
另一个例子,通过 this 调用构造函数中的方法,this 是 new 操作符创建的一个新对象,getThis 是该对象中的一个方法,this.getThis,符合规则 3,对象中调用对象的方法,所以 getThis 中的 this 为其调用其的对象,即 Dog {saying: "wang wang"}:
function Dog(saying) {
this.saying = saying;
this.getThis = function() {
console.log(this); // Dog {saying: "wang wang"}
};
this.getThis();
console.log(this); // Dog {saying: "wang wang"}
}
new Dog('wang wang');
回调函数中的 this
回调函数和闭包的情况类似,看下面例子:
'use strict';
function getThis() {
console.log(this);
}
function higherOrder(callback) {
console.log(this);
callback();
}
higherOrder(getThis);
higherOrder.call({ a: 1 }, getThis);
// undefined
// undefined
// {a: 1}
// undefined
用 new 来调用回调函数的情况,满足规则1,this 为新创建的对象:
function getThis() {
console.log(this);
}
function callbackAsConstructor(callback) {
new callback();
}
callbackAsConstructor(getThis);
// getThis {}
原生 js 提供的 api 中的回调函数中的 this
setTimeout 中回调函数中的 this,在浏览器中是 window 对象,在 node 环境下为 Timeout 对象
'use strict';
function getThis() {
console.log(this);
}
setTimeout(getThis, 0);
// window or Timeout {_called: true, _idleTimeout: 1, _idlePrev: null, _idleNext: null, _idleStart: 338, …}
dom 事件回调函数,包含 html 中的内联回调函数和 js 中的事件回调函数。当内联回调函数直接在定义在内联代码中或者在内联代码中被调用,它们的 this 还是 window。而如果在内联代码中直接使用 this,则 this 指向对应的 dom 元素。在 js 中监听事件,回调函数中的 this 指向对应的 dom 元素。
<h3>Using `this` "directly" inside event handler or event property</h3>
<button id="button1">click() "assigned" using addEventListner()</button><br />
<button id="button2">click() "assigned" using click()</button><br />
<button id="button3" onclick="alert(this+ ' : ' + this.tagName + ' : ' + this.id);">
used `this` directly in click event property
</button>
<h3>Using `this` "indirectly" inside event handler or event property</h3>
<button onclick="alert((function(){return this + ' : ' + this.tagName + ' : ' + this.id;})());">
`this` used indirectly, inside function <br />
defined & called inside event property</button
><br />
<button id="button4" onclick="clickedMe()">
`this` used indirectly, inside function <br />
called inside event property
</button>
<br />
<script>
function clickedMe() {
alert(this + ' : ' + this.tagName + ' : ' + this.id);
}
document.getElementById('button1').addEventListener('click', clickedMe, false);
document.getElementById('button2').onclick = clickedMe;
</script>
eval 中的 this
eval 可以被直接或间接调用,当 eval 被间接调用时,其 this 为 global 对象。当 eval 被直接调用时,this 和它被包围处的 this 一致:
(0, eval)('this === window') // true
// Real functions
function sloppyFunc() {
console.log(eval('this') === window); // true
}
sloppyFunc();
function strictFunc() {
'use strict';
console.log(eval('this') === undefined); // true
}
strictFunc();
// Constructors
var savedThis;
function Constr() {
savedThis = eval('this');
}
var inst = new Constr();
console.log(savedThis === inst); // true
// Methods
var obj = {
method: function () {
console.log(eval('this') === obj); // true
}
}
obj.method();
this 实例
- 经典例子,函数中的 this 取决于调用它的对象
var obj = {
value: 'hi',
printThis: function() {
console.log(this);
}
};
var print = obj.printThis;
obj.printThis(); // -> {value: "hi", printThis: ƒ}
print(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}
- 多条规则组合
var obj1 = {
value: 'hi',
print: function() {
console.log(this);
},
};
var obj2 = { value: 17 };
obj1.print.call(obj2); // -> { value: 17 }
new obj1.print(); // -> {}
- 陷阱:忘记使用 new 操作符
function Point(x, y) {
this.x = x;
this.y = y;
}
var p = Point(7, 5); // we forgot new!
console.log(p === undefined); // true
// Global variables have been created:
console.log(x); // 7
console.log(y); // 5
// strict mode
function Point(x, y) {
'use strict';
this.x = x;
this.y = y;
}
var p = Point(7, 5);
// TypeError: Cannot set property 'x' of undefined
- 陷阱:回调函数中的 this,宽松模式下指向 window 对象
function callIt(func) {
func();
}
var counter = {
count: 0,
// Sloppy-mode method
inc: function () {
this.count++;
}
}
callIt(counter.inc);
// Didn’t work:
console.log(counter.count); // 0
// Instead, a global variable has been created
// (NaN is result of applying ++ to undefined):
console.log(count); // NaN
修复方式:
callIt(counter.inc.bind(counter));
- 陷阱:this 被屏蔽
var obj = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
loop: function () {
'use strict';
this.friends.forEach(
function (friend) {
console.log(this.name+' knows '+friend);
}
);
}
};
obj.loop();
// TypeError: Cannot read property 'name' of undefined
修复方式有很多,个人比较喜欢的方式是直接用箭头函数:
var obj = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
loop: function () {
'use strict';
this.friends.forEach(
(friend) => {
console.log(this.name+' knows '+friend);
}
);
}
};
obj.loop();
- 陷阱:callback(Promises) 中获取 this 的问题
// Inside a class or an object literal:
performCleanup() {
cleanupAsync()
.then(function () {
this.logStatus('Done'); // 这里会失败
});
}
// 修复
// Inside a class or an object literal:
performCleanup() {
cleanupAsync()
.then(() => {
this.logStatus('Done');
});
}
- eval 例子,myFun 没有明确调用对象,满足规则 4
function myFun() {
return this; // window
}
var obj = {
myMethod: function () {
eval("myFun()");
}
};
- 复杂例子,你能梳理清楚吗?
const throttle = (fn, time) => {
let last;
let timerId;
return function(...args) {
const now = Date.now();
if (last && now - last < time) {
clearTimeout(timerId);
timerId = setTimeout(() => {
last = now;
fn.apply(this, args);
}, time);
} else {
last = now;
fn.apply(this, args);
}
};
};
const hi = new func();
hi.deGetA();
const timerId = setInterval(hi.deGetA, 100);
setTimeout(() => {
clearInterval(timerId);
}, 1000);
this 最佳实践
- 使用箭头函数,箭头函数的 this 被设定为在其创建时的上下文,而不是被调用时确定,这样可以避免很多问题。
- 将函数作为回调函数传入另一个函数中的时候要特别注意,如果发现不对,可以考虑下使用 bind 方法,使得回调函数的 this 始终指向你想要的 this
- 在函数内调用回调函数时,如果希望回调函数和外部函数的 this 一致,可以考虑使用 apply,call 或 bind,合理使用这些方法,可以降低使用者在使用你的函数时可能发生的错误
参考文献
- The Simple Rules to ‘this’ in Javascript
- JavaScript’s this: how it works, where it can trip you up
- A different way of understanding this in JavaScript
- https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work/3127440#3127440
- https://thenewstack.io/tutorial-mastering-javascript/
- https://thenewstack.io/mastering-javascript-callbacks-bind-apply-call/
- strict mode code