在写 js 的时候,不可避免地会碰到 this
这个关键字,初学者往往会被 this 的指向搞得一头雾水,但实际上 this 并不是什么很难懂的东西,理解了它的逻辑,判断起来就十分轻松啦。
this 的指向
每个函数内都有一个 this 关键字,储存着函数调用者的信息。函数的 this 只有在函数调用时才能被确定,即运行时绑定 ,这意味着每次函数调用的时候,其 this 的值都有可能不同。
隐式绑定
通常情况下,this 的指向遵循“谁调用,指向谁”的原则,即 this 始终指向直接调用了这个函数的对象,这个性质也被称为隐式绑定
举个简单的例子:
1
2
3
4
5
6
7
8
|
const obj = {
name: "obj",
sayName() {
console.log(this);
},
};
obj.sayName(); // obj
|
这里直接调用 sayName
方法的是 obj
对象,那么 sayName
方法内的 this 就指向 obj
再嵌套一层的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const obj = {
name: "obj",
func: {
sayHello() {
console.log("Hello");
},
sayName() {
console.log(this);
},
},
};
obj.func.sayName(); // func
|
这里直接调用 sayName
方法的是 func
对象,那么 sayName
方法内的 this 就指向 func
但有一种比较奇怪的情况:
1
2
3
4
5
6
7
8
|
const obj = {
sayName() {
console.log(this);
},
};
const mySayName = obj.sayName;
mySayName(); // window
|
这里将 obj.sayName
方法赋值给了mySayName
变量,再将其直接调用,那么实际上就等同于由 window
直接调用了 sayName
方法,自然打印的是 window 了
默认绑定
我们通常情况下在最外部直接声明的函数、变量都是定义在 windows
对象上的,那么自然而然,函数直接调用时 this 指向的就是 window
对象,这也称之为 this 的默认绑定
1
2
3
4
5
6
|
function sayName() {
console.log(this);
}
sayName(); // window
// 等价于 `window.sayName()`
|
隐式丢失
直接上例子
1
2
3
4
5
6
7
8
9
10
11
|
const obj = {
name: "obj",
fn: function () {
console.log(this);
},
};
function fn1(fn) {
fn();
}
fn1(obj.fn); // window
|
咋一看可能有点奇怪,但是实际上这里的 obj.fn
只是作为一个函数传入了 fn1
中,在这个过程中和 obj
本身没有产生任何关系,可以理解为是
1
2
3
4
5
6
7
|
function fn1(fn) {
fn();
}
fn1(function () {
console.log(this);
}); // window
|
这就称之为隐式丢失
显式绑定
我们可以使用 call
、 apply
和 bind
方法来直接改变 this 的指向
Function.prototype.call
借用对象外部的方法:
1
2
3
|
Array.prototype.slice.call(arguments, 1);
// or
[].slice.call(arguments, 1);
|
函数的参数储存在 arguments
类数组对象中,这个对象并没法直接调用数组原型上的 slice
方法,因此可以使用 call
来“借用”数组原型上的这个方法。
1
2
3
4
5
6
7
|
Function.prototype.myCall = function (obj = window, ...args) {
const symbol = Symbol();
obj[symbol] = this;
const res = obj[symbol](...args);
delete obj[symbol];
return res;
};
|
1
2
3
4
5
6
7
8
9
10
|
function say(greeting, word) {
console.log(`${greeting},${this.name},${word}!`);
}
const Xiaoming = {
name: "小明",
};
say.myCall(Xiaoming, "你好啊", "今天天气不错");
// 你好啊,小明,今天天气不错!
|
Function.prototype.apply
1
2
3
4
5
6
7
|
Function.prototype.myApply = function (obj = window, args) {
const symbol = Symbol();
obj[symbol] = this;
const res = obj[symbol](...args);
delete obj[symbol];
return res;
};
|
1
2
3
4
5
6
7
8
9
10
|
function say(greeting, word) {
console.log(`${greeting},${this.name},${word}!`);
}
const Xiaoming = {
name: "小明",
};
say.myApply(Xiaoming, ["你好啊", "今天天气不错"]);
// 你好啊,小明,今天天气不错!
|
Function.prototype.bind
1
2
3
4
5
6
7
|
Function.prototype.myBind = function (obj = window, ...args) {
// 需要绑定 this
const that = this;
return function () {
that.apply(obj, args);
};
};
|
这里的 myBind
仅仅实现了基本功能,并不支持柯里化性质,与原生 bind
函数仍有区别
1
2
3
4
5
6
7
8
9
|
function say(greeting, word) {
console.log(`${greeting},${this.name},${word}!`);
}
const Xiaoming = {
name: "小明",
};
say.myBind(Xiaoming, "你好啊", "今天天气不错")();
|
正如上文所说,这里调用完 say.myBind()
后,再自调用的时候,实际上 this 指向的是 window 对象。上面这里完全可以写成:
1
2
|
const mySay = say.myBind(Xiaoming, "你好啊", "今天天气不错");
mySay(); // this 指向 window
|
所以 bind
需要在返回函数前先绑定作用域,并利用闭包性质,使得返回的函数在外部调用时,也能访问到 that
变量。
特殊情况
new 关键字
在使用 new
关键字调用构造函数时,this 默认指向实例,如
1
2
3
4
5
6
7
|
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this);
}
const p1 = new Person("John", 30);
|
这里的 this 指向 p1
,正因此我们可以将属性赋值到实例上
箭头函数
箭头函数内的 this
是定义该函数时所在的作用域指向的对象,而非调用时确定。箭头函数内的 this 等价于其上层作用域的 this
这里需要注意,js 中只存在全局作用域( window
)、块级作用域和函数作用域,因此需要使用函数去改变对象中的作用域。
这里涉及到了从函数外部访问到函数内部作用域,因此需要使用闭包实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const name = "window";
const obj = {
name: "obj",
child: {
name: "child",
func() {
const name = "func";
return () => {
console.log(this.name);
};
},
},
};
obj.child.func()(); // child
|
在这里,箭头函数中的 this 指向是其上层作用域的 this ,即函数 func
作用域的的 this ,那么自然就是其所处的 child
了。
而如果将 fun
也改写成箭头函数,则结果又有所不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const name = "window";
const obj = {
name: "obj",
child: {
name: "child",
func: () => {
const name = "func";
return () => {
console.log(this.name);
};
},
},
};
obj.child.func()(); // window
|
这里打印出的是 window
,原因是最内层的箭头函数向上寻找上级作用域,找到了 func
,但是它也是一个箭头函数,所以就会继续向上寻找,那再上层的作用域就是 window 了,自然打印出的也就是 window 了。
由于对象的层级嵌套并不会造成作用域级别的改变,因此也不会改变箭头函数的 this 指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const name = "window";
const obj = {
name: "obj",
child: {
name: "child",
child: {
name: "child2",
child: {
name: "child3",
func: () => {
console.log(this.name);
},
},
},
},
};
obj.child.child.child.func();
// 仍然只会打印 window
|
此外,call
、apply
和 bind
等函数也无法改变箭头函数的 this 指向
基于箭头函数的这个性质,我们就完全不需要在注册回调之前绑定 this
的值了,上文提到的 bind
函数也可以改写成这样
1
2
3
4
5
6
|
Function.prototype.myBind = function (obj = window, ...args) {
// 不需要 const that = this
return () => {
this.apply(obj, args);
};
};
|
这里的 this 直接就会指向调用这个方法的函数本身。使用箭头函数也可以避免在类声明中的一些 this 指向问题