引言
而在面试中,经常会被面试官问到各种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.b为undefined?
在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下的foo,this会指向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挂载到obj2的fn上,并没有改变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操作符的执行过程:
- 创建一个空对象
- 设置原型,将构造函数的原型指向空对象的
prototype属性。 - 将
this指向这个对象,通过apply执行构造函数。 - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
需要注意的是区分构造函数的返回值,看个例子
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为箭头函数,此时this为window
看这道题,最后输出多少
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上,此时this为window,执行函数o,内部执行f1箭头函数,由于此时上下文为window,所以箭头函数的this指向window,最后输出1111,window- 通过对象调用执行
say函数,此时this指向obj,所以箭头函数this指向obj,最后输出111 {pro: {...}, say: ƒ} - 也是通过对象调用执行
getPro函数,它是一个箭头函数,此时this为window