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 的指向,函数的执行过程也不会有歧义。
无论是计算机对象调用 addNumber 方法,或是算盘对象调用 addNumber 方法,甚至是人类对象通过心算调用 addNumber 方法,都无所谓,因为我们关注的是结果,而不是它怎么来的。
b.不使用函数自带的 this
有时候我们编写的代码是需要用到一些关于调用对象的信息的,但由于不熟悉 this 的用法,许多开发者使用了另一种变通的方式,也就是显式传参。比如我们在一个方法中,需要打出上下文对象的名字,下面两种编写方式都是可以实现的。
方式二的方式并不是语法错误,可以让开发者避开了因为对 this 关键字的误用而引发的混乱,同样也避开了 this 所带来的对代码的抽象能力和简洁性,同时会造成一些性能上的损失,毕竟这样做会使得每次调用函数时需要处理更多的参数,而这些参数本可以通过内置的 this 获取到。
c.面向对象的编程
提到 this,必然会提到另一个词语——面向对象。"面向对象"是一种编程思想,请暂时抛开封装,继承,多态等高大上的修饰词带来的负担,纯粹地感受一下这种思想本身。有的人说"面向对象"赋予了编程一种哲学的意义,它是使用程序语言的方式对现实世界进行的一种简化抽象,现实世界的一个用户,一种策略,一个消息,某个算法,在面向对象的世界里均将其视为一个对象,也就是哲学意义上的无分别,每一个对象都有其生命周期,它怎么来,要做什么,如何消亡,以及它与万物之间的联系。
面向对象的思想,是用程序语言勾勒现实世界框架的方式之一,它的出现不是用来为难开发者的,而是为了让开发者能以更贴近日常生活的认知方式来提升对程序语言的理解能力。
2.4 如果没有 this
我们来看一下如果 javascript 中不使用 this 关键字,对程序编写会造成什么影响呢?我们先来编写一段简单的定义代码:
在上面的简单示例中,我们定义了一个不包含任何实例属性的人类,并使用不同的方式为其定义介绍你自己这个方法,第一种定义使用常规的面向对象写法,使用 this 获取上下文对象,获取实例的 name 属性;第二种定义不使用 this,而是将调用者名称作为参数传递进方法。我们在控制台进行一些简单的使用:
那么这两种不同的写法区别到底是什么呢?
函数实际功能的变化
从上面的示例中不难看出,当开发中不使用 this 时,需要开发者自行传入上下文对象,并将其以参数的形式在函数执行时传入,如果传入的 invoker 对象和 this 的指向一致,那么结果就一致,如果不一致,则会造成混乱。
从编码角度来看
introduceYourselfWithThis()方法只是 introduceYourself(invoker)方法的特例(当 this === invoker 时)。
从方法的含义来看
定义者希望实现自我介绍功能而编写了 introduceYourself()方法,可是使用者在阅读到 introduceYourself()的源码时看到的代码表达的意义是:我告诉你一个名字,你把它填在’My name is __'这句话中再返回给我。而不是一个与调用对象有着紧密联系的自我介绍动作。
画蛇添足的参数传递
在正确的使用过程中,this 和 invoker 的指向是一致的,形参 invoker 的定义不仅增加了函数使用的复杂度,也增加了函数运行的负担,却没有为函数的执行带来任何新的附加信息。
重复的雷同代码
如果编码中不使用 this,也就相当于汉语中不使用代词,那么我们就需要在每一个独立的句子中使用完整的信息。为了使 introduceYourself()方法能够正确的执行,我们需要在每一个实例生成后,为其绑定确切的实例方法,即:
即时不使用 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 操作符的逻辑可以用伪代码表示为:
暂不考虑构造函数有返回值的情况,那么很容易就可以明白 this 为什么指向实例了,因为类定义函数在执行的时候显式地绑定了 this 为新生成的对象,也就是调用 new 操作符后得到的实例对象。
语文老师解读版:
有些同学喜欢抄袭,抄袭这个动作可以描述为:“把一份作业 Copy 一遍,在最后写上自己的名字。”。如果李雷是喜欢抄袭的人之一,那么他就掌握了"抄袭"这个方法,那你觉得他每次抄完作业后在署名的地方应该写自己的名字"李雷"还是写这一类人的总称"喜欢抄袭的人"呢?
抬杠的那个同学,我记住你了!放学别走!
规则 4——使用 call/apply/bind 方法显式指定 this
码农视角:
call/bind/apply 这三个方法是 javascript 动态性的重要组成部分,后续的篇章会有详细的讲解。这里只看一下 API 用法,了解一下其对于 this 指向的影响:
这个规则很好理解,就是说函数执行时遇到函数体里有 this 的语句都用显式指定的对象来替换。
语文老师解读版:
就是直接告诉你下文中的代词指什么,比如:中华人民共和国宪法(以下简称"本法"),那读者当然就知道后面所说的"本法"指谁。
基本规则示例
为了更清晰地看到上面两条原则的区别,我们来看一个示例:
控制台输出的结果是这样的:
同一个方法,同一个 this,调用的方式不同,得到的结果也不同。
后记
在基础面前,一切技巧都是浮云。
如果认为明白了 this 的基本规则就可以为所欲为,那你就真的 too young too simple 了。
了解了基本指向规则,只能让你在开发中自己尽可能少挖坑或者不挖坑。但是想要填别人的坑或者读懂大师级代码中简洁优雅的用法,还需要更多的修炼和反思。实际应用中许多复杂的使用场景是很难一下子搞明白 this 的指向以及为什么要指定 this 的指向的。
本文转载自 华为云产品与解决方案 公众号。
原文链接:https://mp.weixin.qq.com/s/3BGKmGcI7p8XibJt5536pw
评论