只要你踏入JavaScript 的世界,那么你一定会遇到 this 关键词。有许多人所 this 是 JavaScript 中最复杂的东西之一,也有人说 this 其实很简单......但是事实确实,有许多工作了好多年的小伙伴,在 this 指向问题上也常常出现错误。
总之,我们本篇文章的目的就是为了让大家彻底理解 this,遇到 this 不再害怕!
既然 this 这么多小伙伴都觉得难,那为什么还要使用它呢?根据哲学思想:存在即合理。既然 this 被提了出来,那么它肯定帮助我们解决了一些问题,又或者提升了开发效率。
我们先使用一句比较官方的一句话来总结 this 解决了什么问题。
较为官方的解释:
this 被自动定义在所有函数的作用域中,它提供了一种更好的方式来“隐式”的传递对象引用,这样使得我们的 api 设计或者函数变得更加简洁,而且还更容易复用。
看了上面那样官方的一段话是不是感觉脑子变成了一团浆糊,没看懂要紧。我们可以结合一段代码再来理解。
代码如下:
function say() {
console.log("你好!", this.name);
}
let person1 = {
name: 'fly63前端网'
}
let person2 = {
name: 'fly63'
}
say.call(person1); // 你好! fly63前端网
say.call(person2); // 你好! fly63
上面这段代码非常简单,我们在函数内部使用了 person1 和 person2 对象中的那么属性,但是我们的函数实际上并没有接收参数,而是调用 this 隐式的使用了 name 属性,即隐式使用上下文对象中 name,我们利用了 call 方法将函数内部的 this 指向了 person1 和 person2,这使得我们的函数变得简洁且容易复用。
大家想一想,如果我们没有 this,那么我们就需要显式的将上下文对象传入函数,即显式传入 person1 和 person2 对象。
代码如下:
function say(context) {
console.log("你好!", context.name);
}
let person1 = {
name: 'fly63前端网'
}
let person2 = {
name: 'fly63'
}
say(person1); // 你好! fly63前端网
say(person2); // 你好! fly63
上段代码中没有使用 this,所以我们直接显式的将上下文对象传入了函数,虽然目前代码看起来不复杂,但是随着我们的业务逻辑逐渐复杂,或者说函数变得复杂起来,那么我们传入的 context 上下文对象只会让代码变得越来越混乱。
但是如果我们使用了 this,便不会这样,前提是我们需要清楚的知道 this 指代的上下文对象是谁。
当然,如果你对上面的代码不太理解,别急,慢慢来,看完本篇文章!
对于很多初学者,刚开始接触到 this 关键词时,常常踏入很多误区。很大一部分原因确实是由于 this 有很多坑,但是最终原因还是没有搞懂 this 的指向原理。这里我们举出初学者常见的 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
作用域也是 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 在任何情况下都不指向函数的词法作用域,上段代码就使用使用 this 将 foo 和 bar 函数的词法作用域联通,这是不可行的。
至于词法作用域是什么,这里不展开说,需要大家自行下去学习,简单来说词法作用域是由你在写代码时将变量和块作用域写在哪来决定的。
看了前面两章节,我们大概能理解 this 是什么?它其实就是一个执行上下文中的一个属性,大家也可以简单的把 this 当作一个对象,只不过该对象指向哪儿是在函数调用的时候确定的。
我们简单总结一下 this 的特点:
所以,总结出来大概就一句话:
this 就是一个对象,this 是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
到这里我们知道了 this 的绑定是在函数调用的时候确定的,以及 this 不指向函数自身等等问题。那么,函数在某个位置被调用时,我们怎么确定 this 该绑定到哪里呢?这个时候我们就需要一些绑定规则来帮助我们明确 this 绑定到哪里了,当然,想要运用绑定规则的前提是,我们需要知道函数的调用位置。
有些情况下,函数的调用位置我们可以直接观察出来,但是有些情况稍显复杂,这个时候我们就需要借助调用栈来来分析出函数的实际调用位置了。
我们可以通过浏览器来查看调用栈,简单来说调用栈就相当于函数的调用链,和作用域链有异曲同工之妙,只是我们直接看代码分析可能不太容易。所以我们可以通过打断点的方式,然后借助浏览器来查看调用栈,如下图所示:
调用栈的具体用法还需要大家下来仔细学习。接下来就来学习具体的 this 绑定规则。
我们比较常见的一种函数调用类型就是独立函数的调用,形如foo()等。这个时候的 this 绑定就是采用的默认绑定规则。
代码如下:
var name = 'fly63前端网';
function foo(){
console.log(this) // Window{}
console.log(this.name) // fly63前端网
}
foo(); // fly63前端网
上段代码非常简单,我们在全局作用域中定义了一个变量name,然后我们在函数 foo 中使用this.name,输出的结果就是全局变量name,这说明我们 this 指向了全局作用域,也就是说 this 绑定到了 window 对象上。
函数的这种调用方式就被称为默认绑定,默认绑定规则下的 this 指向全局对象。
我们可以给默认绑定给个定义:
当函数不带用任何修饰进行调用时,此时 this 的绑定就是默认绑定规则,this 指向全局对象。
注意:
let变量声明不会绑定在window上面,只有var声明的才会,这是需要注意的。除此之外,严格模式下上段代码的 this 是 undefined,比如下面这段代码:
var name = 'fly63前端网';
function foo(){
'use strict'
console.log(this.name)
}
foo(); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')
从上段代码可以看出,默认绑定规则下,this 绑定到了全局对象,当然这与函数调用位置有关。但是严格模式下,this 的绑定与函数调用位置无关。
前面的默认绑定规则很好理解,因为我们的函数执行上下文就是全局作用域,this 自然而然绑定到了全局对象上。
独立函数的调用我们可以直接看出执行上下文在哪里,但如果不是独立函数调用,比如下面代码。
代码如下:
function foo() {
console.log(this.name) // fly63前端网
}
let obj = {
name: 'fly63前端网',
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.foo 中 this 就绑定到 obj,但是有一些情况比较特殊,即使采用的隐式绑定规则,但是 this 并没有按照我们的想法去绑定,这就是所谓的隐式绑定 this 丢失,常见于回调函数中。
代码如下:
function foo() {
console.log(this.name) // fly63前端网
}
function doFoo(fn) {
fn(); // 函数调用位置
}
let obj = {
name: 'fly63',
foo: foo
}
let name = 'fly63前端网';
doFoo(obj.foo); // fly63前端网
上段代码中我们很容易会以为 foo 绑定的 this 是 obj 对象,因为我们使用了 obj.foo 的方式,这种方式就是遵循隐式绑定规则。但是事实上 this 却绑定到了全局对象上去,这是因为我们在 doFoo 函数中调用 fn 时,这里才是函数的实际调用位置,此时是独立函数调用,所以 this 指向了全局对象。
实际项目中我们容易遇到这种问题的场景可能就是定时器了,比如下面的代码:
setTimeout(obj.foo, 100)
这种写法就很容易造成 this 丢失。
前面我们已经说了默认绑定和隐式绑定,其中隐式绑定我们通常是以 obj.foo 这种形式来调用函数的,目的就是为了让 foo 的 this 绑定到 obj 对象上。
这个时候,如果我们不想通过 obj.foo 的形式调用函数,我们想要很明确的将函数的 this 绑定在某个对象上。那么可以使用 call、apply 等方法,这就是所谓的显式绑定规则。
代码如下:
function foo() {
console.log(this.name) // fly63前端网
}
let obj = {
name: 'fly63前端网',
}
foo.call(obj);
上段代码我们利用 call 方法直接将 foo 函数内部的 this 指向了 obj 对象,这就是显式绑定。
虽然显式绑定让我们很清楚的知道了函数中的 this 绑定到了哪个对象上,但是它还是无法结局我们 this 绑定丢失的问题,就比如下面这种写法:
function foo() {
console.log(this.name) // fly63前端网
}
function doFoo(fn) {
fn(); // 函数调用位置
}
let obj = {
name: 'fly63',
foo: foo
}
let name = 'fly63前端网';
doFoo.call(obj, obj.foo); // fly63前端网
上段代码我们虽然使用 call 来更改 this 绑定,但是最终结果却是没有用的。
虽然显式绑定本身不能解决 this 绑定丢失的问题,但是我们可以通过变通的方式来解决这个问题,也被称作硬绑定。
硬绑定:
function foo() {
console.log(this.name) // fly63前端网
}
function doFoo(fn) {
fn(); // 函数调用位置
}
let obj = {
name: 'fly63',
}
let bar = function () {
foo.call(obj)
}
let name = 'fly63前端网';
doFoo(bar); // fly63
setTimeout(bar, 100); // fly63
其实思路也比较简单,出现 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)
new 关键词相信大家都知道或者使用过吧,这就是我们将要将的第 4 种 this 绑定,叫做 new 绑定。
想要知道 new 绑定规则,我们就很有必要知道一个当我们 new 一个对象的时候做了什么,或者说 new 关键词会做哪些操作。我们这里简单总结一下,具体的 new 的过程还需要大家自行下来好好学学。
使用 new 来调用函数时,会执行下面操作:
我们可以看到 new 的操作中就有 this 的绑定,我们在来看看代码。
代码如下:
function foo(name) {
this.name = name;
}
let bar = new foo('fly63前端网');
console.log(bar.name); // fly63前端网
上段代码我们使用 new 关键词调用了 foo 函数,大家注意这不是默认调用规则,这是 new 绑定规则。
前面我们总结了 4 条 this 绑定的规则,在大多数情况下我们只需要找到函数的调用位置,然后再判断采用哪条 this 绑定规则,最终确定 this 绑定。
我们这里可以先简单总结一下 4 条规则以及 this 绑定确定流程。
this 绑定确定流程:
先确定函数调用位置,然后确定使用哪条规则,然后根据规则确定 this 绑定。
this 绑定规则:
显式绑定:通过 call、apply 指定 this 绑定到哪里
可以看到,我们确认 this 绑定的时候有 4 条规则,在通常情况下,我们可以根据这 4 条规则来判断出 this 的绑定。但是有时候某个函数的调用位置对应了多个绑定规则,这个时候我们该选用哪一条规则来确定 this 绑定呢?这个时候就需要明确每一条绑定规则的优先级了!
首先我们要明确的式默认绑定规则的优先级是最低的,所以我们考虑的时候暂时不考虑默认绑定规则。
如果函数调用的时候出现了隐式绑定和显式绑定,那么具体采用哪一个规则,我们通过代码来实验一下。
代码如下:
function foo(){
console.log(this.name);
}
let obj1 = {
name: 'fly63前端网',
foo: foo
}
let obj2 = {
name: '李四',
foo: foo
}
obj1.foo(); // fly63前端网
obj2.foo(); // 李四
obj1.foo.call(obj2); // 李四
obj2.foo.call(obj1); // fly63前端网
上段代码中我们涉及到了两种 this 绑定,obj.foo 为隐式绑定,this 绑定给 obj 对象,而 foo.call(obj)为显示绑定,this 绑定给 obj 对象。
从上段代码看出,当两个绑定规则都存在的时候,我们采用的是显式绑定规则。
总结:
显式绑定 > 隐式绑定
接下来我们看看 new 绑定与隐式绑定的优先级。
代码如下:
function foo(name) {
this.name = name;
}
let obj1 = {
foo: foo
}
obj1.foo('fly63前端网');
let bar = new obj1.foo("fly63");
console.log(obj1.name); // fly63前端网
console.log(bar.name); // fly63
上段代码中在在使用 new 关键词的时候又使用了 obj1.foo 隐式绑定,但是最终结果 this 并没有绑定到 obj1 对象上,所以隐式绑定优先级低于 new 绑定。
总结:
隐式绑定 < new 绑定
接下来我们比较显式绑定与 new 绑定规则,但是我们 new 绑定与显式绑定的 call、apply 方法不能一起使用,所以我们无法通过 new foo.call(obj)来进行测试。
但是我们前面讲解显式绑定的时候,提到一种绑定叫做硬绑定,它也是显式绑定中的一种,所以说我们可以利用硬绑定与 new 绑定来进行比较。
代码如下:
function foo(name) {
this.name = name;
}
let obj1 = {};
let bar = foo.bind(obj1);
bar('fly63前端网');
console.log(obj1.name); // fly63前端网
let baz = new bar('fly63');
console.log(obj1.name); // fly63前端网
console.log(baz.name); // fly63
上段代码中我们把 obj 硬绑定到了 bar 上,然后我们通过 new 绑定调用了函数,但是 obj1.name 还是fly63前端网,并没有改为fly63,但是,我们的 new 操作修改了硬绑定(到 obj1 的)调用 bar 中的 this。因为使用了 new 绑定,我们得到了新的 baz 对象,并且 baz.name 为fly63。
总结:
new 绑定 > 显式绑定,需要注意的是,new 操作时将 this 绑定到了新创建的对象。
到这儿,我们基本上能够确定一个函数内部的 this 指向哪儿了,我们这里做出一些总结,以供在项目实践中判断 this 绑定。
this 绑定规则优先级:
默认绑定 < 隐式绑定 < 显式绑定 < new 绑定
判断 this 最终指向,总体流程:
结合上面的绑定优先级以及判断流程,我们在一般的项目中以及能够判断出 this 指向哪儿了!
this 绑定虽然是一个比较难的知识点,但是我们作为一个前端开发者,必须要学会如何理解和使用它,因为它确实能给我们带来很多的便利和好处。
当然,本篇文章只讲解了最常规的 this 绑定及原理,除此之外,this 绑定还有一些意外的情况,这里不做更多解释,感兴趣的小伙伴可以自行下来查询资料,比如说软绑定、间接引用等等。
总之,常规判断如下:
JavaScript中有很多令人困惑的地方,或者叫做机制。但是,就是这些东西让JavaScript显得那么美好而与众不同。比方说函数也是对象、闭包、原型链继承等等,而这其中就包括颇让人费解的this机制。
this的绑定过程之前的调用栈 和 调用位置,this绑定规则:1、默认模式,2、隐式绑定,3、显式绑定,4、new绑定
在 JavaScript 中,this 这个特殊的变量是相对比较复杂的,因为 this 不仅仅用在面向对象环境中,在其他任何地方也是可用的。 本篇博文中会解释 this 是如何工作的以及使用中可能导致问题的地方,最后奉上最佳实践。
this关键字在js中的指向问题不管是工作还是面试中都会经常遇到,所以在此对它进行一下总结:全局作用域中、闭包中指window、函数调用模式:谁调用就指谁、构造函数中,this指实例对象、apply/call改变this的指向、bind改变this指向等
如果这很难明白,为什么我们不停止使用它呢?认真的思考一下?如果你读过 将90%的垃圾扔进垃圾桶后,我如何重新发现对JavaScript的爱, 当我说扔掉它时,你不会感到惊讶,this被丢弃了
this的用法:直接在函数中使用 谁调用这个函数this就指向谁 ,对象中使用, 一般情况下指向该对象 ,在构造函数中使用
this的四种绑定策略:默认绑定、隐式绑定、显示绑定、new绑定。首先要明确的是一般情况下,this不是函数被定义时绑定,而是函数被调用时被绑定 ,那么函数中的this有四种绑定方式:
this是Javascript语言的一个关键字。它代表函数运行时,自动生成的一个内部对象.this永远指向函数的调用者。随着函数使用场合的不同,this的值会发生变化。但是有一个总的原则,那就是this指的是,调用函数的那个对象。
apply和call的区别就是传的参数形式不一样。call是一个一个的传,apply可以将参数以数组的形式传进去。而bind是传入第二个和后面的参数,且绑定this,返回一个转化后的函数。
以函数形式调用,this指向window;以方法形式调用,this指向调用方法的那个对象;构造函数调用,this指向实例的对象;使用window对象的方法使,指向window;多重场景改变this指向
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!