javascript基础修炼(2)——What's this(上)

开发者的javascript造诣取决于对【动态】和【异步】这两个词的理解水平。

一.this是什么

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

this是一个特殊的与Execution Contexts相关的对象,用于指明当前代码执行时的Execution Contextsthis在语句执行进入一个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的用法,许多开发者使用了另一种变通的方式,也就是显式传参。比如我们在一个方法中,需要打出上下文对象的名字,下面两种编写方式都是可以实现的。

//方式一.使用this
invoker.whoInvokeMe = function(){
    console.log(this.name);
}

//方式二.不使用this
function 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 __'这句话中再返回给我。而不是一个与调用对象有着紧密联系的自我介绍动作。
  • 画蛇添足的参数传递
    在正确的使用过程中,thisinvoker 的指向是一致的,形参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代码的境地,只是需要将所有的定义和使用场景全部具体化, 需要手动对所有的具体功能编写具体实现,也就是"面向过程"的编程。

我是华丽的分割线======

【轻松一刻】

话说赤壁之战后,一日闲来无事,孔明与刘关张三兄弟一起喝酒。孔明说,我出三道题考考各位学识修养,如何啊?三兄弟举手赞同。
孔明:第一题,主公,赤壁之战发生在哪里?
刘备:赤壁啊
孔明:答对了,主公果然厉害。第二题,关将军,双方有多少人参战?
关羽:联军5万,曹军20余万。
孔明:答对了,关将军也是智勇双全啊。最后一题,他们分别是谁?
张飞:我......我靠

愿你能够掌握this,不要在自己的代码里搞出他们分别是谁的尴尬,小心被队友活埋。

我是华丽的分割线======

三. 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的指向的。
笔者将在《javascript基础修炼(3)——What's this(下)》中详细讲述开发中千奇百怪的this。欲知后事如何,先点个赞先吧!

参考文章:
[1].js中的new()到底做了什么
[2].ECMA-262-3 in detail. Chapter 1. Execution Contexts

posted @ 2018-08-02 22:04  大史不说话  阅读(630)  评论(0编辑  收藏