理解this的上下文:JavaScript中的this指向机制

2,245 阅读8分钟

引言

而在面试中,经常会被面试官问到各种this指向问题,稍有不注意就容易就会回答错误,在我过往的面试经历中,大部分公司问的最多代码输出题是是this指向和promise输出结果。看起来这俩很简单,多背多看一些题就可以,但实际面试中面试官会留下各种坑,看错一个符号就会导致面试失败。它不像八股一样,多背就行,需要知道它的各种绑定特点,去理解它所在的一个上下文,接下来就和大家分享一下我是如何去学习this指向的,其实很简单,只要理解几种绑定形式就可以。

什么是this

在JavaScript中,this是一个关键字,用于指代当前执行上下文中的对象。这个对象通常被称为上下文对象,它可以是一个普通对象、函数、或者构造函数的实例。this充当了一个占位符,代表了当前代码片段与其所在上下文之间的联系。

this具有动态性,是指它的值在函数调用时才确定,而不是在函数定义时。这导致了this的行为具有上下文相关性,即它的值取决于代码的执行上下文。具体来说:

  • 函数调用方式决定this:在JavaScript中,this的值取决于函数被调用的方式。它可以是以下几种之一:

    • 在全局上下文中,this指向全局对象(通常是window对象)。
    • 在对象方法中,this指向调用该方法的对象。
    • 在构造函数中,this指向新创建的实例对象。
    • 在事件处理函数中,this通常指向触发事件的DOM元素。
  • 箭头函数的例外:箭头函数是JavaScript中的一个特殊情况。它们不会创建自己的this上下文,而是继承外部函数的this。这使得箭头函数的this是静态的,不会随着调用方式而改变。

接下来结合一些面试题,一一来看几种绑定形式。

默认绑定

非严格环境下,全局下的this指向window, 而在严格环境下是undefined,不允许this指向全局window


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

独立调用

当函数独立调用时,this会指向window

function foo() {
	console.log(this === window)  // true
}
foo()

看这段代码

var a = 'ikun'
let b = 'jntm'
function foo() {
	console.log(this.a) // ikun
	console.log(this.b) // undefined
}

foo()

由于foo是在全局环境下调用,他的this指向window ,也就是访问window.a。那为什么this.bundefined

在ES5中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。但ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

❗️这里我之前面试踩过雷,没有看清变量是let还是var声明,大家在看的时候要注意这种细节

再看一个独立调用例子

var a = 'ikun'

const obj ={
	a: 'jntm',
	foo: function() {
		function fn() {
			console.log(this)
			console.log(this.a)
		}
		return fn()
	}
}

obj.foo()

foo函数执行时,此时的this指向的是obj,但是在函数内部独立调用了一个函数fn,此时this会指向window

作为方法调用

当函数作为对象的属性方法调用时,this会绑定到这个对象,也就是谁调用就指向谁

var a = 'ikun'
var obj = {
	a: 'jntm',
	foo: function() {
		var a = 'jnssssztm'
		console.log(this) // {a: jntm, foo: ƒ}
		console.log(this.a) // jntm
	}
}
obj.foo()

foo函数在obj下调用,所以此时的this指向obj对象,也就输出jntm

如果函数调用前面有多个对象,this指向离自己最近的那个对象

function fn(){
	console.log(this.a)
}

const obj1 = {
	a: 'ikun',
	foo: fn
}

const obj2 = {
	a: 'jntm',
	o: obj1
}

obj2.o.foo() // ikun

通过o调用obj1下的foothis会指向o也就是obj1

立即执行函数

立即执行函数也就是定义后立刻执行的匿名函数,在立即执行函数内this指向window

var a = 'ikun'

const obj = {
	a: 'jntm',
	foo: function() {
		(function() {
			console.log(this) // window
			console.log(this.a) // ikun
		})()
	}
}

obj.foo()

隐式绑定

在某些特殊情况下会存在this丢失的问题,常见的就是将调用函数作为参数传递或者变量赋值给另外一个变量,此时this指向window

比如这样,通过一个变量fn1来接收函数,此时指向window,也就输出window、ikun

var bar = 'ikun'
const foo = {
	bar: 'jntm',
	fn: function() {
		console.log(this)
		console.log(this.bar)
	}
}
var fn1 = foo.fn
fn1()

看个例子

const obj1 = {
	text: 1,
	fn: function(){
		return this.text
	}
}

const obj2 = {
	text: 2,
	fn: function(){
		return obj1.fn()
	}
}

const obj3 = {
	text: 3,
	fn: function(){
		var fn1 = obj1.fn
		return fn1()
	}
}

console.log(obj1.fn())
console.log(obj2.fn())
console.log(obj3.fn())

这是经典的隐式绑定题

  • 第一个obj1很简单,fn调用时没有其他操作只是访问了this.text,此时的this指向obj1所以输出1
  • 第二个在执行fn时,返回的是obj1.fn(),可以理解为obj2.fn().obj1.fn(),此时和第一个执行一样,也是在obj1下执行,最后输出1
  • 第三个是一个隐式丢失,在fn内部用一个变量fn1保存obj1.fn函数,最后再将函数返回,此时this就指向window,也就相当于在全局环境下执行function(){return this.text},最后输出为undefined

我们稍作改变一下,再看看输出结果

const obj1 = {
	text: 1,
	fn: function(){
		return this.text
	}
}

const obj2 = {
	text: 2,
	fn: obj1.fn
}

obj2.fn()

此时输出结果为2,在执行fn的时候,是将obj1.fn挂载到obj2fn上,并没有改变this指向,也就相当于

const obj2 = {
	text: 2,
	fn: function() {
		return this.text
	}
}

看懂这个例子后再看一个类似的,

var a = 'ikun'
function foo() {
	console.log(this.a)
}

function foo2() {
	foo()
}

const obj = {
	a: 1,
	foo3: foo2
}

obj.foo3() // ikun

显示绑定call、apply、bind

通过call、apply、bind方法强制改变this指向,让它指向我们指定的对象,这里要注意call、apply、bind三个方法改变this指向的区别!

总结就是call、apply会直接进行函数调用,bind不会立即执行函数,而是返回一个新的函数,返回的这个新函数已经自动绑定了新的this。在传参上,call、bind都是接受多个参数,apply接受一个数组。

正常的绑定

const var = {
	name: 'ikun',
	foo: function() {
		console.log(this.name)
	}
}

const var = {
	name: 'tkl'
}
obj.foo.call(obj2)

这里输出结果为tkl,不难理解,函数foo在调用时通过call改变了this指向,指向了obj2

需要注意的是,如果吧null、undefined作为this传入call、apply、bind,此时不会改变的

var a= 'ikun'
function fn() {
	console.log(this.a)
}

fn.call(null) // ikun

多个bind同时改变,最终的this由第一次bind决定【这是一道面试题】

var obj = {
	name: 'ikun',
	foo: function() {
		console.log(this.name)
	}
}

var obj2 = {
	name: 'tkl'
}

var obj3 = {
	name: 'hcy'
}
obj.foo.bind(obj2).bind(obj3)()

最后输出结果tkl,也就是第一次bind(obj2)的结果

setTimeout和setInterval

setTimeout() 执行的代码是从一个独立于调用setTimeout的函数的执行环境中调用的,它将默认为 window

var name = 'ikun'

const obj = {
	name: 'jntm',
	foo: function(){
		setTimeout(function() {
			console.log(this.name)
		})
	}
}

obj.foo() // ikun

构造函数绑定

函数可以作为构造函数使用new创建对象,此时this会发生改变,回顾一下new关键字做了什么操作

new操作符的执行过程:

  1. 创建一个空对象
  2. 设置原型,将构造函数的原型指向空对象的 prototype 属性。
  3. this 指向这个对象,通过apply执行构造函数。
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

需要注意的是区分构造函数的返回值,看个例子

function Foo() {
	this.name = 'ikun'
	this.age = 66
	const obj = {
		name: 'tkl'
	}
	return obj
}

const foo = new Foo()
console.log(foo.name) // tkl
console.log(foo.age) // undefined

在构造函数内返回了一个对象,此时的实例foo就指向返回的obj,所以输出tkl,也没有age属性。

如果没有返回或者返回是一个原始类型,就指向实例

function Foo() {
	this.name = 'ikun'
	this.age = 66
	const obj = {
		name: 'tkl'
	}
	return name
}

const foo = new Foo()
console.log(foo.name) // ikun
console.log(foo.age) // 66

总结:如果构造函数中返回一个对象,那么this就指向这个对象,如果返回基础数据或者没有返回,this就指向实例。

箭头函数绑定

ES6新增的箭头函数是没有this的,它的this由外层上下文来决定的。【面试基础八股】

const obj = {
	name: 'ikun',
	foo: () => {
		console.log(this)
	}
}

obj.foo()

foo为箭头函数,此时thiswindow

看这道题,最后输出多少

var obj = {
   say: function() {
     var f1 = () =>  {
       console.log("1111", this);
     }
     f1();
   },
   pro: {
     getPro:() =>  {
        console.log(this);
     }
   }
}
var o = obj.say;
o();
obj.say();
obj.pro.getPro();
  • obj.say隐式绑定到o上,此时thiswindow,执行函数o,内部执行f1箭头函数,由于此时上下文为window,所以箭头函数的this指向window,最后输出1111window
  • 通过对象调用执行say函数,此时this指向obj,所以箭头函数this指向obj,最后输出111 {pro: {…}, say: ƒ}
  • 也是通过对象调用执行getPro函数,它是一个箭头函数,此时thiswindow