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

引言

而在面试中,经常会被面试官问到各种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

js 复制代码
console.log(this === window) // true

独立调用

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

js 复制代码
function foo() {
	console.log(this === window)  // true
}
foo()

看这段代码

js 复制代码
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声明,大家在看的时候要注意这种细节

再看一个独立调用例子

js 复制代码
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会绑定到这个对象,也就是谁调用就指向谁

js 复制代码
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指向离自己最近的那个对象

js 复制代码
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

js 复制代码
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

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

看个例子

js 复制代码
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

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

js 复制代码
const obj1 = {
	text: 1,
	fn: function(){
		return this.text
	}
}

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

obj2.fn()

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

js 复制代码
const obj2 = {
	text: 2,
	fn: function() {
		return this.text
	}
}

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

js 复制代码
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接受一个数组。

正常的绑定

js 复制代码
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,此时不会改变的

js 复制代码
var a= 'ikun'
function fn() {
	console.log(this.a)
}

fn.call(null) // ikun

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

js 复制代码
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

js 复制代码
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. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

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

js 复制代码
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属性。

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

js 复制代码
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由外层上下文来决定的。【面试基础八股】

js 复制代码
const obj = {
	name: 'ikun',
	foo: () => {
		console.log(this)
	}
}

obj.foo()

foo为箭头函数,此时thiswindow

看这道题,最后输出多少

js 复制代码
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
相关推荐
时清云27 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学35 分钟前
宏队列和微队列
前端·javascript
沉登c1 小时前
Javascript客户端时间与服务器时间
服务器·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
小程xy4 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
J老熊4 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
我爱学Python!5 小时前
面试问我LLM中的RAG,秒过!!!
人工智能·面试·llm·prompt·ai大模型·rag·大模型应用
OLDERHARD5 小时前
Java - LeetCode面试经典150题 - 矩阵 (四)
java·leetcode·面试
非著名架构师5 小时前
js混淆的方式方法
开发语言·javascript·ecmascript
银氨溶液6 小时前
MySql数据引擎InnoDB引起的锁问题
数据库·mysql·面试·求职