关于javascript:JavaScript中this指向哪儿如何确定this前端面试进阶

52次阅读

共计 8384 个字符,预计需要花费 21 分钟才能阅读完成。

前言

只有你踏入JavaScript 的世界,那么你肯定会遇到 this 关键词。有许多人所 thisJavaScript 中最简单的货色之一,也有人说 this 其实很简略 …… 然而事实的确,有许多工作了好多年的小伙伴,在 this 指向问题上也经常呈现谬误。

总之,咱们本篇文章的目标就是为了让大家彻底了解 this,遇到 this 不再胆怯!

1. 为什么要有 this?

既然 this 这么多小伙伴都感觉难,那为什么还要应用它呢?依据哲学思想:存在即正当。既然 this 被提了进去,那么它必定帮忙咱们解决了一些问题,又或者晋升了开发效率。

咱们先应用一句比拟官网的一句话来总结 this 解决了什么问题。

较为官网的解释:

this 被主动定义在所有函数的作用域中,它提供了一种更好的形式来“隐式”的传递对象援用,这样使得咱们的 API 设计或者函数变得更加简洁,而且还更容易复用。

看了下面那样官网的一段话是不是感觉脑子变成了一团浆糊,没看懂要紧。咱们能够联合一段代码再来了解。

代码如下:

function say() {console.log("你好!", this.name);
}
let person1 = {name: '小猪课堂'}
let person2 = {name: '张三'}


say.call(person1); // 你好!小猪课堂
say.call(person2); // 你好!张三

下面这段代码非常简单,咱们在函数外部应用了 person1person2 对象中的那么属性,然而咱们的函数实际上并没有接管参数,而是调用 this 隐式的应用了 name 属性,即隐式应用上下文对象中 name,咱们利用了 call 办法将函数外部的 this 指向了 person1person2,这使得咱们的函数变得简洁且容易复用。

大家想一想,如果咱们没有 this,那么咱们就须要显式的将上下文对象传入函数,即显式传入 person1person2 对象。

代码如下:

function say(context) {console.log("你好!", context.name);
}
let person1 = {name: '小猪课堂'}
let person2 = {name: '张三'}


say(person1); // 你好!小猪课堂
say(person2); // 你好!张三

上段代码中没有应用 this,所以咱们间接显式的将上下文对象传入了函数,尽管目前代码看起来不简单,然而随着咱们的业务逻辑逐步简单,或者说函数变得复杂起来,那么咱们传入的 context 上下文对象只会让代码变得越来越凌乱。

然而如果咱们应用了 this,便不会这样,前提是咱们须要分明的晓得 this 指代的上下文对象是谁。

当然,如果你对下面的代码不太了解,别急,慢慢来,看完本篇文章!

更多面试题解答参见 前端进阶面试题具体解答

2.this 谬误的了解

对于很多初学者,刚开始接触到 this 关键词时,经常踏入很多误区。很大一部分起因的确是因为 this 有很多坑,然而最终起因还是没有搞懂 this 的指向原理。这里咱们举出初学者常见的 this 误区,也是很多面试题外面经常喜爱挖坑的中央。

2.1 this 指向函数本身?

这一个误区是很多初学者都会踏入的,毕竟 this 关键词英译过去就是“这里”的意思,咱们在函数外面应用 this,天经地义认为 this 指代的是以后函数。

然而事实果真如此吗?咱们一起来看一段代码。

代码如下:

function say(num) {console.log("函数执行:", num);
  this.count++;
}
say.count = 0;
say(1); // 函数执行:1
say(2); // 函数执行:2
say(3); // 函数执行:3
console.log(say.count); // 0

上段代码中咱们给 say 函数增加了一个 count 属性,因为在 JS 中函数也是一个对象。而后咱们执行了 3 次函数,并且每次执行都调用了 count++

如果咱们认为 this 指向的是函数自身,那么 this.count++ 执行的便是 say.count,所以按理来说咱们最终打印 say.count 后果应该是3,然而后果却是0。阐明this.count 并不是say.count

所以咱们最终得出结论:say 函数外部的 this 并不执行函数自身!

那么咱们上段代码中的 this.count 是哪里的 count 呢?实际上执行 this.count++ 的时候,会申明一个全局变量 count,至于为什么,本篇文章和前面会解释。

打印 count:

console.log(say.count); // 0
console.log(count); // NaN

2.2 this 作用域问题

作用域也是 JS 中比拟难的知识点之一了,咱们这里不会开展说作用域问题。咱们只给出 this 指向在作用域方面的误区,这个误区很多初学者甚至好多年教训的开发者也会踏入此误区。

咱们能够先来看一段十分经典的代码:

function foo() {
  var a = 2;
  this.bar();}
function bar() {console.log(this.a);
}
foo(); // undefined

上段代码中咱们在 foo 函数外部应用 this 调用了 bar 函数,而后在 bar 函数外部打印 a 变量,如果咱们依照作用域链的思维思考的话,此时的 a 变量按情理是可能读取到的,然而事实却是 undefined

造成上述问题的起因有多个,其中有一个就是 this 在任何状况下都不指向函数的词法作用域,上段代码就应用应用 thisfoobar 函数的词法作用域联通,这是不可行的。

至于词法作用域是什么,这里不开展说,须要大家自行上来学习,简略来说词法作用域是由你在写代码时将变量和块作用域写在哪来决定的。

3.this 的定义

看了后面两章节,咱们大略能了解 this 是什么?它其实就是一个执行上下文中的一个属性,大家也能够简略的把 this 当作一个对象,只不过该对象指向哪儿是在函数调用的时候确定的。

咱们简略总结一下 this 的特点:

  • this 是在运行时绑定的,不是在编写时绑定
  • this 的绑定与函数的申明和地位没有任何关系
  • 函数在调用时,会创立一个执行上下文,this 就是这个执行上下文中的一个属性,在函数执行的时候能够用到 this。所以 this 是在函数调用的时候确定绑定关系的,也就是运行时。

所以,总结进去大略就一句话:

this 就是一个对象,this 是在函数被调用时产生的绑定,它指向什么齐全取决于函数在哪里被调用。

4.this 绑定规定

到这里咱们晓得了 this 的绑定是在函数调用的时候确定的,以及 this 不指向函数本身等等问题。那么,函数在某个地位被调用时,咱们怎么确定 this 该绑定到哪里呢?这个时候咱们就须要一些绑定规定来帮忙咱们明确 this 绑定到哪里了,当然,想要使用绑定规定的前提是,咱们须要晓得函数的调用地位。

有些状况下,函数的调用地位咱们能够间接观察出来,然而有些状况稍显简单,这个时候咱们就须要借助 调用栈来 来剖析出函数的理论调用地位了。

咱们能够通过浏览器来查看调用栈,简略来说调用栈就相当于函数的调用链,和作用域链有殊途同归之妙,只是咱们间接看代码剖析可能不太容易。所以咱们能够通过打断点的形式,而后借助浏览器来查看调用栈,如下图所示:调用栈的具体用法还须要大家下来认真学习。

接下来就来学习具体的 this 绑定规定。

4.1 默认绑定

咱们比拟常见的一种函数调用类型就是独立函数的调用,形如 foo() 等。这个时候的 this 绑定就是采纳的默认绑定规定。

代码如下:

var name = '小猪课堂';
function foo(){console.log(this) // Window{}
  console.log(this.name) // 小猪课堂
}
foo(); // 小猪课堂

上段代码非常简单,咱们在全局作用域中定义了一个变量name,而后咱们在函数 foo 中应用this.name,输入的后果就是全局变量name,这阐明咱们 this 指向了全局作用域,也就是说 this 绑定到了 window 对象上。

输入后果:

函数的这种调用形式就被称为默认绑定,默认绑定规定下的 this 指向全局对象。

咱们能够给默认绑定给个定义:

当函数不带用任何润饰进行调用时,此时 this 的绑定就是默认绑定规定,this 指向全局对象。

留神:

let 变量申明不会绑定在 window 下面,只有 var 申明的才会,这是须要留神的。除此之外,严格模式下上段代码的 thisundefined,比方上面这段代码:

var name = '小猪课堂';
function foo(){
  'use strict'
  console.log(this.name)
}
foo(); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')

从上段代码能够看出,默认绑定规定下,this 绑定到了全局对象,当然这与函数调用地位无关。然而严格模式下,this 的绑定与函数调用地位无关。

4.2 隐式绑定

后面的默认绑定规定很好了解,因为咱们的函数执行上下文就是全局作用域,this 自然而然绑定到了全局对象上。

独立函数的调用咱们能够间接看出执行上下文在哪里,但如果不是独立函数调用,比方上面代码。

代码如下:

function foo() {console.log(this.name) // 小猪课堂
}
let obj = {
  name: '小猪课堂',
  foo: foo
}
obj.foo();

上段代码咱们在 obj 对象中援用了函数 foo,而后咱们应用 obj.foo(函数别名)的形式调用了该函数,此时不是独立函数调用,咱们不能应用默认绑定规定。

此时 this 的绑定规定称为隐式绑定规定,因为咱们不能间接看出函数的调用地位,它的理论调用地位在 obj 对象外面,调用 foo 时,它的执行上下文对象为 obj 对象,所以 this 将会被绑定到 obj 对象上,所以咱们函数中的 this.name 其实就是obj.name。这就是咱们的隐式绑定规定。

留神:

如果咱们调用函数时有多个援用调用,比方 obj1.obj2.foo()。这个时候函数 foo 中的 this 指向哪儿呢?其实不论援用链多长,this 的绑定都由最顶层调用地位确定,即obj1.obj2.foo()this 还是绑定带 obj2

隐式绑定中 this 失落

在隐式绑定规定中,咱们认为谁调用了函数,this 就绑定谁,比方 obj.foothis 就绑定到 obj,然而有一些状况比拟非凡,即便采纳的隐式绑定规定,然而 this 并没有依照咱们的想法去绑定,这就是所谓的隐式绑定 this 失落,常见于回调函数中。

代码如下:

function foo() {console.log(this.name) // 小猪课堂
}


function doFoo(fn) {fn(); // 函数调用地位
}


let obj = {
  name: '张三',
  foo: foo
}
let name = '小猪课堂';
doFoo(obj.foo); // 小猪课堂

上段代码中咱们很容易会认为 foo 绑定的 thisobj 对象,因为咱们应用了 obj.foo 的形式,这种形式就是遵循隐式绑定规定。然而事实上 this 却绑定到了全局对象下来,这是因为咱们在 doFoo 函数中调用 fn 时,这里才是函数的理论调用地位,此时是独立函数调用,所以 this 指向了全局对象。

理论我的项目中咱们容易遇到这种问题的场景可能就是定时器了,比方上面的代码:

setTimeout(obj.foo, 100)

这种写法就很容易造成 this 失落。

4.3 显式绑定

后面咱们曾经说了默认绑定和隐式绑定,其中隐式绑定咱们通常是以 obj.foo 这种模式来调用函数的,目标就是为了让 foothis 绑定到 obj 对象上。

这个时候,如果咱们不想通过 obj.foo 的模式调用函数,咱们想要很明确的将函数的 this 绑定在某个对象上。那么能够应用 callapply 等办法,这就是所谓的显式绑定规定。

代码如下:

function foo() {console.log(this.name) // 小猪课堂
}


let obj = {name: '小猪课堂',}


foo.call(obj);

上段代码咱们利用 call 办法间接将 foo 函数外部的 this 指向了 obj 对象,这就是显式绑定。

尽管显式绑定让咱们很分明的晓得了函数中的 this 绑定到了哪个对象上,然而它还是无奈终局咱们 this 绑定失落的问题,就比方上面这种写法:

function foo() {console.log(this.name) // 小猪课堂
}


function doFoo(fn) {fn(); // 函数调用地位
}


let obj = {
  name: '张三',
  foo: foo
}
let name = '小猪课堂';
doFoo.call(obj, obj.foo); // 小猪课堂

上段代码咱们尽管应用 call 来更改 this 绑定,然而最终后果却是没有用的。

尽管显式绑定自身不能解决 this 绑定失落的问题,然而咱们能够通过变通的形式来解决这个问题,也被称作 硬绑定

硬绑定:

function foo() {console.log(this.name) // 小猪课堂
}


function doFoo(fn) {fn(); // 函数调用地位
}


let obj = {name: '张三',}
let bar = function () {foo.call(obj)
}
let name = '小猪课堂';
doFoo(bar); // 张三
setTimeout(bar, 100); // 张三

其实思路也比较简单,呈现 this 绑定失落起因无非就是咱们传入的回调函数在被执行时,this 绑定规定变为了默认绑定,那么为了解决这个问题,咱们无妨在封装一个函数,将 foo 函数的 this 显式绑定到 obj 对象下来即可。

这里提一点,上面写法是谬误的:

doFoo(foo.call(obj));

因为回调函数是在 doFoo 外面执行的,下面的写法相当于 foo 函数立刻执行了。

补充:

其实咱们的 bind 函数就是一个硬绑定,大家想一想,bind 函数是不是创立一个新的函数,而后将 this 指定,是不是就和咱们上面这段代码的成果一样。

let bar = function () {foo.call(obj)
}


// bind 模式
let bar = foo.bind(obj)

4.4 new 绑定

new 关键词置信大家都晓得或者应用过吧,这就是咱们将要将的第 4this 绑定,叫做 new 绑定。

想要晓得 new 绑定规定,咱们就很有必要晓得一个当咱们 new 一个对象的时候做了什么,或者说 new 关键词会做哪些操作。咱们这里简略总结一下,具体的 new 的过程还须要大家自行下来好好学学。

应用 new 来调用函数时,会执行上面操作:

  • 创立一个全新的对象
  • 这个新对象会被执行原型连贯
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其它对象,那么 new 表达式种的函数调用会主动返回这个新对象

咱们能够看到 new 的操作中就有 this 的绑定,咱们在来看看代码。

代码如下:

function foo(name) {this.name = name;}
let bar = new foo('小猪课堂');
console.log(bar.name); // 小猪课堂

上段代码咱们应用 new 关键词调用了 foo 函数,大家留神这不是默认调用规定,这是 new 绑定规定。

5. 优先级

后面咱们总结了 4 条 this 绑定的规定,在大多数状况下咱们只须要找到函数的调用地位,而后再判断采纳哪条 this 绑定规定,最终确定 this 绑定。

咱们这里能够先简略总结一下 4 条规定以及 this 绑定确定流程。

this 绑定确定流程:

先确定函数调用地位,而后确定应用哪条规定,而后依据规定确定 this 绑定。

this 绑定规定:

  • 默认绑定:this 绑定到全局对象
  • 隐式绑定:个别绑定到调用对象,如 obj.foo 绑定到 obj
  • 显式绑定:通过 callapply 指定 this 绑定到哪里

    • 硬绑定:应用 bind 函数
  • new 绑定:应用 new 关键词,绑定到以后函数对象

能够看到,咱们确认 this 绑定的时候有 4 条规定,在通常状况下,咱们能够依据这 4 条规定来判断出 this 的绑定。然而有时候某个函数的调用地位对应了多个绑定规定,这个时候咱们该选用哪一条规定来确定 this 绑定呢?这个时候就须要明确每一条绑定规定的优先级了!

首先咱们要明确的式默认绑定规定的优先级是最低的,所以咱们思考的时候临时不思考默认绑定规定。

5.1 隐式绑定与显式绑定

如果函数调用的时候呈现了隐式绑定和显式绑定,那么具体采纳哪一个规定,咱们通过代码来试验一下。

代码如下:

function foo(){console.log(this.name);
}
let obj1 = {
  name: '小猪课堂',
  foo: foo
}
let obj2 = {
  name: '李四',
  foo: foo
}


obj1.foo(); // 小猪课堂
obj2.foo(); // 李四


obj1.foo.call(obj2); // 李四
obj2.foo.call(obj1); // 小猪课堂

上段代码中咱们波及到了两种 this 绑定,obj.foo 为隐式绑定,this 绑定给 obj 对象,而 foo.call(obj)为显示绑定,this 绑定给 obj 对象。

从上段代码看出,当两个绑定规定都存在的时候,咱们采纳的是显式绑定规定。

总结:

显式绑定 > 隐式绑定

5.2 new 绑定与隐式绑定

接下来咱们看看 new 绑定与隐式绑定的优先级。

代码如下:

function foo(name) {this.name = name;}
let obj1 = {foo: foo}


obj1.foo('小猪课堂');
let bar = new obj1.foo("张三");
console.log(obj1.name); // 小猪课堂
console.log(bar.name); // 张三

上段代码中在在应用 new 关键词的时候又应用了 obj1.foo 隐式绑定,然而最终后果 this 并没有绑定到 obj1 对象上,所以隐式绑定优先级低于 new 绑定。

总结:

隐式绑定 < new 绑定

5.3 显式绑定与 new 绑定

接下来咱们比拟显式绑定与 new 绑定规定,然而咱们 new 绑定与显式绑定的 call、apply 办法不能一起应用,所以咱们无奈通过 new foo.call(obj)来进行测试。

然而咱们后面解说显式绑定的时候,提到一种绑定叫做硬绑定,它也是显式绑定中的一种,所以说咱们能够利用硬绑定与 new 绑定来进行比拟。

代码如下:

function foo(name) {this.name = name;}
let obj1 = {};
let bar = foo.bind(obj1);
bar('小猪课堂');
console.log(obj1.name); // 小猪课堂


let baz = new bar('张三');
console.log(obj1.name); // 小猪课堂
console.log(baz.name); // 张三

上段代码中咱们把 obj 硬绑定到了 bar 上,而后咱们通过 new 绑定调用了函数,然而 obj1.name 还是小猪课堂,并没有改为张三,然而,咱们的 new 操作批改了硬绑定(到 obj1 的)调用 bar 中的 this。因为应用了 new 绑定,咱们失去了新的 baz 对象,并且 baz.name 为张三。

总结:

new 绑定 > 显式绑定,须要留神的是,new 操作时将 this 绑定到了新创建的对象。

6.this 绑定总结

到这儿,咱们基本上可能确定一个函数外部的 this 指向哪儿了,咱们这里做出一些总结,以供在我的项目实际中判断 this 绑定。

this 绑定规定优先级:

默认绑定 < 隐式绑定 < 显式绑定 < new 绑定

判断 this 最终指向,总体流程:

  1. 判断函数调用时是否应用了 new,即 new 绑定,如果应用了,则 this 绑定的是新创建的对象。
  2. 函数调用是否应用了 callapply 等显式绑定,或者硬绑定(bind),如果是的话,this 指向指定的对象。
  3. 函数是否在某个上下文对象中调用,即隐式绑定,如 obj1.foo,如果是的话,this 指向绑定的那个上下文对象。
  4. 以上 3 点都不波及的话,则采纳默认绑定,然而须要留神的是,在严格模式下,默认绑定的 thisundefined,非严格模式下绑定到全局对象。

联合下面的绑定优先级以及判断流程,咱们在个别的我的项目中以及可能判断出 this 指向哪儿了!

总结

this 绑定尽管是一个比拟难的知识点,然而咱们作为一个前端开发者,必须要学会如何了解和应用它,因为它的确能给咱们带来很多的便当和益处。

当然,本篇文章只解说了最惯例的 this 绑定及原理,除此之外,this 绑定还有一些意外的状况,这里不做更多解释,感兴趣的小伙伴能够自行下来查问材料,比如说软绑定、间接援用等等。

总之,惯例判断如下:

  1. new 调用函数,绑定到新对象
  2. call 等调用,绑定到指定对象
  3. 由上下文对象效用,绑定到上下文对象
  4. 默认调用,绑定到全局对象或 undefined

正文完
 0