this关键字是JavaScript函数内部的一个对象,this是一个指针,指向调用函数的对象。看似简单的定义但却由于在解析this引用过程中可能涉及到执行上下文、作用域链、闭包等复杂的机制,导致this的指向问题变得异常复杂。首先必须明白一点,任何复杂的机制都不可能轻而易举的学懂弄通,因此,本文将与大家一起耐心回顾this对象,归纳总结this的引用机制,希望对你有所帮助。

一、函数到底执行了没?

要向弄懂this对象,必须先搞懂函数是什么时候执行?
先看看简单的一个例子(例1):

function fn(){console.log('你好');
}fn;
fn();
let f = fn;
f();

上面的例子一共输出 2 次 你好
fn()f()表达式中函数被调用执行,fnlet f=fn表达式中函数并未被调用执行。

函数执行主要看是否存在函数名(),使用不带括号的函数名会访问函数指针,并非调用该函数


再看看一个加入闭包机制的例子(例2):

function fn(){let hi = '你好'return function gn(){console.log(hi);}
}fn;
fn();
let f = fn;
f();
let g = fn();
g();

上面的例子似乎较为复杂了,那一共输出多少次 你好?

  1. 输出 你好 必须是函数 gn 被调用执行,因此关键在于函数 gn 什么时候被调用?
  2. 根据前一个例子,表达式fnf=fn 没有调用函数 fn,则更不会调用函数 gn
  3. 根据前一个例子,表达式fn()f() 是相同的含义,均调用了函数fn。在闭包中,调用fn返回返回一个函数gn的函数指针,但最终并没有通过该函数指针调用gn,因此在表达式fn()f()g=fn()并没有执行函数gn
  4. 表达式g=fn(),可以将函数gn赋值给g,最后通过g()完成对函数gn的调用执行。类似于:
let hi = ‘你好’;
let g = function (){console.log(hi);}g();//函数执行

因此最终该例子仅输出一次 你好


最后看看一个对象内部的函数调用例子(例3):

let o = {hi:'好难呀',fn: function () {let hi = '你好'return function gn() {console.log(hi);}}
}o.fn;
o.fn();
let f = o.fn;
f();
let g = o.fn();
g();

这个例子中,一共输出多少次 你好?

其实无论函数放到对象内部定义还是外部定义,均可以采用前一个例子的分析步骤解析函数被调用执行的过程,因此,本例子中也仅输出一次 你好

全局环境中定义的function,则该函数自动成为window对象的方法,即全局环境下的fn()调用等价于window.fn()调用。

二、神奇的this

what,上面讲了一大堆的都还没有讲到this

别急,理解this的引用机制,我认为最关键的是理解函数 执行 的上下文。倘若连函数什么时候执行都傻傻搞不清,那理解this对象更无从谈起,来,我们开始继续探索。

红宝书第四版将this对象阐述为:

1.在标准函数中,this引用的是把函数当成方法调用的上下文对象
2.在箭头函数中,this引用的是定义箭头函数的上下文

(一)标准函数的this对象

普通函数(除箭头函数)内部中均有一个this对象。this在函数执行时确定所指对象。这里有两个关键点:执行时对象

  1. 普通函数在执行时才能确定this。那在定义时能确定码?不行!记住:函数执行时确定!函数执行时确定!函数执行时确定!
  2. 普通函数的this指向的是调用该函数的对象。那可以指向其他函数吗?可以指向原始数据类型吗?统统不行!记住:指向调用该函数的对象、指向调用该函数的对象、指向调用该函数的对象

虽然很多文章对this的引用分了情况讨论,但我依旧认为理解上述两个关键点是最重要的。来,我们通过例子进一步分析,以下将按照掘金文章:嗨,你真的懂this吗?的分类标准进行讨论。

默认绑定

默认绑定简单说就是在全局环境中执行函数,即没有任何对象直接调用该函数。这种情况下,函数的this将指向window(非严格模式)或为undefined(严格模式)

//非严格模式下,this指向window
function fn1(){console.log(this);
}
//严格模式下,this为undefined
function fn2(){'use strict'console.log(this);
}

简单吧,可是你能准确的判断出函数是在全局环境中执行的么?请看下面例子(例4):

var hi = 'window'
let o = {hi: '对象',gn: function (){let hi = '函数';console.log(this.hi);},fn: function () {let hi = '函数'return function (){let hi = '闭包函数';console.log(this.hi);};}
}o.gn();
let f = o.fn();
f();

一旦涉及对象内部方法、闭包等机制,就会导致问题变得复杂许多。你能看出一共输出了几次?有多少次输出是在全局环境中执行的呢?

按照前一章节分析,可以知道一共有两次输出(若是不理解可以回看第一节),分别为表达式o.gn()f()输出对象,window。分析如下:

  1. 表达式o.gn()显然是通过对象o对函数gn进行调用,因此gn执行时的this所指向的就是o
  2. 表达式let f = o.fn();将执行fn函数并将闭包函数的函数指针赋值给f变量,此时执行的函数是fn而并非是闭包函数,因此此刻fnthis指向对象o,但是闭包函数的this现在还没有确定;
  3. 通过调用表达式f();,让闭包函数执行,此刻闭包函数并不是某个对象调用执行,因此是运行在全局环境中,所以闭包函数的this将指向window(非严格模式)

因此,不管函数如何赋值,只要该函数并未执行,this指针就不会确定所指对象。第一个关键点就是理解函数是什么时候执行的!第二个关键点就是找到函数是如何被调用的!

隐式绑定

隐式绑定是指通过某个对象调用函数时,函数的this就指向该对象。简单说就是谁调用函数,函数就指谁。
我们看看下面这个例子,该例子出自知乎文章:JavaScript 的 this原理是什么?(例5)

const o1 = {text: 'o1',fn: function() {return this.text}
}
const o2 = {text: 'o2',fn: function() {return o1.fn()}
}
const o3 = {text: 'o3',fn: function() {var fn = o1.fnreturn fn()}
}console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())

一看感觉很复杂,但本质上还是找到哪个对象调用函数进行执行,分析一波:

  1. 执行表达式console.log(o1.fn())时,对象o1调用执行函数fn,因此,函数fnthis指向对象o1,所以输出o1
  2. 执行表达式console.log(o2.fn())时,对象o2调用执行函数fn,因此,o2内部函数fnthis指向对象o2,但且慢,这里有个表达式return o1.fn(),不难看出这又通过o1调用了o1内部函数fn(该函数this指向o1对象),并将执行结果返回。因此绕个弯还是回到执行o1内部函数fn,输出o1
  3. 执行表达式console.log(o3.fn())时,对象o3调用执行函数fn,因此,o3内部函数fnthis指向对象o3,但o3内部函数fn并没有直接用this,而是通过赋值操作获取了o1内部的fn函数,并执行fn函数。注意,这里有个坑,最后执行fn函数是没有对象调用的,因此fn函数的this指向window,这个跟例4类似,若不理解可以回头看例4。

一定要分清默认绑定和隐式绑定的场景,关键点还是在于判断出函数执行的时间,然后找出哪个对象调用了该函数。


结合回调函数再看一个例子(例6):

var hi = 'window'
let o = {hi: '对象',fn: function () {let hi = '函数';setInterval(function(){console.log(this.hi);},1000);}
}o.fn();

你觉得应该输出什么呢?我们先分析一波:

  1. 很明显,表达式o.fn();执行过程中,函数fnthis铁定是指向对象o
  2. 再看fn函数里面,执行了setInterval函数,特别是还传入了匿名函数作为回调函数,匿名函数在每一秒执行过程中并没有任何对象调用它,因此匿名函数的this指向window,最终输出window

结合传参再看一个例子(例7):

var hi = 'window'
let o = {hi: '对象',fn: function () {let hi = '函数';console.log(this);}
}function gn(fn){fn();
}gn(o.fn)

你觉得这回输出什么呢?不断的分析:

  1. 首先明确一点:参数的传入等价于赋值。因此gn(o.fn)等价于f = o.fn; gn(f);好家伙,又是赋值,没有执行函数的都是骗子!
  2. 函数gn内部执行传入的函数fn,并没有发生对象调用,因此此刻执行的环境就是全局环境,输出window

赋值、回调、闭包都是this的头号大敌,一定等确定函数真的执行了,再去找关联的对象。

显示绑定

显示绑定是指通过call、apply、bind对函数的this进行重定向,直接指定函数this所指的对象。

var hi = 'window'
let o = {hi: '对象',
}function fn() {let hi = '函数';console.log(this.hi);}fn.call(o);

通过fn函数的call方法,可以将全局环境中执行的fn函数内部this强行指向对象o,因此输出:对象
让我们思考一下,关于方法call、apply、bind之间有什么不同呢?

红宝书第四版解释如下:

  1. callapply作用是一样的,只是传入参数的形式不同,call向函数传入参数需要一个个列出来,而apply需要使用参数数组进行传入参数
  2. bind方法会创建一个新的函数实例,其this值会绑定到传给bind的对象。

值得注意的是,callapply方法在调用后会直接执行函数,bind方法则不会,但是bind方法将会一直绑定固定的this给新创建的实例。bind的具体用法如下:

var hi = 'window'
let o = {hi: '对象',
}function fn() {let hi = '函数';console.log(this.hi);}let f = fn.bind(o);f();//无论f如何调用,f内部的this始终指向对象o,输出:对象
fn();//this 依旧按照正常绑定规则进行绑定,输出:window

new绑定

new关键字会出现在使用构造函数创建特定类型对象中,且看一下红宝书对于new关键字的操作解释:

红宝书第四版将new操作步骤解释为:

  1. 在内存中创建一个新对象
  2. 这个新对象内部[[Prototype]]特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

上述操作流程已经很清楚了,看看下面的例子(例8):

    function fn() {this.name = "Tony";this.showName = function (){console.log(this.name);}}let newObj = new fn();newObj.showName();

结合红宝书的解释,我们可以知道在使用new关键字时有如下步骤:

  1. 生成一个新匿名对象
  2. 该匿名对象的[[Prototype]]特性被赋值为构造函数的prototype属性(这块涉及原型链知识)
  3. 构造函数fn的内部this指向该匿名函数
  4. 执行fn内部代码,给匿名函数添加属性name和方法showName
  5. 返回匿名函数,并赋给newObj

(二)箭头函数的this对象

相比于普通函数内部有一个this对象,箭头函数内部是没有this对象。你没有听错,箭头函数内部是没有this对象!
那该如何确定箭头函数的this引用呢?回顾JavaScript关于作用域链机制,当一个函数作用域中没有某个变量时,则将会在作用域链中的逐级往后寻找,直到找到某个变量或因找不到而报错。因此,箭头函数内部没有this对象,则在使用this对象时,必须要找到外层函数的this对象或者windowthis对象,而箭头函数对应的外层this关系是在箭头函数定义时确定的,因此无论箭头函数是在哪里调用,箭头函数所能找到的this已经在定义时就确定了。

我们通过例子来找箭头函数的this(例9):

var hi = 'window'
let o = {hi:'对象',gn:()=>{let hi = '箭头函数';console.log(this.hi);},fn:function () {let hi = '函数'return ()=>{let hi = '箭头函数';console.log(this.hi);};}
}o.gn();
let f = o.fn();
f();
f = o.fn.call(window);
f();

先寻找箭头函数的this

  1. 函数gn是箭头函数,而且外层没有其他的函数包裹,因此根据变量解析的作用域链规则,箭头函数的的this就是windowthis
  2. 函数fn是一个返回箭头函数的匿名函数,根据作用域链规则,在查找箭头函数this过程中,找到外层函数fnthis当做箭头函数的this

最后我们得出:

  1. gn箭头函数的this就是windowthis;
  2. fn返回的箭头函数的this就是函数fnthis
    运行上述例子,可以获得浏览器以下输出

简单分析一下:

  1. 第一个输出由表达式o.gn()产生,由于gn箭头函数的this就是windowthis,因此hi变量就是window;
  2. 第二个输出由表达式let f = o.fn(); f();产生,由于fn返回的箭头函数的this就是函数fnthis,通过表达式o.fn()将函数fnthis指向对象o,导致箭头函数的this也是o,最终输出对象;
  3. 第三个输出由表达式f = o.fn.call(window); f();产生,由于fn返回的箭头函数的this就是函数fnthis,通过表达式f = o.fn.call(window)将函数fnthis指向window,导致箭头函数的this也是window,最终输出window

最后请思考一个问题,可以通过call()、apply()、bind()这些方法直接改变箭头函数的this指向吗?

三、总结

this对象是JavaScript的比较复杂的知识点,我看过一些文章讨论this对象引用问题分多类阐述或者直接给出公式,混合作用域链、闭包、赋值、回调、传参等多个知识点导致理解起来过于复杂。我认为,this对象设计其实很精妙,重点要把握好函数执行时确定this的本质,再通过研究几个特殊场景下的例子,就可以较好的理解this对象的指向问题。最后你会发现普通函数和箭头函数本质上是一样,唯一的区别在于普通函数有自己的this,而箭头函数没有自己的this

由于作者水平有限,不正之处敬请指正。谢谢

参考材料:

  1. JavaScript高级程序设计(第四版)
  2. 掘金文章:嗨,你真的懂this吗?
  3. 知乎文章:JavaScript 的 this原理是什么?

this到底指向谁?相关推荐

  1. 栈顶指针到底指向哪_被称为“程序员试金石”的指针真的没有那么难!不信的话你来看看

    很多朋友放弃C语言都是因为指针,说什么指针的*号很讨厌啦.分不清址与值啦,当然了,最烦的还是链表结点,本来链表操作就让人烦了,再加上指针这个东西真是让初学的朋友苦不堪言,最后放弃了C语言转投其他语言的 ...

  2. this到底指向哪里

    this指向调用它的对象 首先要明确,this指向调用方,谁调用,this指向谁. 直接调用 举个栗子: var test = 'window' ; function testThis () {var ...

  3. 【面试题】闭包是什么?this 到底指向谁?

    一通百通,其实函数执行上下文.作用域链.闭包.this.箭头函数是相互关联的,他们的特性并不是孤立的,而是相通的.因为内部函数可以访问外层函数的变量,所以才有了闭包的现象.箭头函数内没有 this 和 ...

  4. this:它到底指向哪里!箭头函数的this又是啥?

    文章目录 JavaScritp 中的 this 总结一句话(永真给的信) 默认绑定(指向少主window) 严格模式(变若之子) 隐式绑定(苇名弦一郎) 隐式丢失(赤鬼 + 鬼庭形部雅孝) 显式绑定( ...

  5. javascript中关于this指向问题详解

      前  言 LiuDaP 在前端的学习中,我们必然要用到js,js可以说是前端必不可少的的东西.在学习js的过程中,我们会经常用到this这个东西,而this的指向问题就变得尤为重要.今天正好有空闲 ...

  6. 彻底理解js中this的指向

    首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然 ...

  7. 理解学习this指向问题

    要点 this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象. this指向的三种情况 情况1:如果一个函数中有this ...

  8. this指向总结(无栗子)

    1.有对象就指向调用对象2.没调用对象就指向全局对象:window3.用new关键字构造就指向构造出来的新对象4.严格模式下 this默认都指向underfined 除了定时器跟箭头函数5.通过app ...

  9. JS 中 this 的指向

    为什么要使用this? 解决的问题? 可以先通过一个例子了解下 function speak(){var name = this.nameconsole.log("Hello I am -- ...

最新文章

  1. java可以使用c语言中的输入,c语言中的scanf在java中应该怎么表达,Scanner类。
  2. swift golang java,解决两数之和 (Javascript, Java, C#, Swift, Kotlin, Python,C++, Golang)
  3. [jQuery] Zepto的点透问题如何解决?
  4. 17现代软件工程十五组第三次作业
  5. 专门针对苹果 M1 芯片的首款恶意软件已现身
  6. angular 新建命令
  7. 移动端测试必须具备的技能
  8. 线性筛法--------2013年1月2日
  9. 重磅!Apache Flink 1.11 功能前瞻来啦
  10. 20145303刘俊谦 Exp7 网络欺诈技术防范
  11. 第三章 机器人系统的动力学模型
  12. python输出星号等腰三角形_Python 打印矩形、直角三角形、等腰三角形、菱形
  13. U盘全新安装High sierra及打造mac,win10双系统
  14. vscode格式化代码使html标签不换行
  15. python开题报告范文样本,毕业论文开题报告怎么写
  16. 数字网络监控系统解决方案1
  17. 计算机科学终审多长时间,一些计算机科学核心期刊的投稿经验
  18. 5G应用创新发展策略研究
  19. C#Windows7任务栏开发Thumbnail Toolbars(缩略图、工具栏按钮)
  20. 2022年中国科学技术大学细胞生物III复习资料

热门文章

  1. 求和问题---每日一题
  2. css伪元素覆盖,CSS :befor :after 伪元素的巧妙用法
  3. RP Fiber Power 版本
  4. 51单片机tea5767收音机 红外遥控 自动搜台 存台 DIY
  5. c语言rand的参数,C语言中rand()函数的用法
  6. 如何在网上获取全国计算机等级考试证书电子版
  7. elementui 下拉框滚动条样式修改
  8. 什么样的产品 网站值得我们坚持下去?
  9. 降维算法——LE算法的推导
  10. Linux用户相关命令