this指向
# 前言
提到this,基本就会提到this的指向问题。其他静态语言中,this永远指向定义它的对象,在程序编译的时候this的指向已经明确。但是在javaScript中this是动态的,这在提供了极大的灵活性的同时,也使得this的指向在很多时候使得开发者感到困惑。
首先谨记一个原则
this的指向是在程序运行的时候确定的,绝大多数情况下指向当前执行代码的环境对象
以下是几种情况下this
的指向:
# 在全局环境中
// 浏览器环境
console.log(this === window); // true
consol.log(this === globalThis) // true 这是各大环境都必须支持的全局对象
// node环境
console.log(this === global); // true
console.log(this === globalThis) // 这是各大环境都必须支持的全局对象
2
3
4
5
6
7
# 在函数中(手写call、apply、bind)
在函数内部,this
的值取决于函数被调用的方式。
// 非严格模式下this是指向全局对象
function testThis() {
return this
}
testThis() //在浏览器环境中里是window,node环境中是global,只要知道是指向全局对象就可以了
// 严格模式下是undefined
function testThisUseStrict() {
'use strict'
return this
}
testThis() // undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
this
一般都是在运行时才能确定它的指向。但是在某些应用场景下,我们需要操控this
的指向,可以使用call
、apply
、bind
三个方法指定this
的指向。
const obj = {num: 1};
function whatsThis (arg) {
return this.num;
}
let n = whatsThis.apply(obj)
console.log(n); // 1
let m = whatsThis.call(obj);
console.log(m); // 1
2
3
4
5
6
7
8
9
10
call
和apply
效果上没有什么区别,参数上有所不同: 第一个参数都是this
指向的对象,同时都会立即调用函数。真正的区别在于第二个参数,看下面的例子:
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
// 第一个参数是作为‘this’使用的对象
// 后续参数作为参数传递给函数调用
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
// 第一个参数也是作为‘this’使用的对象
// 第二个参数是一个数组,数组里的元素用作函数调用中的参数
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
2
3
4
5
6
7
8
9
10
11
12
13
使用bind
则不会立即调用函数,而是返回一个相同的函数,并且函数内的this
永远指向bind
的第一个参数
const obj = {num: 1};
const anotherObj = {num: 2}
function whatsThis (arg) {
return this.num;
}
let func = whatsThis.bind(obj)
console.log(func()); // 1
let func1 = func.bind(anotherObj);
console.log(func1());// 1 bind一旦生效则后面的绑定都会失效
2
3
4
5
6
7
8
9
10
11
12
这三个是很强大的方法,可以使开发者编写更有趣的代码,但是内部是如何实现的呢?
我们来简单实现下:
手写call
,得先知道call
做了什么事情:
入参:call函数一共接收两个参数,第一个参数是this的指向,第二个参数是传递给该对象的参数
返回:若是调用的函数有返回值就返回,否则返回undefined
const obj = {num: 1};
var num = 2;
function whatsThis (a,b) {
return this.num + a + b;
}
Function.prototype.myCall = function(context) {
if (typeof context === 'undefined' || typeof context === 'null') {
context = globalThis
}
let symbolKey = Symbol("防止覆盖其他属性");
context[symbolKey] = this;
let args = [...arguments].slice(1);
delete context.symbolKey;
return context[symbolKey](...args);
}
let n = whatsThis.myCall() // 这里不传第一个参数,默认取环境中的全局对象为this
console.log(n);
let m = whatThis.myCall(obj)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以看到实现call
的思路其实也是利用了在函数中this指向调用该函数的对象的特点。
手写bind
,其实bind
和call
实在是大同小异,只不过参数变成了数组
const obj = {num: 1};
var num = 2;
function whatsThis (a,b,c) {
return this.num + a + b + c;
}
Function.prototype.myApply = function(context,args) {
if (typeof context === 'undefined' || typeof context === 'null') {
context = globalThis
}
let symbolKey = Symbol("防止覆盖其他属性");
context[symbolKey] = this;
delete context.symbolKey;
return context[symbolKey](...args);
}
let a = whatsThis.myApply(obj,[2,3,6]);
console.log(a); // 12
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
基本也是相同的实现思路,只不过这里对args
不做处理。
手写bind
,bind
和call
和apply
行为有所不同:
- bind方法是返回一个原函数的拷贝,第一个参数是this的指向
- 绑定的函数被调用时,bind方法除了第一个参数之外的参数将会置于实参之前传递给拷贝的函数
- 绑定的函数也可以通过new操作符创建对象,这种行为就像是把原函数当作构造器,此时bind的第一个参数指定的this对象无效。
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new Error("Only function can call the bind method")
}
let _self = this;
let args = Array.prototype.slice.call(arguments,1);
return function () {
let bindArgs = Array.prototype.slice.call(arguments)
return _self.apply(context,args.concat(bindArgs)); // 这里控制了参数调用的顺序,bind的参数是置于调用函数之前的
}
}
2
3
4
5
6
7
8
9
10
11
12
上面比较简单的实现了bind
的基本用法。但是没有考虑到new
操作符的情况,使用new
操作符时new
绑定的函数实际上是new
原函数,基于这个行为,我们再把代码完善下:
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new Error("Only function can call the bind method")
}
let that = this;
let args = Array.prototype.slice.call(arguments,1);
let fBound = function () {
let _self = this instanceof that ? this : context; // **
let bindArgs = Array.prototype.slice.call(arguments)
return that.apply(_self,args.concat(bindArgs));
}
fBound.prototype = that.prototype; // **
return fBound;
}
// 测试用例
function Point(x, y) {
this.x = x;
this.y = y;
}
var emptyObj = {};
var YAxisPoint = Point.myBind(emptyObj, 4);
let s = new YAxisPoint(1); // **
console.log(s.x,s.y) // 4 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
测试没有问题,x
和y
都分别返回了这个值,在使用new
操作符后,返回的绑定函数的this
忽略了myBind
函数的第一个参数,而是指向了new
操作符创建的s
实例。相对于之前的改动,我们改动了**
中的代码,我们分别分析下着三步都做了什么事情:
let _self = this instanceof that ? this : context; // **
指定this
的时候由于考虑到使用new
的情况,我们这里得做一个判断,当前的实例化的对象(上例中就是s
),由于使用了new
关键字,此时的fBound
中的this
其实已经指向了s
这个实例,that
一直是指向调用myBind
的Point
函数,实际就是判断 s.prototype
是否等于Point.protoype
如果返回true
,当前的this
就是指向s
,否则就是没有使用new
,this
指向我们传入myBind
函数的第一个参数就行了。
但是此时存在一个问题,官方的bind
函数返回的也是一个函数,也就是上例中的fBound
,实际上new
操作符操作的是new fBound
,并不是new Point
,我们的实例s
访问不了我们传入的参数4
和1
,那怎么办呢?我们就可以把fBound
的prototype
指向Point
的prototype
,这样s
实例就可以访问Point
的属性了,这符合了官方的bind
函数的行为。也就是下面这段代码的作用。
fBound.prototype = that.prototype;
但是这样的写法就有问题的,什么问题呢?我们测试一下:
function Point(x, y) {
this.x = x;
this.y = y;
}
var emptyObj = {};
var YAxisPoint = Point.myBind(emptyObj, 4);
let s = new YAxisPoint(1);
let sPrototype = Object.getPrototypeOf(s);
sPrototype.m = 520; // 造成原型链污染了
let currentPoint = new Point(11);
console.log(currentPoint.m); // 520
2
3
4
5
6
7
8
9
10
11
12
13
由于fBound
和Point
拥有相同的prototype
,当我们修改fBound
的原型的属性值时,污染了Point
的prototype
的,即使我们没有在Point
的原型上定义m
属性,但是仍然获取了m
属性,显然这不是我们希望的行为,我们希望的是修改fBound
的原型不会影响到Point
的原型,可以使用一个中间对象传递原型:
// 完整版本
Function.prototype.myBind = function (context) {
if (typeof this !== "function") {
throw new Error("Only function can call the bind method");
}
let that = this;
let args = Array.prototype.slice.call(arguments, 1);
let fNOP = function () {}
let fBound = function () {
let _self = this instanceof fNOP ? this : context; // 检测是否是new操作符的操作
let bindArgs = Array.prototype.slice.call(arguments);
return that.apply(_self, args.concat(bindArgs));
};
if (this.prototype) {
fNOP.prototype = this.prototype
}
fBound.prototype = new fNOP(); // 当我们修改fBound.prototype,只会修改当前这个实例,而不会修改Point.prototype
return fBound;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们测试下:
function Point(x, y) {
this.x = x;
this.y = y;
}
var emptyObj = {};
var YAxisPoint = Point.myBind(emptyObj, 4);
let s = new YAxisPoint(1);
let sPrototype = Object.getPrototypeOf(s);
sPrototype.m = 520;
console.log(s.m); // 当前实例被修改 520
let currentPoint = new Point(11);
console.log(currentPoint.m); // 没有定义m这个属性 undefiend
2
3
4
5
6
7
8
9
10
11
12
13
14
这样我们就完整的实现了bind函数
。
# 箭头函数中this
箭头函数没有自己的
this
,this
与封闭词法环境的this
保持一致。在全局代码中,它将被设置为全局对象
看个例子:
const obj = {
name: '马松松',
getName: function () {
console.log(this == obj,111); // true 111
setTimeout(() => {
console.log(this == obj,222); // 1s后 输出 true 222
},1000)
}
}
let myName = obj.getName()
2
3
4
5
6
7
8
9
10
11
可以看到调用getName
方法时,父级的this
是obj
,箭头函数由于没有自己的this
,所以使用的是父级的this
,也就是obj
再看这个用法:
const obj = {
name: '马松松',
getName: () => {
return this.name
}
}
obj.getName() // undefined 对象没有this
2
3
4
5
6
7
8
注意这种用法是不正确的。
# 作为对象的方法
这种方法已经很常规了,这里就不多说了,只要是注意对象调用多个成员的情况:
function function func () {
return this.num;
}
const obj = {num: 1};
obj.b = {f: func, num: 2}
console.log(obj.b.f()); // 2
2
3
4
5
6
this
的绑定只受最近成员引用的影响,上面例子中最近的调用是{f: func, num: 2}
,所以取的是2
;
# 原型链中的this
原型链中的this
,和对象中的this
指向规则是一样的。
const obj = {
f: function () {
return this.num;
}
}
const o = Object.create(obj);
o.num = 2;
console.log(o.f()) // 2
2
3
4
5
6
7
8
9
虽然在o
中没有f
这个方法,但是查找原型链在obj
中找到这个方法,最后看起来是o
调用了这个方法,所以this
只想o
# getter和setter中的this
function sum () {
return this.a + this.b
}
const o = {
a: 1,
b: 2,
c: 3,
get average () {
return (this.a + this.b) / 2
}
}
Object.defineProperty(o, 'sum', {
get: sum,
enumerable: true,
configurable: true
})
console.log(o.average,o.sum);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
其实也是相同的规则,均是指向设置或者绑定的属性对象。
# 构造函数中
也就是使用new
操作符,this
被绑定到实例的新对象。
function C(){
this.a = 37;
}
var o = new C();
console.log(o.a); // 37
2
3
4
5
6
不过注意一种行为,当构造函数有返回时,this
指向的默认对象被丢弃了,this
指向返回的对象。
function C2(){
this.a = 37;
return {a:38};
}
o = new C2();
console.log(o.a); // 38
2
3
4
5
6
# 作为DOM事件处理函数
this
指向触发事件的元素
function (e) {
console.log(this === e.currentTarget); // true
}
2
3
# 作为内联事件处理函数
this
指向监听器所在的DOM元素
<button onclick="alert(this.tagName.toLowerCase());">
Show this
</button>
2
3
这里会弹出button。
# 小结
其实,正如开头说的基本准则,this
绝大多数情况都是指向当前执行代码的环境对象,只要记住这个准则,就能解决绝大多数的this指向问题。