很多教程对this的解释比较抽象,简单来说,在全局函数作用域中,this一般指向window,在对象函数作用域中,this指向所属对象。先说对象的,看看下面的例子:
const a = {
b: function() {
console.log(this);
},
c: {
d: function() {
console.log(this);
}
}
};
第一个this指向a,因为b是a的函数;第二个this指向a.c,因为d是a.c的函数。看懂了吗?你在哪个对象上调用的函数,该函数内部的this就指向哪个对象。为什么全局作用域中指向window呢?因为window也是一个对象,在js设计之初,所有全局作用域下定义的变量和函数,都会被自动绑定到window上。看看这个例子:
function a() {
console.log(this);
}
a();
a函数在定义时就被绑定到window上,调用a()等同于调用window.a(),看到window.就懂了吧,这也可以是广义的对象函数作用域,this指向所属的对象window。看似很合理,不幸的是,这种设计存在缺陷:并非所有全局函数都在window上,特别是ES6引入块级作用域后,这种问题尤其常见,比如下面的声明方式:
const a = function() {
console.log(this);
};
const和let声明的变量不会绑定到window上(即使在全局作用域中声明),但这种语境下,this仍然指向window。这属于历史遗留问题,为解决这一缺陷,ES5提出了严格模式(use strict),当函数没有调用对象时,this改为指向undefined:
'use strict';
const a = function() {
console.log(this);
};
a();
关于箭头函数,由于箭头函数自身没有this,故this从外部继承,把this当局部变量来看就好,当前作用域没定义就往上找。这不难理解,只是在多层函数嵌套中容易混淆,因此也有重命名一个变量来存储this值的做法。
类中的this
在类的构造函数和原型方法中,this指向实例。什么是类和实例呢?可以简单理解为所有需要new的东西就是类,new的结果就是实例,比如:
const xhr = new XMLHttpRequest();
XMLHttpRequest是一个类,xhr是他的实例。这种设计可以共享变量和函数,比如所有数组都有push方法,是不是每个数组都需要在内存中声明一个push函数?当然不是,push只声明在Array原型上,所有数组(或者说Array的实例)都共用这个函数。如何区分是哪个数组?这就是this指向实例的作用。你说数组不需要new?这不过是简化了的字面量语法,完整写法长这样:
const arr = new Array(1, 2, 3); // const arr = [1, 2, 3];
包括数字Number、字符串String、对象Object、函数Function等等,一切都可以抽象成类和实例的概念(面向对象编程)。构造函数就是类在实例化时自动执行的函数,看下面的例子就能理解了:
class MyClass {
constructor(num) { // 构造函数
console.log('我被实例化了!');
this.num = num;
}
add() { // 原型方法,MyClass.prototype.add
this.num++;
}
}
const obj = new MyClass(123);
console.log(obj.num);
obj.add();
console.log(obj.num);
改变this
函数的this可以被改变,包括显式和隐式的改变,其中隐式改变是最容易被忽视的地方,看看这个例子:
元素2.onclick = () => {
元素1.click();
};
这里你可能会认为两个函数的参数一致,因此简写成:
元素2.onclick = 元素1.click;
这两种写法真的等价吗?第二种写法实际是以click函数替换onclick,既然onclick是元素2的方法,this自然指向元素2,也就是说,代码的实际效果是这样:
元素2.onclick = () => {
元素1.click.bind(元素2)();
}
由于所有元素都共用同一个click函数,函数内通过this来区分实例,因此最终效果变成了这样:
元素2.onclick = () => {
元素2.click();
}
这就是隐式改变this所带来的问题,不注意的话就容易犯错。看另一个例子:
class MyClass {
constructor() {
this.num = 0;
}
add() {
this.num++;
}
}
const obj1 = new MyClass();
const obj2 = {
num: 10,
add: obj1.add // () => obj1.add()
};
obj2.add();
console.log(obj1.num, obj2.num);
分别试试两种写法,再结合前面例子,可以帮助你更好地理解这种差异性。
显式改变this,即通过apply、call、bind等方法强制指定this,比如要让上面click的例子正常工作,可以改成:
元素2.onclick = 元素1.click.bind(元素1);
你希望用$简写document.querySelector?一样的道理,可以写成:
const $ = document.querySelector.bind(document);
劫持时的this
下面是一个很典型的函数劫持写法:
const oldPush = Array.prototype.push;
Array.prototype.push = function(...args) {
// ...
let result = oldPush.apply(this, args);
// ...
return result;
};
劫持函数内部通过oldPush调用原函数,由于oldPush没有从属对象,内部this会指向window(严格模式指向undefined),这显然是错误的,我们需要将实例传递过去,这就是为什么要用apply。当然如果原函数不访问this,或者this就是window,那不传递也没影响,但只要你不想自找麻烦,传递了总不会犯错。
还有另一种写法:
Array.prototype.oldPush = Array.prototype.push;
Array.prototype.push = function(...args) {
// ...
let result = this.oldPush(...args);
// ...
return result;
};
这种写法将原函数保存在原型上,然后用this.的方式调用,本质上跟apply没有区别,只是提供了从外部获取原函数的途径,但也可能对原对象造成污染,各有优劣。