深耕系列之this指向

131 阅读4分钟

前面文章讲到深耕系列之执行上下文,提到在ES8中,执行上下文中的this值被归纳到词法环境,this指针也是我们经常使用的。

那么这篇文章会去深入理解this,理解this的指向。

先来看一个例子

var value = 1
var obj = {
	value: 2,
	getValue: function() {
		return this.value
	}
}
function fn() {
	return this.value
}

console.log(fn())
console.log(obj.getValue())
console.log((obj.getValue)())
var test = obj.getValue
console.log(test())

粘贴到控制台上打印,发现相关结果如下:

console.log(fn())  // 1
console.log(obj.getValue())  // 2
console.log((obj.getValue)()) // 2
var test = obj.getValue
console.log(test()) // 1

注:以上是在非严格模式下的结果,严格模式下的this会保持为undefined,会报错。

可以看出,使用括号和使用赋值语句的打印结果是不相同的,那么该如何确定this的指向呢?

this指向

我们来分析一下不同执行上下文中的this指向。

全局上下文

无论是否在严格模式下,this都指向全局对象

console.log(this)
console.log(this === window)  // true

可使用globalThis 获取全局对象,不管当前上下文是否是全局上下文。

函数上下文

在函数内部,可简单总结,谁调用就this就指向谁。

function fn() {
	return this
}
fn() === window  // true, fn()相当于window.fn(),为window调用,所以this指向window

但在严格模式下,如果没有设置this值,this会保持为undefined

function fn() {
	"use strict";
	return this
}
fn() === window  // false
fn() === undefined  // true

另外,最开始的那个例子中,使用括号并不会影响到this的指向,但使用赋值语句后,会改变this指向,this会指向全局对象

console.log((obj.getValue)()) // 2
var test = obj.getValue
console.log(test()) // 1

类上下文

因为类本质上也是函数,所以this指向是类似的。在类的构造函数中,会把所有非静态的方法添加到this的原型上。

class Example {
  constructor() {
    const proto = Object.getPrototypeOf(this);
    console.log(Object.getOwnPropertyNames(proto));
  }
  first(){}
  second(){}
  static third(){}
}

new Example(); // ['constructor', 'first', 'second']

遇到需要更改this指向的场景,我们该怎么办?

更改this指向

call函数

var obj = { a: 1 }
var a = 2

function fn(param) {
	return param + this.a
}

fn('test')  // test2
fn.call(obj, 'test')  // test1

通过call方法,把fn函数内的this设置为obj,所以fn.call(obj, 'test') 打印出来的是test1

为了更好的理解call,我们来手写代码实现call函数,如下:

Function.prototype.myCall = function(obj) {
	obj = obj || window
	obj.fn = this
	const args = [...arguments].slice(1)
	const result = obj.fn(...args)
	
	delete obj.fn
	return result
}

实现思路:

  1. 不传入第一个参数,那么上下文就默认为window
  2. 给对象obj创建一个属性fn,并将值设置为需要调用的函数
  3. 移除arguments的第一个元素来获取调用函数的参数
  4. 支持return并把ojb的fn属性删除

apply函数

var obj = { a: 1 }
var a = 2

function fn(param) {
	return param + this.a
}

fn('test')  // test2
fn.apply(obj, ['test'])  // test1

apply方法和call方法类似,区别在于参数的处理,我也贴出apply的实现代码,如下:

Function.prototype.myApply = function(obj, arr) {
	obj = obj || window
	obj.fn = this
	const result = obj.fn(...arr)
	
	delete obj.fn
	return result
}

bind函数

var obj = { a: 1 }
var a = 2

function fn(param = '') {
	console.log(param + this.a)
}

fn() // 2
const newFn = fn.bind(obj, 'test')
newFn() // test1

可以看出,bind方法会创建一个新的函数, 在bind被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

实现代码如下:

Function.prototype.myBind = function(obj) {
	const self = this
	const args = [...arguments].slice(1)
	
	return function F() {
		// 这个时候的arguments是指bind返回的函数传入的参数
		
		// 因为返回了一个函数,我们可以 new F(),所以需要判断
		if (this instanceof F) {
			return new self(...args, ...arguments)
		}
		
		return self.apply(obj, args.concat(...arguments))
	}
}

实现思路:

  1. bind方法会返回一个函数,该函数有两种调用方式,一种是直接调用,一种是通过new方式,都需支持。
  2. 针对直接调用,我们使用apply的方式实现。
  3. 因为bind会有类似这种的代码fn.bind(obj, 1)(2),所以需要将两边的参数通过concat拼接起来。

箭头函数

在箭头函数中,this与封闭词法环境的this保持一致。在全局代码中,它将被设置为全局对象:

var arrowFn = (() => this)
var fn = function() {
	return this
}
var obj = { a: 1}

console.log(arrowFn() === window) // true
console.log(fn() === window) // true
console.log(arrowFn.call(obj) === window) // true
console.log(fn.call(obj) === window) // false

可以看出,如果将this传递给call、bind、apply来调用箭头函数,它将被忽略

总结

以上就是我对this指向的理解,后续文章会再总结new等内容。如果觉得对你有帮助,请帮忙点个赞+评论+收藏,关注不失联。