为什么要学习 this?
- 阅读源码需要;很多
js
库里面都有关于this
以及this
绑定的用法,所以我们需要深入学习了解this
帮助我们高效阅读源码。 - 提高编程能力;
this
提供了一种更优雅的方式来隐式“传递”一个对象的引用,我们可以使用this
编写出更加高级优雅的代码。
关于 this 的误解?
-
指向自身
从字面意思上理解确实是说得通的。在我刚刚接触javascript
this
的时候,我也以为它是指向于自身,这个主要是受到以前python
编程经验的影响,觉得this
指向实例本身。事实上,javascript
通过new
实例化操作后的实例也确实指向于实例本身,不过javascript
里面的this
的情况更为多样和多变。 -
指向作用域
this
指向作用域,有些情况下是正确的,但是在其他情况下却是错误的。
function foo() {
const a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
打印出来的结果是 undefined
,这个我们想象的不一样;我们以为应该是 2
。所以 this
并不是指向作用域的。
this 到底是什么?
this
是在运行时进行绑定的,并不是编写时绑定,它的上下文取决于函数调用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(调用位置)
。
当一个函数被调用时,会创建一个活动记录(也称为执行上下文)。这个记录包含函数在哪里被调用(调用栈),函数调用方法以及传入参数等信息。this
就是其中的一个属性,会在函数执行的过程中用到。
调用位置
在正式开始 this
学习之前,我们先通过下面这个例子学习下调用位置。
function baz() {
// 当前调用栈是:baz,因此,当前调用位置是全局作用域
console.log("baz");
bar();
}
function bar() {
// 当前调用栈是 baz -> bar,因此,当前调用位置在 baz 中
console.log("bar");
foo();
}
function foo() {
// 当前调用栈是 baz -> bar -> foo,因此,当前调用位置在 bar 中
console.log("foo");
}
baz();
this 的绑定规则
- 默认绑定(独立函数调用)
可以把这条规则看做是无法应用其他规则的默认规则。
如上面的调用位置的例子,如果我们在每个函数里面打印this
,我们会发现this
都执行Window
对象。(严格模式下this
为undefined
)
function baz() {
console.log(this, "baz");
bar();
}
function bar() {
console.log(this, "bar");
foo();
}
function foo() {
console.log(this, "foo");
}
baz();
- 隐式调用(调用位置是否有上下文对象,或者说是否被某个对象拥有或包围,不过这种说法可能会造成一些误导)
function foo() {
console.log(this.a);
}
const obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
对象属性引用链只有最顶层或者最后一层会影响调用位置。
function foo() {
console.log(this.a);
}
const obj2 = {
a: 42,
foo: foo
};
const obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式丢失
function foo() {
console.log(this.a);
}
const obj = {
a: 2,
foo: foo
};
const bar = obj.foo;
const a = 'test';
bar(); // "test";
虽然 bar
是 obj.foo
的一个引用,但实际上,它引用的是 foo
函数本身,因此此时 bar()
其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// 这里存在一个隐性赋值,fn = obj.foo
fn();
}
const obj = {
a: 2,
foo: foo
};
const a = "test";
doFoo(obj.foo); // "test"
参数传递其实就是一种隐式赋值,这是我们需要注意的,这样的问题开发中如果不太清楚,很容易迷惑。
- 显示绑定
直接指定this
的绑定对象,有三种方法:bind
,call
,apply
function foo() {
console.log(this.a);
}
const obj = {
a: 2
};
foo.call(obj); // 2
通过 foo.call(...)
,我们可以在调用 foo
时强制把 this
绑定到 obj
上。
但是显示绑定也不能解决绑定丢失的问题,通过借助 bind 方法我们可以实现硬绑定。
- new 绑定
function foo(a) {
this.a = a;
}
const bar = new foo(2);
console.log(bar.a); // 2
使用 new
来调用 foo(...)
时,我们会构造一个新对象并把它绑定到 foo(...)
调用的 this
上。
绑定优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
例外:关于箭头函数的 this
箭头函数是一种无法使用我们上面四种规则的特殊函数类型。
function foo() {
return (a) => {
console.log(this.a);
}
}
const obj1 = {
a: 2
};
const obj2 = {
a: 3
};
const bar = foo.call(obj1);
bar.call(obj2); // 2,不是 3
foo()
内部创建的箭头函数会捕获调用时 foo()
的 this
。由于 foo()
的 this
绑定到了 obj1
,bar
的 this
也会绑定到 obj1
,箭头函数的绑定无法被修改。(new 也不行)。
箭头函数常用于回调函数中,例如事件处理或定时器:
function foo() {
setTimeout(() => { console.log(this.a) }, 100);
}
const obj = { a: 2 };
foo.call(obj); // 2
总结:
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
参考:《你不知道的 Javascript(上卷)》
网友评论