迷失中的this指向,看完这篇就会了

发表于 2年以前  | 总阅读数:316 次

this是一个比较迷惑人的东西,尽管你对this有很多的了解,但是面试题里面考察this指向,总会让你有种猜谜的感觉,知道一些,但是还是会出错,或许你猜对了,但是又好像解释不太清楚。

嗯,不只你一个人这样,很多人都是这样,包括我自己,本质上就是面试埋下的坑,让你跳进去,你想跳过去,那还是不太容易,真正对知识的理解与应用,绝不只是停留在概念与理念,也不是为了完成一道面试题,答不对也没关系,如果面试官给你耐心解释了这道题,那也是一次不错的学习机会。

正文开始...

在阅读本文之前,主要会从以下几点对this的思考

  • this 是什么时候产生的
  • 迷惑的this在函数中的指向问题
  • 箭头函数this
  • 常用改变this的指向方案

this是什么

  • 全局this

为了了解this,我们先看下this,新建一个index.html1.js

console.log(this, Object.getPrototypeOf(this));

index.html


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>this</title>
</head>
<body>
    <div id="app"></div>
    <script src="./1.js"></script>
</body>
</html>

当我们在浏览器打开时,我们会发现this是一个window对象

如果我们在终端直接运行1.js


{} [Object: null prototype] {}

node环境下,全局的this居然是一个{}对象

  • 严格模式下函数内部的this

现在我们在js的最顶部使用use strict采用严格模式。

我们在函数内部写一个this


"use strict"
console.log(this, Object.getPrototypeOf(this));
var publicName = "Maic";
function hello() {
    console.log(this) // undefined
    console.log(this.publicName) // undefined
}
hello();

严格模式下函数内部会是undefined,并且访问publicName会直接报错

为啥use strict严格模式下全局this无法访问

于是查找资料寻得,严格模式主要有以下特征

  • 未提前申明的变量不能使用,会报错
  • 不能用delete删除对象的属性
  • 定义的变量名不能重复申明
  • 函数内部的this不再指向全局对象

还有其他的更多的参考js-script[1]

this的指向

在这之前我们很基础的了解到在非严格模式下this指向的是window或者{}对象,在普通函数中this的指向是window全局对象

而你通常会看到this的指向并不都是指向全局对象,而是动态变化的,正因为它会变化,所以令人十分费脑壳

  • 非严格模式普通函数this指向

function hello() {
    console.log(this) // window
    // console.log(this.publicName);
}
hello();

在普通函数内部this指向的是window对象

  • 构造函数的this指向

...
function Person() {
    this.age = 10;
    this.name = 'Web技术学苑';
    console.log(this, '111')
}
const person = new Person();
console.log(person, '222'); // Person { age: 10, name: 'Web技术学苑' }

至此你会发现,构造函数内部的this居然就是实例化的那个对象person

  • 对象定义的内部函数

const userInfo = {
    publicName: 'Jack',
    getName: function () {
        console.log(this.name, '--useInfo') // Jack
    }
}
userInfo.getName();

不出意外打印都知道肯定publicName肯定是Jack,内部的this也是指向userInfo

箭头函数的this

但是如果改成下面这种呢


var publicName = "Maic";
const userInfo = {
    publicName: 'Jack',
    getName: () => {
        console.log(this.publicName, '---useInfo')
    }
}
userInfo.getName();

这是一个很迷惑的问题,箭头函数不是没有自己的this吗,而且这里是userInfo.getName()这不是一个隐式调用吗?应也是userInfo这个对象才对,但是并不是,当改成箭头函数后,内部的this居然变成了全局的window对象了

我们看下babel对上面一段代码编译成es5的代码

es6代码

var publicName = 'Maic';
const userInfo = {
    publicName: 'Jack',
    getName: () => {
        console.log(this.publicName, '---useInfo')
    }
}
userInfo.getName();

编译后的代码,大概就是下面这样的了

var _this = this;
var publicName = "Maic";
var userInfo = {
  publicName: "Jack",
  getName: function getName() {
    console.log(_this.publicName, "---useInfo");
  }
};
userInfo.getName();

其实箭头函数是非常迷惑人的,而且外面是一个被调用的是一个对象,所以时常会给人一种幻觉,我们常听到一句this指向的是被调用的那个对象,那么这里箭头函数this指向的是window,而const定义的变量会被转换成var

那怎么能让getName指向的是本身自己的useInfo

var publicName = 'Maic';
const userInfo = {
    publicName: 'Jack',
    getName: function(){
        console.log(this.publicName, '---useInfo') // Jack
    }
}
userInfo.getName();

你看当我把箭头函数改成普通函数,这个普通函数内部的this就指向userInfo

this指向被调用的那个对象貌似这句话后又在此时好像又是正确的

我们接下来看下下面一种情况


var publicName = 'Maic';
const userInfo = {
    publicName: 'Jack',
    getName: function(){
        console.log(this.publicName, '---useInfo') // Jack
    }
}
var user = userInfo.getName;
user();

那么此时getName内部的this又是谁呢?

此时你会发现打印的是Maic此时会发现this指向的是window,也就是说指向的那个被调用者,那被调用者是谁?

上面那段代码同等于下面,你仔细看


var publicName = 'Maic'; // var 定义,实际上等同于window.publicName = publicName
function getName () {
console.log(this.publicName, '---useInfo') // Jack
}
const userInfo = {
    publicName: 'Jack',
    getName
}
// var user = userInfo.getName;
// or 等价于
// window.user = userInfo.getName;
// or 进一步等价
window.user = function getName () {
  console.log(this.publicName, '---useInfo') // Jack
}
// user();
// or 等价于
window.user();

所以你现在是不是很清晰明白this指向的也是被调用的那个对象window

但是有一点必须申明,必须在非严格模式下,此时的this才会指向window

迷失中的this指向

在这之前我们了解到非严格模式下

  • 普通函数内部的this指向的是window对象
  • 构造函数内的this指向的是实例化的那个对象
  • 普通申明的对象,如果调用的方法是箭头函数,那么内部this指向的是全局对象,如果不是那么指向的是被调用本身的那个对象

我们再来看下那些面试题中很迷惑的this


var user = {
    name: 'Maic',
    a: {
        name: 'Tom',
        b: function () {
            console.log(this.name)
        }
    }
}
console.log(user.a.b()) // Tom

没错,你看到的这个打印是Tom,这里直接调用的是b这个方法,被调用的是user.a这个对象,所以在b这个方法内部的this指向了a对象

如果是箭头函数呢


var name = "Maic";
...
var user = {
    name: 'Jack',
    a: {
        name: 'Tom',
        b: () => {
            console.log(this.name)
        }
    }
}
console.log(user.a.b()) // Maic

我们会发现通过babel转换后会是这样的

var _this = this;
var user = {
  name: "Jack",
  a: {
    name: "Tom",
    b: function b() {
      console.log(_this.name);
    }
  }
};

所以依然箭头函数内部依然是个全局对象window

我们接下来看一道真实的面试题

var obj = {
    a: 1,
    b: function () {
        console.log(this.a)
    },
    c: () => {
      console.log(this.a)
    }
}
var a = 2;
var objb = obj.b;
var objc = {
  a: 3
}
objc.b = obj.b;
const t = objc.b;
obj.b(); // 1
obj.c(); // 2
objb(); // 2
objc.b(); // 3
obj.b.call(null); // 2
obj.b.call(objc); // 3
t() // 2

我想信绝大大部分第一个obj.b()肯定是可以正确答出来,但是后面的貌似有些迷惑人,时常会让你掉进坑里

我们先看结论打印的依次肯定是


1
2
2
3
2
3
2

obj.b()的调用实际上在之前例子已经有讲,b方法是一个普通方法,内部this指向的就是被调用的obj对象,所以此时内部访问的a属性就是对象obj

var objb = obj.b,当我们看到这样的代码时,其实这段代码可以拆分以下

function b() {
  console.log(this.b)
}
window.objb = b;

本质上就是将对象obj的一个方法b赋值给了window.objb的一个属性

所以objb()的调用也是window.objb()objb方法内部this自然指向的就是window对象,而我们用var a = 2这个默认会绑定在window对象上

obj.c(),因为c是一个箭头函数,所以内部的this就是指向的全局对象

obj.b.call(null)这个null是非常迷惑人,通常来说call不是改变函数内部this的指向吗,但是这里,如果call(null)实际上会默认指向window对象

objc.b()这打印的是3,其实与objb的赋值有异曲同工之笔

...
var objc = {
  a: 3
}
objc.b = obj.b;

本质上就在objc动态的新增了一个属性b,而这个属性b赋值了一个方法,也就是下面这样

objc.b = function() {
  console.log(this.a)
}
objc.b() // 3

如果是const t = objc.b,至此你会发现,当我们执行t()时,此时打印的却是2那是因为const t定义的变量会编译成var从而t变量变成一个全局的window对象下的属性,本质上等价下面

...
// const t = objc.b
var a = 2;
/* 
等价于下面
var t = function() {
  console.log(this.a)
}
*/
// 本质上就是
window.t = function() {
    console.log(this.a)
}
  • 多层对象嵌套下的this
var nobj = {
    name: '1',
    a: {
        name: '2',
        b: {
            name: '3',
            c: function () {
                console.log(this.name)
            }
        }
    }
}
console.log(nobj.a.b.c()); //3

以上的结果是3,实际上我们从之前案例中明白,非严格模式下this指向被调用那个对象

所以你可以把上面那段代码看成下面这样


...
console.log((nobj.a.b).c()); //3
//or 相当于
/*
*
  var n = nobj.a.b;
  n.c()
*/

改变this对象的指向

这个相信很多小伙伴已经耳熟能祥了,call,apply,bind,能手撕call,apply,bind的文章已经不计其数

这里就只讲解如何使用,以及他们在业务中的一些具体使用场景

  • call

用一段伪代码举证以下

// index.vue
import configOption from './config'
export default {
  name: 'index',
  computed: {
   optionsBtnGroup() {
     return configOption.call(this)
   }
  },
  methods: {
    handleEdit(id) {
      console.log(id)
    },
    handleDelete(id) {
      console.log(id)
    }
  }
}

对应的template可能就是下面这样几个按钮


<div>
  <a href="javascript:void(0)" v-for="(item, index) in optionsBtnGroup" :key="index" @click="item.handle(item.id)">{{item.text}}</a>
</div>

我们再来看下config.js


export default () => {
  const options = [
    {
      text: '编辑',
      id: 123,
      handle: (id) => {
        this.handleEdit(id)
      }
    },
    {
      text: '删除',
      id: 234,
      handle: (id) => {
        this.handleDelete(id)
      }
    }
  ]
}

正因为在计算属性中用了call所以在config.js中才能访问外部methods的方法,有些人看到这样的代码肯定会说,两个按钮这么搞配置,代码反而多了这么多,还不如模版上放两个按钮完事

是的,确实是,当我们为了使用call而使用反而增加了业务代码的维护成本,正常情况还是建议不要写出上面那段坏代码的味道,我们只要明白在什么时候可以用,什么可以不用就行,不要为了使用而使用,反而本末倒置。

但是有时候如果业务复杂,你想隔离业务的耦合,达到通用,call能帮你减少不少代码量

  • apply

apply也是可以改变this对象

const userInfo = {
    publicName: 'Jack',
    getName: () => {
        console.log(this.publicName, '---useInfo')
    }
}
function test(...args) {
   console.log(args); // ['hello', 'world']
   console.log(this.publicName);
}

test.apply(userInfo, ['hello', 'world'])

apply会立即执行该函数,如果传入的首个参数是null或者undefined,那么此时内部this指向的是window

另外还有一个方法可以让函数立即执行,也能改变当前函数this指向

...
var publicName = 'Maic';
function test(...args) {
   console.log(args);
   console.log(this.publicName);
}
Reflect.apply(test, {publicName: 'aaa'}, [1,2,3]) // aaa [1,2,3]
Reflect.apply(test, window, ['a', 'b', 'c']) // Maic ['a', 'b', 'c']
  • bind

这也是可以改变this指向,不过会返回一个新函数,我们常常在react中发现这样用bind显示绑定方案。

我们写个简单的例子,尝试改变页面背景,切换body肤色

document.body.addEventListener('click', function () {
    console.log(this) // body
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
})

可以切换背景肤色

以上貌似没有问题,但是你可能会写这样的代码


document.body.addEventListener('click',  () => {
    console.log(this)
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
})

此时内部的this一定指向的window,而且内部访问style报错

于是你会改成这样


const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}
document.body.addEventListener('click', fn)

是的,这样是可以的,本质上就是一个fn的形参,内部this指向仍然是document.body

于是为了借助bind,你可以这么做


const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}.bind(body)
body.addEventListener('click', fn)

这么做也是ok的

不知道你有没有疑问,为什不像下面这么做呢?


const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}

body.addEventListener('click', fn.bind(this))

如果你仔细看下,其实fn内部this指向是window,所以这是一个常会犯的错误。

还有为啥不是像下面这样

const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}

body.addEventListener('click', fn.bind(body))

以上功能没有任何问题,但是我们每次点击都会调用bind,从而返回一个新的函数,所以这种方式虽然效果一样,但是性能远不如第一种,为了更好理解,你可以写成下面这样

const body = document.body;
const fn = function () {
    if (this.style.backgroundColor === 'red') {
        this.style.backgroundColor = 'green'
    } else {
        this.style.backgroundColor = 'red';
    }
}
const callback = fn.bind(body)
body.addEventListener('click', callback)

总结

  • 了解this怎么产生的,通常情况this在非严格模式下,指向的是全局window对象,在严格模式下,普通函数内的this不是全局对象
  • 迷惑的this指向问题,正常情况this指向的是被调用的那个对象,但是如果是箭头函数,那么指向的是全局对象window
  • bind,call, apply改变this指向
  • code example[2]
  • 推荐一篇关于阮一峰老师this[3]的博文

参考资料

[1]js-script: https://www.runoob.com/js/js-strict.html

[2]code example: https://github.com/maicFir/lessonNote/tree/master/javascript/05-this

[3]this: https://wangdoc.com/javascript/oop/this.html

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/mnMf2jl7I79TEtGDS6gMuQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237268次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录