HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

javascript 基础修炼(2)

  • 2020-04-01
  • 本文字数:5864 字

    阅读完需:约 19 分钟

javascript基础修炼(2)

this 是什么

this 是 javascript 关键字之一,是 javascript 能够实现面向对象编程的核心概念。用得好能让代码优雅高端,风骚飘逸,用不好也绝对是坑人坑己利器。我们常常会在一些资料中看到对 this 的描述是:


this 是一个特殊的与 Execution Contexts 相关的对象,用于指明当前代码执行时的 Execution Contexts,this 在语句执行进入一个 Execution Contexts 时被赋值,且在代码执行过程中不可再改变。注:Execution Contexts 也就是我们常听到的"上下文"或"执行环境"。


看不懂?看不懂就对了,我也看不懂。对于 this 的指向,我们常会听到这样一个原则——this 是一个指针,指向当前调用它的对象。但实际使用中,我们却发现有时候很难知道当前调用它的是哪个对象,从而引发了一系列的误用和奇怪现象。


今天,我们就换一种思路,试试如何从语言的角度一步一步地去理解 this,你会发现:只要你能听懂中国话,就意味着你能理解 this。

近距离看 this

2.1 this 的语法意义

javascript 是一门程序设计语言,也就是说,它是一种语言,是语言,就有语法特性。如果抛开 this 的原理和编程中的用法,仅从语文的层面去理解,它的本质就是代词。什么是代词?汉语中的你,我,他,你们,我们,他们这一类的词语就是代词。代词并不具体指某一个具体的事物,但结合上下文,就可以知道这类词语代替的是谁。比如下面这几句描述的语境:


他大爷是赵本山


  • 请问:谁大爷是赵本山?

  • 没法回答,因为没有上下文约束,此处的他可能指任何人。

  • 李雷来头可不小,他大爷是赵本山

  • 请问:谁大爷是赵本山?

  • 很容易回答,因为前一句话使得我们能够得知当前上下文中,“他"指的就是"李雷”。

  • ___来头可不小,他大爷是赵本山

  • 请问:谁大爷是赵本山?

  • 此处空格填谁,谁大爷就是赵本山。


小结一下:


代词,用于指代某个具体事物,当结合上下文时,就可以知道其具体的指向。换句话说,有了上下文时,代词就有了具体的意义。this 在 javascript 语言中的意义,就如同代词在汉语中的意义是一样的。

2.2 不同作用域中的 this

在 ES6 出现前,javascript 中的作用域只分为全局作用域和函数作用域两种。(以下部分暂不讨论严格模式)。


  • 全局作用域中使用 this

  • 全局作用域中的 this 是指向 window 对象的,但 window 对象上却并没有 this 这个属性:



  • 函数作用域使用 this

  • 函数作用域中的 this 也是有指向的(本例中指向 window 对象),我们知道函数的原型链是会指向 Object 的,所以函数本身可以被当做一个对象来看待,但遗憾的是函数的原型链上也没有 this 这个属性:



综上所述,this 可以直观地理解为:


this 与函数相关,是函数在运行时解释器自动为其赋值的一个局部常量。

2.3 javascript 代码编写方式

a.不使用 this


这是有可能发生的。很多初学者会发现,自己在编写 javascript 代码时并没有用到 this,但是也并不影响自己编写代码。前面提到过上下文信息的意义在于让代词明确其指向,那么如果一段话的上下文中并没有使用代词,在语文中我们就不需要联系上下文就能理解这段话;同理,如果函数的函数体中并没有使用 this 关键字来指代任何对象,或者不需要关注其调用对象,那实际上就算不确定 this 的指向,函数的执行过程也不会有歧义。


/***数据加工转换类的函数,对开发者来说更关注结果,而并不在乎是谁在调用。*/function addNumber(a,b) {   return a + b;}
复制代码


无论是计算机对象调用 addNumber 方法,或是算盘对象调用 addNumber 方法,甚至是人类对象通过心算调用 addNumber 方法,都无所谓,因为我们关注的是结果,而不是它怎么来的。


b.不使用函数自带的 this


有时候我们编写的代码是需要用到一些关于调用对象的信息的,但由于不熟悉 this 的用法,许多开发者使用了另一种变通的方式,也就是显式传参。比如我们在一个方法中,需要打出上下文对象的名字,下面两种编写方式都是可以实现的。


//方式一.使用thisinvoker.whoInvokeMe = function(){   console.log(this.name);}
//方式二.不使用thisfunction whoInvokeMe2(invoker){ console.log(invoker.name);}
复制代码


方式二的方式并不是语法错误,可以让开发者避开了因为对 this 关键字的误用而引发的混乱,同样也避开了 this 所带来的对代码的抽象能力和简洁性,同时会造成一些性能上的损失,毕竟这样做会使得每次调用函数时需要处理更多的参数,而这些参数本可以通过内置的 this 获取到。


c.面向对象的编程


提到 this,必然会提到另一个词语——面向对象。"面向对象"是一种编程思想,请暂时抛开封装,继承,多态等高大上的修饰词带来的负担,纯粹地感受一下这种思想本身。有的人说"面向对象"赋予了编程一种哲学的意义,它是使用程序语言的方式对现实世界进行的一种简化抽象,现实世界的一个用户,一种策略,一个消息,某个算法,在面向对象的世界里均将其视为一个对象,也就是哲学意义上的无分别,每一个对象都有其生命周期,它怎么来,要做什么,如何消亡,以及它与万物之间的联系。


面向对象的思想,是用程序语言勾勒现实世界框架的方式之一,它的出现不是用来为难开发者的,而是为了让开发者能以更贴近日常生活的认知方式来提升对程序语言的理解能力。

2.4 如果没有 this

我们来看一下如果 javascript 中不使用 this 关键字,对程序编写会造成什么影响呢?我们先来编写一段简单的定义代码:


 //假设我们定义一个人的类   function Person(name){
} // 方法-介绍你自己(使用this编写) Person.prototype.introduceYourselfWithThis = function () { if (Object.hasOwnProperty.call(this, 'name')) { return `My name is ${this.name}`; } return `I have no name`; }
// 方法-介绍你自己(不使用this编写) Person.prototype.introduceYourself = function (invoker) { if (Object.hasOwnProperty.call(invoker, 'name')) { return `My name is ${invoker.name}`; } return `I have no name`; }
//生成两个实例,并为各自的name属性赋值 var liLei = new Person(); liLei.name = 'liLei'; var hanMeiMei = new Person(); hanMeiMei.name = 'hanMeiMei';
复制代码


在上面的简单示例中,我们定义了一个不包含任何实例属性的人类,并使用不同的方式为其定义介绍你自己这个方法,第一种定义使用常规的面向对象写法,使用 this 获取上下文对象,获取实例的 name 属性;第二种定义不使用 this,而是将调用者名称作为参数传递进方法。我们在控制台进行一些简单的使用:



那么这两种不同的写法区别到底是什么呢?


  • 函数实际功能的变化

  • 从上面的示例中不难看出,当开发中不使用 this 时,需要开发者自行传入上下文对象,并将其以参数的形式在函数执行时传入,如果传入的 invoker 对象和 this 的指向一致,那么结果就一致,如果不一致,则会造成混乱。

  • 从编码角度来看

  • introduceYourselfWithThis()方法只是 introduceYourself(invoker)方法的特例(当 this === invoker 时)。

  • 从方法的含义来看

  • 定义者希望实现自我介绍功能而编写了 introduceYourself()方法,可是使用者在阅读到 introduceYourself()的源码时看到的代码表达的意义是:我告诉你一个名字,你把它填在’My name is __'这句话中再返回给我。而不是一个与调用对象有着紧密联系的自我介绍动作。

  • 画蛇添足的参数传递

  • 在正确的使用过程中,this 和 invoker 的指向是一致的,形参 invoker 的定义不仅增加了函数使用的复杂度,也增加了函数运行的负担,却没有为函数的执行带来任何新的附加信息。

  • 重复的雷同代码

  • 如果编码中不使用 this,也就相当于汉语中不使用代词,那么我们就需要在每一个独立的句子中使用完整的信息。为了使 introduceYourself()方法能够正确的执行,我们需要在每一个实例生成后,为其绑定确切的实例方法,即:


 var liLei = new Person();   liLei.name = 'liLei';   //定义实例方法   liLei.introduceYourself = function (){       return `My name is liLei`;   };     var hanMeiMei = new Person();   hanMeiMei.name = 'hanMeiMei';   //定义实例方法   hanMeiMei.introduceYourself = function (){       return `My name is hanMeiMei`;   }
复制代码


即时不使用 this,你也不会直接陷入无法编写 javascript 代码的境地,只是需要将所有的定义和使用场景全部具体化, 需要手动对所有的具体功能编写具体实现,也就是"面向过程"的编程。

this 的一般指向规则

javascript 中有四条关于 this 指向的基本规则。今天,我们将一起通过【码农视角】和【语文老师视角】来分别解读这些规则,你会发现他们理解起来其实很自然。


  • 规则 1——作为函数调用时,this 指向全局对象

  • 码农视角:

  • 浏览器中的全局对象,指的是 window 对象。这一规则指的就是我们在全局作用域或者函数作用域中使用 function 关键字直接声明或使用函数表达式赋值给标识符的方式创建的函数。为了在调用时在内存中找到所声明的方法,我们需要一个标识符来指向它的位置,具名函数可以通过它的名字找到,匿名函数则需要通过标识符来找到。作为函数调用的实质,就是通过方法名直或标识符找到函数并执行它。


一般什么样的函数我们会这样定义呢?


就是那些不关注调用者的函数,比如上面举例的 addNumber()方法,这类函数往往是将一步或几步业务逻辑组合在一起,起一个新的名字便于管理和重用,而并不关注使用者到底是谁。


  • 语文老师解读版:

  • 很好理解,当你想描述一个动作却不知道或者不关注具体是谁做的,代词就指向有的人。

  • 比如臧克家同学在作文里写的这样:有的人活着,但是他已经死了;有的人死了,但是他还活着;

  • 上文中的他指谁?指有的人;那有的人是谁?随便,爱谁谁。

  • 规则 2——作为方法调用时,this 指向上下文对象

  • 码农视角:

  • 上文中我们看到函数的作用域链上是包含 Object 对象的,所以函数可以被当做对象来理解。当函数作为对象被赋值在另一个对象的属性上时,这个对象的属性值里会保存函数的地址,因为用函数作为赋值运算的右值时是一个引用类型赋值。如果这个函数正好又是一个匿名函数,那么执行时只能通过对象属性中记录的地址信息来找到这个函数在内存中的位置,从而执行它。所以当函数作为方法调用时,this 中包含的信息的本质是这个函数执行时是怎么被找查找到的。答案就是:通过 this 所指向的这个对象的属性找到的。

  • 一般什么样的函数我们会这样定义呢?


作为方法定义的函数,往往是另一个抽象合集的具体实现。比如前例的 addNumber()这个方法,只是将两个数字相加这样一个抽象动作,至于是谁通过什么方式来执行这个计算过程,无所谓,它可以概括所有对象将两个数字相加并给出结果这一动作。可如果它作为一个对象方法来调用时,就有了更明确的现实指向意义:


Computer.addNumber()表达了计算机通过软硬件联合作用而给出结果的过程


Calculator.addNumber()表达了计算器通过简易硬件计算给出结果的过程


Abacus.addNumber()表达了算盘通过加减珠子的方式给出结果的过程



  • 语文老师解读版:

  • 当你想知道一个代词具体指的是谁时,当然需要联系上下文语境进行理解。

  • 规则 3——作为构造函数使用时,this 指向生成的实例

  • 码农视角:

  • 作为构造函数使用,就是 new + 构造函数名的方式调用的情况。js 引擎在调用 new 操作符的逻辑可以用伪代码表示为:


new Person('liLei') = {   //生成一个新的空对象   var obj = {};    //空对象的原型链指向构造函数的原型对象   obj.__proto__ = Person.prototype;    //使用call方法执行构造函数并显式指定上下文对象为新生成的obj对象   var result = Person.call(obj,"liLei");    // 如果构造函数调用后返回一个对象,就return这个对象,否则return新生成的obj对象   return typeof result === 'object'? result : obj;}
复制代码


暂不考虑构造函数有返回值的情况,那么很容易就可以明白 this 为什么指向实例了,因为类定义函数在执行的时候显式地绑定了 this 为新生成的对象,也就是调用 new 操作符后得到的实例对象。


  • 语文老师解读版:

  • 有些同学喜欢抄袭,抄袭这个动作可以描述为:“把一份作业 Copy 一遍,在最后写上自己的名字。”。如果李雷是喜欢抄袭的人之一,那么他就掌握了"抄袭"这个方法,那你觉得他每次抄完作业后在署名的地方应该写自己的名字"李雷"还是写这一类人的总称"喜欢抄袭的人"呢?

  • 抬杠的那个同学,我记住你了!放学别走!

  • 规则 4——使用 call/apply/bind 方法显式指定 this

  • 码农视角:

  • call/bind/apply 这三个方法是 javascript 动态性的重要组成部分,后续的篇章会有详细的讲解。这里只看一下 API 用法,了解一下其对于 this 指向的影响:


func.call(this, arg1, arg2...)func.apply(this, [arg1, arg2...])func.bind(this [, arg1[, arg2[, ...]]])
复制代码


这个规则很好理解,就是说函数执行时遇到函数体里有 this 的语句都用显式指定的对象来替换。


  • 语文老师解读版:

  • 就是直接告诉你下文中的代词指什么,比如:中华人民共和国宪法(以下简称"本法"),那读者当然就知道后面所说的"本法"指谁。

基本规则示例

为了更清晰地看到上面两条原则的区别,我们来看一个示例:


var heroIdentity = '[Function Version]Iron Man';       function checkIdentity(){   return this.heroIdentity;} 
var obj = { name:'Tony Stark', heroIdentity:'[Method Version]Iron Man', checkIdentityFromObj:checkIdentity}
function TheAvenger(name){ this.heroIdentity = name; this.checkIdentityFromNew = checkIdentity;}
var tony = new TheAvenger('[New Verison]Iron Man');
console.log('1.直接调用方法时结果为:',checkIdentity());console.log('2.通过obj.checkIdentityFromObj调用同一个方法结果为:',obj.checkIdentityFromObj());console.log('3.new操作符生成的对象:',tony.checkIdentityFromNew());console.log('4.call方法显示修改this指向:',checkIdentity.call({heroIdentity:'[Call Version]Iron Man'}));
复制代码


控制台输出的结果是这样的:



同一个方法,同一个 this,调用的方式不同,得到的结果也不同。

后记

在基础面前,一切技巧都是浮云。


如果认为明白了 this 的基本规则就可以为所欲为,那你就真的 too young too simple 了。


了解了基本指向规则,只能让你在开发中自己尽可能少挖坑或者不挖坑。但是想要填别人的坑或者读懂大师级代码中简洁优雅的用法,还需要更多的修炼和反思。实际应用中许多复杂的使用场景是很难一下子搞明白 this 的指向以及为什么要指定 this 的指向的。


本文转载自 华为云产品与解决方案 公众号。


原文链接:https://mp.weixin.qq.com/s/3BGKmGcI7p8XibJt5536pw


2020-04-01 14:56738

评论

发布
暂无评论
发现更多内容

商品评价聚合:利用API从多个来源获取数据的详细指南

Noah

SD-WAN助力企业数据传输安全

Ogcloud

SD-WAN 企业网络 SD-WAN组网 SD-WAN服务商 SDWAN

一次投入,多倍回报,英特尔vPro驱动全新商用AI PC

E科讯

敏捷开发最佳实践:质量维度实践案例之软硬一体持续交付

爱吃小舅的鱼

敏捷 敏捷开发 敏捷质量管理

拼多多API助力商家提升销售效果和市场竞争力

技术冰糖葫芦

API 接口

聚道云助力时尚巨头打通数据孤岛,实现全渠道管理升级!

聚道云软件连接器

案例分享

探秘闲鱼商品详情关键词搜索电商API接口,让你轻松找到心仪的宝贝!

联讯数据

超越传统:人工智能赋能加持下的自动化测试

霍格沃兹测试开发学社

国内行业桌面云解决方案哪家比较强?

青椒云云电脑

桌面云 云桌面 云桌面方案

WiMinet 评说1.1:多跳无线网络的现状

Geek_ab1536

SD-WAN网络搭建技术:企业降本增效的首选

Ogcloud

SD-WAN 企业网络 SD-WAN组网 SD-WAN服务商 SDWAN

陆海×微帧,在海洋卫星传输环境下的极限视频压缩

微帧Visionular

视频编码 视频压缩

揭秘「 B 站最火的 RAG 应用」是如何炼成的

Zilliz

AI Milvus Zilliz LLM rag

如何创建自己的Spring Boot Starter并为其编写单元测试

EquatorCoco

Spring Boot 开发测试 功能测试

舞台LED显示屏与传统LED显示屏的区别

Dylan

LED显示屏 全彩LED显示屏 led显示屏厂家 户内led显示屏 舞台表演

SD-WAN对金融行业的重要性

Ogcloud

SD-WAN 企业网络 SD-WAN组网 SD-WAN服务商 SDWAN

测试开发名企定向培养训练营,快速提升核心竞争力!

霍格沃兹测试开发学社

云桌面跟远程桌面有什么区别?

青椒云云电脑

云桌面 云桌面方案 云桌面系统

Hybird App开发,纯血鸿蒙系统快速助力兼容

Geek_2305a8

如何利用Allure报告提升你的测试效率?

霍格沃兹测试开发学社

Scrumban的实施指南

爱吃小舅的鱼

敏捷 敏捷开发 Scrumban

2024-02-28:用go语言,有一个由x轴和y轴组成的坐标系, “y下“和“y上“表示一条无限延伸的道路,“y下“表示这个道路的下限,“y上“表示这个道路的上限, 给定一批长方形,每一个长方形有(

福大大架构师每日一题

福大大架构师每日一题

为什么您的网站需要美国主机CDN加速?一文解答所有疑问

一只扑棱蛾子

美国主机

火山引擎弹性容器实例:从节点中心转型 Serverless 化架构的利器

Geek_2d6073

MES系统中的手动排产和自动排产-助力生产效率

万界星空科技

制造业 mes 万界星空科技 生产管理 自动排班

Java面试题大全(2024最新版)1000+大厂面试题附答案详解

架构师之道

程序员 java面试

如何生成速卖通平台的API密钥?

技术冰糖葫芦

API 接口

KaiwuDB 拿下 “物联之星” 双项殊荣

KaiwuDB

数据库 物联网

Web3.0区块链技术全流程方案:DApp项目开发、推广以及运营

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

云桌面系统厂家-青椒云

青椒云云电脑

云桌面 云桌面厂家 云桌面方案 云桌面系统

HFT算力合约模式系统开发丨技术详情开发

l8l259l3365

javascript基础修炼(2)_行业深度_华为云产品与解决方案_InfoQ精选文章