引言
而在面试中,经常会被面试官问到各种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.b
为undefined
?
在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
下的foo
,this
会指向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
挂载到obj2
的fn
上,并没有改变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
创建对象,此时thi
s会发生改变,回顾一下new
关键字做了什么操作
new
操作符的执行过程:
- 创建一个空对象
- 设置原型,将构造函数的原型指向空对象的
prototype
属性。 - 将
this
指向这个对象,通过apply执行构造函数。 - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
需要注意的是区分构造函数的返回值,看个例子
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
为箭头函数,此时this
为window
看这道题,最后输出多少
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