相学java_从这两套题,重新认识JS的this、作用域、闭包、对象

日常开发中,我们经常用到this。例如用Jquery绑定事件时,this指向触发事件的DOM元素;编写Vue、React组件时,this指向组件本身。对于新手来说,常会用一种意会的感觉去判断this的指向。以至于当遇到复杂的函数调用时,就分不清this的真正指向。

本文将通过两道题去慢慢分析this的指向问题,并涉及到函数作用域与对象相关的点。最终给大家带来真正的理论分析,而不是简简单单的一句话概括。

相信若是对this稍有研究的人,都会搜到这句话:this总是指向调用该函数的对象。

然而箭头函数并不是如此,于是大家就会遇到如下各式说法:

箭头函数的this指向外层函数作用域中的this。

箭头函数的this是定义函数时所在上下文中的this。

箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

各式各样的说法都有,乍看下感觉说的差不多。废话不多说,凭着你之前的理解,来先做一套题吧(非严格模式下)。

/**

* Question 1

*/

var name = 'window'

var person1 = {

name: 'person1',

show1: function () {

console.log(this.name)

},

show2: () => console.log(this.name),

show3: function () {

return function () {

console.log(this.name)

}

},

show4: function () {

return () => console.log(this.name)

}

}

var person2 = { name: 'person2' }

person1.show1()

person1.show1.call(person2)

person1.show2()

person1.show2.call(person2)

person1.show3()()

person1.show3().call(person2)

person1.show3.call(person2)()

person1.show4()()

person1.show4().call(person2)

person1.show4.call(person2)()

大致意思就是,有两个对象person1,person2,然后花式调用person1中的四个show方法,预测真正的输出。

你可以先把自己预测的答案按顺序记在本子上,然后再往下拉看正确答案。

正确答案选下:

person1.show1() // person1

person1.show1.call(person2) // person2

person1.show2() // window

person1.show2.call(person2) // window

person1.show3()() // window

person1.show3().call(person2) // person2

person1.show3.call(person2)() // window

person1.show4()() // person1

person1.show4().call(person2) // person1

person1.show4.call(person2)() // person2

对比下你刚刚记下的答案,是否有不一样呢?让我们尝试来最开始那些理论来分析下。

person1.show1()与person1.show1.call(person2)好理解,验证了谁调用此方法,this就是指向谁。

person1.show2()与person1.show2.call(person2)的结果用上面的定义解释,就开始让人不理解了。

它的执行结果说明this指向的是window。那就不是所谓的定义时所在的对象。

如果说是外层函数作用域中的this,实际上并没有外层函数了,外层就是全局环境了,这个说法也不严谨。

只有定义函数时所在上下文中的this这句话算能描述现在这个情况。

person1.show3是一个高阶函数,它返回了一个函数,分步走的话,应该是这样:

var func = person3.show()

func()

从而导致最终调用函数的执行环境是window,但并不是window对象调用了它。所以说,this总是指向调用该函数的对象,这句话还得补充一句:在全局函数中,this等于window。

person1.show3().call(person2) 与 person1.show3.call(person2)() 也好理解了。前者是通过person2调用了最终的打印方法。后者是先通过person2调用了person1的高阶函数,然后再在全局环境中执行了该打印方法。

person1.show4()(),person1.show4().call(person2)都是打印person1。这好像又印证了那句:箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。因为即使我用过person2去调用这个箭头函数,它指向的还是person1。

然而person1.show4.call(person2)()的结果又是person2。this值又发生改变,看来上述那句描述又走不通了。一步步来分析,先通过person2执行了show4方法,此时show4第一层函数的this指向的是person2。所以箭头函数输出了person2的name。也就是说,箭头函数的this指向的是谁调用箭头函数的外层function,箭头函数的this就是指向该对象,如果箭头函数没有外层函数,则指向window。这样去理解show2方法,也解释的通。

这句话就对了么?在我们学习的过程中,我们总是想以总结规律的方法去总结结论,并且希望结论越简单越容易描述就越好。实际上可能会错失真理。

下面我们再做另外一个相似的题目,通过构造函数来创建一个对象,并执行相同的4个show方法。

/**

* Question 2

*/

var name = 'window'

function Person (name) {

this.name = name;

this.show1 = function () {

console.log(this.name)

}

this.show2 = () => console.log(this.name)

this.show3 = function () {

return function () {

console.log(this.name)

}

}

this.show4 = function () {

return () => console.log(this.name)

}

}

var personA = new Person('personA')

var personB = new Person('personB')

personA.show1()

personA.show1.call(personB)

personA.show2()

personA.show2.call(personB)

personA.show3()()

personA.show3().call(personB)

personA.show3.call(personB)()

personA.show4()()

personA.show4().call(personB)

personA.show4.call(personB)()

同样的,按照之前的理解,再次预计打印结果,把答案记下来,再往下拉看正确答案。

正确答案选下:

personA.show1() // personA

personA.show1.call(personB) // personB

personA.show2() // personA

personA.show2.call(personB) // personA

personA.show3()() // window

personA.show3().call(personB) // personB

personA.show3.call(personB)() // window

personA.show4()() // personA

personA.show4().call(personB) // personA

personA.show4.call(personB)() // personB

我们发现与之前字面量声明的相比,show2方法的输出产生了不一样的结果。为什么呢?虽然说构造方法Person是有自己的函数作用域。但是对于personA来说,它只是一个对象,在直观感受上,它跟第一道题中的person1应该是一模一样的。 JSON.stringify(new Person('person1')) === JSON.stringify(person1)也证明了这一点。

说明构造函数创建对象与直接用字面量的形式去创建对象,它是不同的,构造函数创建对象,具体做了什么事呢?我引用红宝书中的一段话。

使用 new 操作符调用构造函数,实际上会经历一下4个步骤:

创建一个新对象;

将构造函数的作用域赋给新对象(因此this就指向了这个新对象);

执行构造函数中的代码(为这个新对象添加属性);

返回新对象。

所以与字面量创建对象相比,很大一个区别是它多了构造函数的作用域。我们用chrome查看这两者的作用域链就能清晰的知道:

4bb1267a3894c98a9cf5f26ae14c5c2c.png

a066efe69a1a423d47e9d6e271e51a3f.png

personA的函数的作用域链从构造函数产生的闭包开始,而person1的函数作用域仅是global,于是导致this指向的不同。我们发现,要想真正理解this,先得知道到底什么是作用域,什么是闭包。

有简单的说法称闭包就是能够读取其他函数内部变量的函数。然而这是一种闭包现象的描述,而不是它的本质与形成的原因。

我再次引用红宝书的文字(便于理解,文字顺序稍微调整),来描述这几个点:

...每个函数都有自己的执行环境(execution context,也叫执行上下文),每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

...当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。当代码在环境中执行时,会创建一个作用域链,来保证对执行环境中的所有变量和函数的有序访问。函数执行之后,栈将环境弹出。

...函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中。

具体来说,当我们 var func = personA.show3() 时,personA的show3函数的活动对象,会一直保存在func的作用域链中。只要不销毁func,那么show3函数的活动对象就会一直保存在内存中。(chrome的v8引擎对闭包的开销会有优化)

而构造函数同样也是闭包的机制,personA的show1方法,是构造函数的内部函数,因此执行了 this.show1 = function () { console.log(this.name) }时,已经把构造函数的活动对象推到了show1函数的作用域链中。

我们再回到this的指向问题。我们发现,单单是总结规律,或者用一句话概括,已经难以正确解释它到底指向谁了,我们得追本溯源。

红宝书中说道:

...this引用的是函数执行的环境对象(便于理解,贴上英文原版:It is a reference to the context object that the function is operating on)。

...每个函数被调用时都会自动获取两个特殊变量:this和arguments。内部在搜索这个两个变量时,只会搜索到其活动对象为止,永远不可能直接访问外部函数中的这两个变量。

我们看下MDN中箭头函数的概念:

一个箭头函数表达式的语法比一个函数表达式更短,并且不绑定自己的 this,arguments,super或 new.target。...箭头函数会捕获其所在上下文的 this 值,作为自己的 this 值。

也就是说,普通情况下,this指向调用函数时的对象。在全局执行时,则是全局对象。

箭头函数的this,因为没有自身的this,所以this只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域(即最靠近箭头函数的普通函数作用域,或者全局环境),并指向调用该普通函数的对象。

或者从现象来描述的话,即箭头函数的this指向声明函数时,最靠近箭头函数的普通函数的this。但这个this也会因为调用该普通函数时环境的不同而发生变化。导致这个现象的原因是这个普通函数会产生一个闭包,将它的变量对象保存在箭头函数的作用域中。

故而personA的show2方法因为构造函数闭包的关系,指向了构造函数作用域内的this。而

var func = personA.show4.call(personB)

func() // print personB

因为personB调用了personA的show4,使得返回函数func的作用域的this绑定为personB,进而调用func时,箭头函数通过作用域找到的第一个明确的this为personB。进而输出personB。

讲了这么多,可能还是有点绕。总之,想充分理解this的前提,必须得先明白js的执行环境、闭包、作用域、构造函数等基础知识。然后才能得出清晰的结论。

我们平常在学习过程中,难免会更倾向于根据经验去推导结论,或者直接去找一些通俗易懂的描述性语句。然而实际上可能并不是最正确的结果。如果想真正掌握它,我们就应该追本溯源的去研究它的内部机制。

我上述所说也是我自己推导出的结果,即使它不一定正确,但这个推断思路跟学习过程,我觉得可以跟大家分享分享。

--转载请先经过本人授权。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/487435.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

洛谷1373 小a和uim之大逃离

https://www.luogu.org/problem/show?pid1373 题目背景 小a和uim来到雨林中探险。突然一阵北风吹来,一片乌云从北部天边急涌过来,还伴着一道道闪电,一阵阵雷声。刹那间,狂风大作,乌云布满了天空,紧接着豆大…

java 日期处理工具类_Java日期处理工具类DateUtils详解

本文实例为大家分享了Java日期处理工具类DateUtils的具体代码,供大家参考,具体内容如下import java.sql.Timestamp;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.Date;/*** */public…

Ipython知识点总结

初识Python: Python是一种面向对象、直译式计算机程序语言。也是一种功能强大而完善的通用型语言,已经具有十多年的发展历史,成熟且稳定。Python 具有脚本语言中最丰富和强大的类库,足以支持绝大多数日常应用。 Python语法简捷而清…

光刻技术的“鬼斧”之变

来源:脑极体 在我们今天看来,晶体管发明以后,集成电路的出现一直到今天超大规模集成电路的出现,似乎是一件水到渠成的事情。但是如果回到半导体产业初兴的历史现场,我们就会发现没有任何一项关键技术的突破是“必然产生”的。&…

php设置html全局路径_PHPCMS V9 URL去掉或修改/html路径的方法

PHPCMS V9 URL去掉或修改/html路径的方法,在后台找了好长时间没有找到在哪儿可以设置栏目生成的前缀路径/html,不过最终还是找到了。修改配置文件。找到\caches\configs\system.php 找到“html_root”这一项,然后把/html这个字符串删除即可。更新缓存&am…

[PA 2014]Kuglarz

Description 魔术师的桌子上有n个杯子排成一行,编号为1,2,…,n,其中某些杯子底下藏有一个小球,如果你准确地猜出是哪些杯子,你就可以获得奖品。花费c_ij元,魔术师就会告诉你杯子i,i1,…,j底下藏有球的总数的奇偶性。采…

城市大脑从起源到终极状态的7个阶段

0.序言21世纪以来,前沿科技领域出现诸多“大脑”概念,如谷歌大脑,百度大脑,阿里大脑,360安全大脑,腾讯超级大脑等,城市大脑,城市神经网络,工业大脑,航空大脑&…

php7 字符串,php7 参数、整形及字符串处理机制修改实例分析

本文实例讲述了php7 参数、整形及字符串处理机制修改。分享给大家供大家参考,具体如下:参数处理机制修改一、重复参数命名不再支持。重复的参数命名不再支持。比如下面的代码执行的时候会报错:public function foo($a, $b, $unused, $unused)…

【转】成功的背后

转载:来自CSDN第一名博主:http://blog.csdn.net/phphot/article/details/2187505 成功的背后,有着许多不为人知的故事,而正是这些夹杂着泪水和汗水的过去,才成就了一个个走向成功的普通人。 凌晨两点半,早已…

地理信息技术加持 用数字孪生让城市更“聪明”

文章转载自微信公众号中地数码MapGIS,版权归原作者及刊载媒体所有。相关资料表明,过去二十年,中国智慧城市建设投资巨大,建设的传感网络已经领先全球,但硬件重复建设,多注重单方面的智慧化建设,…

php5.3升级5.4,centos php 5.3升级到 php5.4版本

centos php 5.3升级到 php5.4版本发布时间:2020-06-20 06:07:39来源:51CTO阅读:1021作者:kuingfuturephp5.3听说有bug,因此单独升级php5.3相关的版本到5.4具体步骤:下面是我之前的版本之前php版本是:[rootlocalhost ~]…

你所不知道的SQL Server数据库启动过程(用户数据库加载过程的疑难杂症)

转http://www.cnblogs.com/zhijianliutang/p/4100103.html 前言 本篇主要是上一篇文章的补充篇,上一篇我们介绍了SQL Server服务启动过程所遇到的一些问题和解决方法,可点击查看,我们此篇主要介绍的是SQL Server启动过程中关于用户数据库加载…

可持续发展的人工智能

微软亚洲研究院副院长刘铁岩在 WAIC 2020 微软论坛演讲视频来源:微软研究院AI头条可持续发展是一个非常重要的主题,无论是环保、健康、能源和材料,都与人类的生存和发展息息相关。随着工业的发展和科技的进步,我们看到大气、水质、…

php width,PHP imagefontwidth()用法及代码示例

imagefontwidth()函数是PHP中的内置函数,用于获取指定字体中字符的像素宽度。用法:int imagefontwidth( int $font )参数:该函数接受一个包含字体的单个参数$font。对于内置字体,它可以是1、2、3、4、5。对于自定义字体,它可以与i…

Sqring核心概念

Spring 是大规模企业级框架,用户数量多,数据规模大,功能众多,业务复杂, 性能和安全要求高 灵活多变 Spring框架是轻量级的框架,javaEE的春天,当前主流的框架,一站式的企业应用开发…

台积电2纳米获得重大突破

来源:经济日报(台)台积电冲刺先进制程,在2纳米研发有重大突破,已成功找到路径,将切入GAA(环绕闸极)技术,为台积电发展鳍式场效电晶体(FinFET)取得…

php url乱码java接收,java中url乱码解决方法

java中url乱码解决方法:(推荐:java视频教程)1、将字符串转码:newString(“xxxxx”.getBytes(“iso-8859-1”),”utf-8”)这种转码方式有很大的弊端,因为它是使用指定的字符集将此String编码为 byte 序列,并将结果存储到…

腾讯首次发布 AI 白皮书讲了什么?

来源 | 腾讯研究院(转载请注明来源)编辑 | 蒲蒲近日,在世界人工智能大会腾讯论坛上,腾讯集团副总裁、腾讯研究院院长司晓正式发布了《腾讯人工智能白皮书:泛在智能》(以下简称白皮书)。作为腾讯…

String, StringBuffer, StringBuilder之间的区别

String与StringBuffer/StringBuilder之间的主要区别 1.String对象不可变, 如果修改会重新创建一个对象, 然后把值保存进去. StringBuffer/StringBuilder对象是可变的. 2.String性能远远低于StringBuffer/StringBuilder 3.StringBuilder速度稍快于StringBuffer, 但与此带来的缺陷…