前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >this到底指向啥?看完这篇就知道了!

this到底指向啥?看完这篇就知道了!

作者头像
蒋鹏飞
发布于 2020-10-15 01:59:51
发布于 2020-10-15 01:59:51
44000
代码可运行
举报
文章被收录于专栏:进击的大前端进击的大前端
运行总次数:0
代码可运行

JS中的this是一个老生常谈的问题了,因为它并不是一个确定的值,在不同情况下有不同的指向,所以也经常使人困惑。本篇文章会谈谈我自己对this的理解。

this到底是啥

其实this就是一个指针,它指示的就是当前的一个执行环境,可以用来对当前执行环境进行一些操作。因为它指示的是执行环境,所以在定义这个变量时,其实是不知道它真正的值的,只有运行时才能确定他的值。同样一段代码,用不同的方式执行,他的this指向可能是不一样的。我们来看看如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function func() {
  this.name = "小小飞";

  console.log(this);    // 看一下this是啥
}
复制代码

这个方法很简单,只是给this添加了一个name属性,我们把这个方法复制到Chrome调试工具看下结果:

上图中我们直接调用了func(),发现this指向的是window,name属性添加到了window上。下面我们换一种调用方式,我们换成new func()来调用:

我们看到输出了两个func {name: "小小飞"},一个是我们new返回的对象,另一个是方法里面的console。这两个值是一样的,说明这时候方法里面this就指向了new返回的对象,而不是前面例子的window了。这是因为当你使用new去调用一个方法时,这个方法其实就作为构造函数使用了,这时候的this指向的是new出来的对象。

下面我们分别讲解下几种情况

使用new调用时,this指向new出来的对象

这个规则其实是JS面向对象的一部分,JS使用了一种很曲折的方式来支持面向对象。当你用new来执行一个函数时,这个函数就变成了一个类,new关键字会返回一个类的实例给你,这个函数会充当构造函数的角色。作为面向对象的构造函数,必须要有能够给实例初始化属性的能力,所以构造函数里面必须要有某种机制来操作生成的实例,这种机制就是this。让this指向生成的实例就可以通过this来操作实例了。关于JS的面向对象更详细的解释可以看这篇文章。

this的这种特性还有一些妙用。一个函数可以直接调用,也可以用new调用,那假如我只想使用者通过new调用有没有办法呢?下图截取自Vue源码:

Vue巧妙利用了this的特性,通过检查this是不是Vue的一个实例来检测使用者是通过new调用的还是直接调用的。

没有明确调用者时,this指向window

这个其实在最开始的例子就讲过了,那里没有明确调用者,this指向的是window。我们这里讲另外一个例子,函数里面的函数,this指向谁?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function func() {
  function func2() {
    console.log('this:', this);   // 这里的this指向谁?
  }

  func2();
}
复制代码

我们执行一下看看:

直接执行:

使用new执行:

我们发现无论是直接执行,还是使用new执行,this的值都指向的window。直接执行时很好理解,因为没有明确调用者,那this自然就是window。需要注意的是使用new时,只有被new的**func**才是构造函数,他的this指向new出来的对象,他里面的函数的this还是指向**window**。

有明确调用者时,this指向调用者

看这个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var obj = {
  myName: "小小飞",
  func: function() {
    console.log(this.myName);
  }
}

obj.func();    // 小小飞
复制代码

上述例子很好理解,因为调用者是obj,所以func里面的this就指向obj,this.myName就是obj.myName。其实这一条和上一条可以合在一起,没有明确调用者时其实隐含的调用者就是window,所以经常有人说this总是指向调用者

下面我们将这个例子稍微改一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var myName = "大飞哥";

var obj = {
  myName: "小小飞",
  func: function() {
    console.log(this.myName);
  }
}

var anotherFunc = obj.func;

anotherFunc();   // 输出是啥?
复制代码

这里的输出应该是“大飞哥”,因为虽然anotherFunc的函数体跟obj.func一样,但是他的执行环境不一样,他其实没有明确的调用者,或者说调用者是window。这里的this.myName其实是window.myName,也就是“大飞哥”。

我们将这个例子再改一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let myName = "大飞哥";

var obj = {
  myName: "小小飞",
  func: function() {
    console.log(this.myName);
  }
}

var anotherFunc = obj.func;

anotherFunc();   // 输出是啥?
复制代码

这次我们只是将第一个var改成了let,但是我们的输出却变成了undefined。这是因为let,const定义变量,即使在最外层也不会变成window的属性,只有var定义的变量才会成为window的属性。

箭头函数并不会绑定this

这句话的意思是箭头函数本身并不具有this,箭头函数在被申明确定this,这时候他会直接将当前作用域的this作为自己的this。还是之前的例子我们将函数改为箭头函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var myName = "大飞哥";

var obj = {
  myName: "小小飞",
  func: () => {
    console.log(this.myName);
  }
}

var anotherFunc = obj.func;

obj.func();      // 大飞哥
anotherFunc();   // 大飞哥

上述代码里面的obj.func()输出也是“大飞哥”,是因为obj在创建时申明了箭头函数,这时候箭头函数会去寻找当前作用域,因为obj是一个对象,并不是作用域,所以这里的作用域是window,this也就是window了。

再来看一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var myName = "大飞哥";

var obj = {
  myName: "小小飞",
  func: function () {
    return {
      getName: () => {
        console.log(this.myName);
      }
    }
  }
}

var anotherFunc = obj.func().getName;

obj.func().getName();      // 小小飞
anotherFunc();   // 小小飞
复制代码

两个输出都是“小小飞”,obj.func().getName()输出“小小飞”很好理解,这里箭头函数是在obj.func()的返回值里申明的,这时他的this其实就是func()的this,因为他是被obj调用的,所以this指向obj。

那为什么anotherFunc()输出也是“小小飞”呢?这是因为anotherFunc()输出的this,其实在anotherFunc赋值时就确定了:

  1. var anotherFunc = obj.func().getName;其实是先执行了obj.func()
  2. 执行obj.func()的时候getName箭头函数被申明
  3. 这时候箭头函数的this应该是当前作用域的this,也就是func()里面的this
  4. func()因为是被obj调用,所以this指向obj
  5. 调用anotherFunc时,其实this早就确定了,也就是obj,最终输出的是obj.myName

再来看一个构造函数里面的箭头函数,前面我们说了构造函数里面的函数,直接调用时,他的this指向window,但是如果这个函数时箭头函数呢:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var myName = "大飞哥";

function func() {
  this.myName = "小小飞";

  const getName = () => {
    console.log(this.myName);
  }

  getName();
}

new func(); // 输出啥?
复制代码

这里输出的是“小小飞”,原理还是一样的,箭头函数在申明时this确定为当前作用域的this,在这里就是func的作用域,跟func的this一样指向new出来的实例。如果不用new,而是直接调用,这里的this就指向window。

DOM事件回调里面,this指向绑定事件的对象

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function func(e) {
  console.log(this === e.currentTarget);   // 总是true
  console.log(this === e.target);          // 如果target等于currentTarget,这个就为true
}

const ele = document.getElementById('test');

ele.addEventListener('click', func);

currentTarget指的是绑定事件的DOM对象,target指的是触发事件的对象。DOM事件回调里面this总是指向currentTarget,如果触发事件的对象刚好是绑定事件的对象,即target === currentTarget,this也会顺便指向target。如果回调是箭头函数,this是箭头函数申明时作用域的this。

严格模式下this是undefined

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function func() {
  "use strict"
  console.log(this);
}

func();   // 输出是undefined
复制代码

注意这里说的严格模式下this是undefined是指在函数体内部,如果本身就在全局作用域,this还是指向window。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  ...
  
    <span class="hljs-string">"use strict"</span>
    console.log(this);     // window
  
  ...

this能改吗

this是能改的,callapply都可以修改this,ES6里面还新增了一个bind函数。

使用call和apply修改this

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const obj = {
  myName: "大飞哥",
  func: function(age, gender) {
    console.log(`我的名字是${this.myName}, 我的年龄是${age},我是一个${gender}`);
  }
}

const obj2 = {
  myName: "小小飞"
}

obj.func.call(obj2, 18, "帅哥");  // 我的名字是小小飞, 我的年龄是18,我是一个帅哥

注意上面输出的名字是"小小飞",也就是obj2.myName。正常直接调用obj.func()输出的名字应该是obj.myName,也就是"大飞哥"。但是如果你使用call来调用,call的第一个参数就是手动指定的this。我们将它指定为obj2,那在函数里面的this.myName其实就是obj2.myName了。

apply方法跟call方法作用差不多,只是后面的函数参数形式不同,使用apply调用应该这样写,函数参数应该放到一个数组或者类数组里面:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
obj.func.apply(obj2, [18, "帅哥"]);   // 我的名字是小小飞, 我的年龄是18,我是一个帅哥

之所以有call和apply两个方法实现了差不多的功能,是为了让大家使用方便,如果你拿到的参数是一个一个的,那就使用call吧,但是有时候拿到的参数是arguments,这是函数的一个内置变量,是一个类数组结构,表示当前函数的所有参数,那就可以直接用apply,而不用将它展开了。

使用bind修改this

bind是ES5引入的一个方法,也可以修改this,但是调用它并不会立即执行方法本身,而是会返回一个修改了this的新方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const obj = {
  myName: "大飞哥",
  func: function(age, gender) {
    console.log(`我的名字是${this.myName}, 我的年龄是${age},我是一个${gender}`);
  }
}

const obj2 = {
  myName: "小小飞"
}

const func2 = obj.func.bind(obj2);   // 返回一个this改为obj2的新方法
func2(18, "帅哥");    // 我的名字是小小飞, 我的年龄是18,我是一个帅哥

bind和call,apply最大的区别就是call,apply会立即执行方法,而bind并不会立即执行,而是会返回一个新方法供后面使用。

bind函数也可以接收多个参数,第二个及以后的参数会作为新函数的参数传递进去,比如前面的bind也可以这样写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const func3 = obj.func.bind(obj2, 18);   // 注意我们这里已经传了一个年龄参数
func3("帅哥");    //注意这里只传了性别参数,年龄参数已经在func3里面了,输出还是:我的名字是小小飞, 我的年龄是18,我是一个帅哥

自己写一个call

知道了call的作用,我们自己来写一个call:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Function.prototype.myCall = function(...args) {
  // 参数检查
  if(typeof this !== "function") {
    throw new Error('Must call with a function');
  }

  const realThis = args[0] || window;
  const realArgs = args.slice(1);
  const funcSymbol = Symbol('func');
  realThis[funcSymbol] = this;   // 这里的this是原方法,保存到传入的第一个参数上

  //用传入的参数来调方法,方法里面的this就是传入的参数了
  const res = realThis[funcSymbol](...realArgs); 

  delete realThis[funcSymbol];  // 最后删掉临时存储的原方法

  return res;  // 将执行的返回值返回
}

自己写一个apply

apply方法跟call方法很像,区别只是在取调用参数上:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Function.prototype.myApply = function(...args) {
  if(typeof this !== "function") {
    throw new Error('Must call with a function');
  }

  const realThis = args[0] || window;
  // 直接取第二个参数,是一个数组
  const realArgs = args[1];        
  const funcSymbol = Symbol('func');
  realThis[funcSymbol] = this;   

  const res = realThis[funcSymbol](...realArgs); 

  delete realThis[funcSymbol]; 

  return res; 
}

自己写一个bind

自己写一个bind需要用到前面的apply,注意他的返回值是一个方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Function.prototype.myBind = function(...args) {
  if(typeof this !== "function") {
    throw new Error('Must call with a function');
  }

  const _func = this;    // 原方法
  const realThis = args[0] || window;   // 绑定的this
  const otherArgs = args.slice(1);    // 取出后面的参数作为新函数的默认参数

  return function(...args2) {   // 返回一个方法
    return _func.apply(realThis, [...otherArgs,...args2]);  // 拼接存储参数和新参数,然后用apply执行
  }
}

总结

  1. 函数外面的this,即全局作用域的this指向window。
  2. 函数里面的this总是指向直接调用者。如果没有直接调用者,隐含的调用者是window。
  3. 使用new调用一个函数,这个函数即为构造函数。构造函数里面的this是和实例对象沟通的桥梁,他指向实例对象。
  4. 箭头函数里面的this在它申明时确定,跟他当前作用域的this一样。
  5. DOM事件回调里面,this指向绑定事件的对象(currentTarget),而不是触发事件的对象(target)。当然这两个可以是一样的。如果回调是箭头函数,请参考上一条,this是它申明时作用域的this。
  6. 严格模式下,函数里面的this指向undefined,函数外面(全局作用域)的this还是指向window。
  7. call和apply可以改变this,这两个方法会立即执行原方法,他们的区别是参数形式不一样。
  8. bind也可以修改this,但是他不会立即执行,而是返回一个修改了this的函数。

原创不易,每篇文章都耗费了作者大量的时间和心血,如果本文对你有帮助,请点赞支持作者,也让更多人看到本文~~

更多文章请看我的掘金文章汇总

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
[剑指offer题解][Java]1到n整数中1出现的次数
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
蛮三刀酱
2019/09/10
7010
[剑指offer题解][Java]最小的k个数
有多高,以我目前不多的面试来看,在所有遇到的面试算法题中,出现原题的概率大概能有6成,如果把基于原题的变种题目算上,那么这个出现概率能到达9成,10题中9题见过。
Rude3Knife的公众号
2019/08/06
3960
剑指offer | 面试题34:1~n 整数中 1 出现的次数
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
千羽
2021/12/29
2730
剑指offer | 面试题34:1~n 整数中 1 出现的次数
牛客网-剑指offer-10
主要是想为什么会有最大的和,一个情况是,新加上的数比原来的数都要大,就要开始考虑需不需要原来的数了。所以我们需要两个数,一个保存最大的和,用来返回,一个 保存当前的和,可以在适当的时候丢掉。 另一种情况,加入的数都比原来的小,即都是负数的时候,可能最大和只是一个最小的数;另外,当都是正数的时候也比较好解决。 代码如下:
小二三不乌
2018/08/07
4780
剑指Offer题解 - Day70
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
chuckQu
2022/08/19
1330
剑指Offer - 面试题43. 1~n整数中1出现的次数(找规律+公式)
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
Michael阿明
2020/07/13
3610
[剑指offer题解][Java]数组中出现次数超过一半的数字
最近看了好多数据结构文章,但是数据结构拾遗系列迟迟憋不出,主要原因是很多数据结构其实非常偏门,不仅日常很难遇到,学起来还涉及很多数学模型,很难有快速的理解方法。
蛮三刀酱
2019/04/29
9490
[剑指offer题解][Java]数组中出现次数超过一半的数字
剑指Offer题解 - Day67
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
chuckQu
2022/08/19
2730
[菜鸟SpringCloud入门]第四章:远程调用服务实战
欢迎来到菜鸟SpringCloud入门实战系列(SpringCloudForNoob),该系列通过层层递进的实战视角,来一步步学习和理解SpringCloud。
Rude3Knife的公众号
2019/08/07
6190
[菜鸟SpringCloud入门]第四章:远程调用服务实战
剑指Offer题解 - Day50
在一个数组nums中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
chuckQu
2022/08/19
1530
【剑指offer题解】二维数组中的查找
如果你和我一样是个算法菜鸡,那么最推荐的是先把剑指offer的题目搞明白,其次再去刷LeetCode等习题,这样对于面试突击非常有用,因为面试官最常考的算法题都在这本书里。
蛮三刀酱
2019/12/03
3650
python-剑指offer21-40
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
西西嘛呦
2020/08/26
5280
剑指offer 43——1~n整数中1出现的次数
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
健程之道
2020/06/16
3450
[剑指offer] 整数中1出现的次数(从1到n整数中1出现的次数)
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
尾尾部落
2018/09/04
1K0
统计0到n之间1的个数[数学,动态规划dp](经典,详解)
问题描述 给定一个十进制整数N,求出从1到N的所有整数中出现”1”的个数。 例如:N=2时 1,2出现了1个 “1” 。 N=12时 1,2,3,4,5,6,7,8,9,10,11,12。出现了5个“1”。 方法一 暴力求解 最直接的方法就是从1开始遍历到N,将其中每一个数中含有“1”的个数加起来,就得到了问题的解。 下面给出代码: 1 #include <bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 int mai
Angel_Kitty
2018/04/08
1.1K0
《剑指offer》– 数组中的逆序对、最小的K个数、从1到n整数中1出现的次数、正则表达式匹配、数值的整数次方
数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。
全栈程序员站长
2021/12/23
9200
《剑指offer》– 数组中的逆序对、最小的K个数、从1到n整数中1出现的次数、正则表达式匹配、数值的整数次方
剑指Offer(三十一)-- 整数中1出现的次数
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
秦怀杂货店
2022/02/15
3540
【leetcode】43.1~n整数中1出现的次数
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
海盗船长
2020/08/27
1K0
算法题目(三)
第一步在树A中查找与根结点的值一样的结点,这实际上就是树的遍历。递归调用HasSubTree遍历二叉树A。如果发现某一结点的值和树B的头结点的值相同,则调用DoesTreeHavaTree2,做第二步判断。
Helloted
2022/06/06
2980
每天一道剑指offer-整数中1出现的次数
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
乔戈里
2019/03/06
8040
推荐阅读
相关推荐
[剑指offer题解][Java]1到n整数中1出现的次数
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文