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

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

一.this是什么

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

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

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

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

二.近距离看this

2.1 this的语法意义

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

  • 大爷是赵本山
    • 请问:谁大爷是赵本山?
    • 无法回答,由于没有上下文约束,此处的可能指任何人。
  • 李雷来头可不小,大爷是赵本山
    • 请问:谁大爷是赵本山?
    • 很容易回答,由于前一句话使得咱们可以得知当前上下文中,"他"指的就是"李雷"
  • ___来头可不小,大爷是赵本山
    • 请问:谁大爷是赵本山?
    • 此处空格填谁,谁大爷就是赵本山。

小结一下:框架

代词,用于指代某个具体事物,当结合上下文时,就能够知道其具体的指向。换句话说,有了上下文时,代词就有了具体的意义。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

相关文章
相关标签/搜索