写点什么

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:56567

评论

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

Atlassian Server用户新选择 | Data Center产品是否适合您的企业?

龙智—DevSecOps解决方案

Server Atlassian Atlassian迁移 Data Center

NFTScan 团队正式发布 NFT Portfolio 产品

NFT Research

NFT

Apifox IDEA 插件 | 帮助开发者快速生成 API 文档

Apifox

IDEA idea插件 Apifox API文档 idea web

MQTT持久会话与Clean Session详解

EMQ映云科技

物联网 IoT mqtt 企业号 2 月 PK 榜 持久会话

墨天轮发布数据库行业报告,亚信科技AntDB“超融合+流式实时数仓”开启新纪元

亚信AntDB数据库

数据库 AntDB 国产数据库 AntDB数据库 企业号 2 月 PK 榜

袋鼠云高教行业数字化转型方案,推进数字化技术和学校教育教学深度融合 | 行业方案

袋鼠云数栈

大数据‘’

flutter系列之:在flutter中使用导航Navigator

程序那些事

flutter 大前端 程序那些事

在TitanIDE中使用ChatGPT辅助科研开发

行云创新

AI 云端开发 TitanIDE

到底怎么理解分布式事务

做梦都在改BUG

快速实现一个企业级域名SSL证书有效期监控巡检系统

观测云

前端 后端 可观测性 观测云 可观测性用观测云

3款强大到离谱的电脑软件,个个提效神器,从此远离加班

这我可不懂

低代码 开发工具 低代码开发平台 协同办公软件 办公软件

项目上线后我是如何通过慢查询和索引让系统快起来的

做梦都在改BUG

MySQL 数据库 索引

京东力荐!深入理解高并发编程手册,GitHub上线3小时飙升榜首

做梦都在改BUG

Java 并发编程 高并发

用javascript分类刷leetcode15.链表(图文视频讲解)

js2030code

JavaScript LeetCode

代码质量与安全 | 一文了解高级驾驶辅助系统(ADAS)及其开发中需要遵循的标准

龙智—DevSecOps解决方案

静态代码分析 ADAS 汽车软件开发 汽车软件

佛萨奇2.0智能合约矩阵公排系统开发源代码(可改链)

开发微hkkf5566

聚焦中国大数据流程挖掘,这场发布会值得关注!

ToB行业头条

线上研讨会报名 | 与龙智、Perforce共探大规模研发中的数字资产管理与版本控制,赢取千元大奖

龙智—DevSecOps解决方案

版本控制 数字资产 游戏开发 芯片开发 数字资产管理

数仓在线运维:如何进行在线增删CN

华为云开发者联盟

数据库 后端 华为云 企业号 2 月 PK 榜 华为云开发者联盟

MQTT QoS 0,1,2介绍

EMQ映云科技

物联网 IoT mqtt QoS 企业号 2 月 PK 榜

天翼云AI团队夺得ZeroCLUE榜单桂冠

Geek_2d6073

2023 届春招 Java 岗高频面试题盘点,老司机也未必全会,真的太卷了

架构师之道

编程 程序员 java面试

为什么推荐一个容器只运行一个进程?

追赶者

k8s 为什么

JavaScript刷LeetCode拿offer-栈相关题目

js2030code

JavaScript LeetCode

【立哥】【每日一个小知识】按照法律,遗产应该怎么继承?

Lee Chen

法律 知识

来讲讲怎样获取到url上所有参数并以对象形式保存,再讲讲JSON解析与序列化

梁木由

前端 前端开发 前端面试

Teradata退出中国,您可以相信中国数据库!

墨天轮

数据库 数据仓库 GaussDB gbase8a teradata

【NeurIPS 2022】视频动作识别,AFNet 用更低的成本接收更多数据

Zilliz

分布式事务系统Seata的这些安保机制是否会让你更放心

做梦都在改BUG

Java 分布式 Spring Boot seata

带你动手设计一个高速公路多节点温度采集系统

华为云开发者联盟

云计算 物联网 华为云 企业号 2 月 PK 榜 华为云开发者联盟

BSN-DDC基础网络详解(三):注册门户账号和业务开通(1)

BSN研习社

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