为什么要有 this
this 是 JavaScript 的关键字,用来隐式传递当前对象的引用。函数不必写死某个对象的名称,也不必额外接收对象参数,就能读取或修改调用者的数据。
这使函数只需要描述"做什么",调用方式则决定"为谁执行"。同一个函数可以被不同对象复用,代码也因此更加简洁。
this 可以出现在哪
1. 全局作用域
在浏览器的普通脚本中,全局作用域里的 this 指向 window:
js
console.log(this);
console.log(this === window); // true
2. 函数体内
普通函数中的 this 不是在函数定义时确定的,而是由函数的调用方式决定。同一个函数换一种调用方式,this 就可能发生变化。
因此,分析 this 时要始终抓住一句话:
先看这个
this属于谁,再看它指向谁。
- 看
this属于谁 :先找到this写在哪里。普通函数有自己的this;箭头函数没有自己的this,需要继续向外寻找,直到找到最近的非箭头函数或全局作用域。 - 看
this指向谁 :找到真正拥有这个this的位置后,再结合调用方式判断。普通函数看它怎样被调用;全局作用域则直接根据运行环境判断。
全局作用域中的 this 可以直接判断;函数体内的 this 则需要结合下面的绑定规则分析。
this 的绑定规则
绑定规则解决的是第二步:已经知道 this 属于哪个函数后,再根据这个函数的调用形式判断 this 最终指向谁。
常见调用形式与绑定结果如下:
| 调用形式 | 绑定规则 | this 的指向 |
|---|---|---|
foo() |
默认绑定 | window |
obj.foo() |
隐式绑定 | obj |
oo.foo.foo() |
多层调用 | 离函数最近的调用对象 |
foo.call(obj) / foo.apply(obj) / foo.bind(obj)() |
显式绑定 | 指定的 obj |
new Foo() |
new 绑定 |
新创建的实例 |
1. 默认绑定
函数以 foo() 的形式独立执行,并且 this 没有被显式绑定或 new 绑定改变时,会使用默认绑定。此时函数中的 this 指向 window。
js
var a = 1;
function bar() {
var a = 2;
function foo() {
console.log(this.a);
}
foo();
}
bar(); // 1
这段代码可以分三步判断:
this出现在foo中。foo()是独立调用,使用默认绑定,所以this指向window。- 浏览器普通脚本中,顶层通过
var声明的a会成为window.a,因此输出1。
bar 内部的 a 是局部变量。函数嵌套关系不会改变 foo 的 this,所以这里不会输出 2。
2. 隐式绑定
当函数保存在对象属性中,并通过这个对象调用时,函数中的 this 指向该对象。
js
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo
};
obj.foo(); // 1
对象属性 foo 保存的是函数引用。真正决定 this 的是调用表达式 obj.foo():调用点前有 obj,所以 foo 中的 this 指向 obj,最终读取到 obj.a。
如果同一个函数由另一个对象调用,this 也会随之改变:
js
var another = {
a: 2,
foo: foo
};
another.foo(); // 2
这正是 this 便于复用的原因:函数本身没有改变,调用者决定了它操作哪个对象。
3. 隐式丢失:多层对象调用
多层对象调用不会把最外层对象一直传递给函数。判断时只看最终调用点,this 指向离函数最近的调用对象。
js
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo
};
var oo = {
a: 2,
foo: obj
};
oo.foo.foo(); // 1
oo.foo 的值是 obj,所以 oo.foo.foo() 可以看成由 obj 调用 foo。直接调用者是 obj,而不是最外层的 oo,因此 this.a 的结果是 1。
4. 显式绑定
不想由调用形式自动判断时,可以使用 call、apply 或 bind 直接指定函数中的 this。
js
function foo(x, y) {
console.log(this.a, x + y);
}
var GGbond = { a: 1 };
var FF = { a: 2 };
var CRQ = { a: 3 };
foo.call(GGbond, 1, 2); // 1 3
foo.apply(FF, [2, 3]); // 2 5
const bar = foo.bind(CRQ, 3, 4);
bar(); // 3 7
三种方法都能指定 this,区别在于调用时机和参数形式:
call立即调用函数,从第二个参数开始逐个传递实参。apply立即调用函数,使用数组传递实参。bind不会立即调用,而是返回一个绑定好this的新函数,需要再次调用。
5. new 绑定
函数通过 new 调用时,其中的 this 指向新创建的实例对象。构造函数中通过 this 添加的属性,也会出现在这个实例上。
js
function Person() {
this.name = "GGBond";
}
const p = new Person();
console.log(p.name); // GGBond
调用 new Person() 时,Person 中的 this 指向实例 p,所以 this.name = "GGBond" 等同于给 p 添加 name 属性。
箭头函数
箭头函数没有自己的 this,也不使用普通函数的绑定规则。它会沿用定义位置外层作用域中的 this;当箭头函数写在普通函数内部时,它会沿用该普通函数的 this。
js
function foo() {
var fn = () => {
this.a = 2;
};
fn();
}
var obj = {
a: 1,
bar: foo
};
obj.bar();
console.log(obj.a); // 2
这段代码的执行过程是:
obj.bar()调用普通函数foo,所以foo的this指向obj。- 箭头函数
fn没有自己的this,于是沿用foo的this。 this.a = 2实际修改的是obj.a,最终输出2。
箭头函数不能作为构造函数,因此不能通过 new 调用。
普通函数的 this 取决于调用方式;箭头函数的 this 取决于定义位置的外层作用域。抓住这一区别,就能快速判断代码中的 this 指向。