聊聊JavaScript核心之"this"关

作者: 长梦未央 | 来源:发表于2017-10-28 21:27 被阅读8次

    在我能想到的所有的编程语言中,都有动态引用当前对象的方法。在JavaScript中,我们使用关键词"this"来进行这种引用。但是我们都知道,有时候这个this很容易让人搞不清它到底指的是什么,而返回的对象也并不是你所期望的结果。 其实参数”this“可能是这门语言中最容易理解错的一个概念了,但只要理解对,还是很好用的。参数"this"除了某些特殊情况外,它表现的和普通参数几乎一模一样。大家都知道,参数就是函数的括号里面的字。那么普通参数和参数”this“之间,主要有两个区别:第一个是你无法给参数this起名,它永远都叫this;第二个就是,当你给this绑定值时,会和给普通参数绑定值有点不一样。
    所以参数"this"到底是干嘛用的呢?This是一个标识符,它需要和值进行绑定,跟变量差不多。但是在代码中,它并不是和某个具体的值进行绑定,而是自动绑定到正确的对象上。一般来说,参数究竟绑定在哪个对象上,都是由定位函数参数规则决定的。而定位函数参数和参数"this"的区别决定了你在调用方法或者构造函数时,对于谁才是真正的对象的判断。
    下面看一段代码

    var obj = { fn :function(a,b){ 
    log(this); 
    }
    }
    obj.fn(3,4);
    

    在这段代码里,你认为"this"指的是什么呢?
    当函数被调用时,如果它的左边有一个点符号,就意味着它是作为某个对象的属性被查找的。你可以看这个点符号的左边,从而得知它是在哪个对象中被查找的。在这个函数被调用时,它所查找的这个对象就是关键词this绑定的内容。这个定义在90%的情况下都适用,因为有时候我们有可能并没有使用点符号访问这个函数。比如我们也有可能使用括号访问。然而在这个例子中,点符号左边的对象正好与我们最初将这个函数定义为其属性的对象相同。事实上这只是一个巧合。如果这个函数也是其他对象的方法,我们也同样可以将其作为那个对象的属性调用。无论如何,并不能认为就是函数中关键词this所指代的内容。也不能认为this最初被定义的对象或函数就是关键词this指代的内容。事实上跟它相关的是调用时的点符号以及点符号左边的对象。
    先看一段没有参数"this"的代码

    var fn = function(one,two){ 
    log(one,two);
    }
    ;var r = {},g = {},b={};
    fn(g,b);
    

    这段代码里,在执行fn(g,b);之前,参数”one“”two“是没有被绑定任何内容的,当执行函数fn(g,b)后,"one"绑定了g,"two"绑定了b;如果这个理解透彻,那么你就更容易深入理解参数this了。因为在JavaScript中,关键词this在大部分重要的用法中都以参数的形式出现。关于判断如何向函数中传入值,以及如何在调用时绑定这些传入的参数,这些都适用于判断使用参数this的方式。
    下面先讲讲this的常见用法,就是在方法调用中作为参数使用。
    为了使这个函数可以作为方法被调用,我们首先需要将其添加为一个对象的属性,这里把它设置为r的属性

    var fn = function(one, two){
     log(this,one, two);
    };
    var r = {},g = {},b={};
    r.method = fn;r.method(g,b);
    

    这里,括号中被传入的这两个参数被绑定在了输入参数one和two上,通过点符号,我们以对象属性的方式调用了该属性所对应的函数,由此也传入了第三个参数,点符号左边的值在这个函数调用中被自动命名为this。如果我们要进行面向对象的编程,这种表现很有用。因为对于任意给定方法的调用而言,通常都会对应一个相关的目标对象。而在方法被调用时,这个对象通常出现在点符号的左侧。那么此时的this指的就是对象r,输出结果是r的value(值),g,b。
    那么如果我们没有使用点符号来为关键词this传入一个具体的绑定时,this会被绑定到哪里呢?

    fn(g,b);
    

    答案是全局对象,global。此时JavaScript会默认将this绑定为global。这与参数在没有足够的参数的情况下被调用时,JavaScript将undefined绑定到定位参数上是相似的。即如果在执行函数fn()时没有传入参数g,b。那么one和two就会被绑定到undefined。点符号是我们为关键词this传入绑定的机制与方法,因此如果没有点符号,参数this就会被绑定为某些默认的值。
    那么如果拥有参数"this"的函数没有作为某个对象的方法呢,此时就不能用点语法来调用此函数了,此时如何绑定"this"的值呢?

    var fn = function(one,two){ 
    log(this,one,two);
    };
    r.method = fn;v
    ar r = {},g = {},b={};
    fn.call(r,g,b);
    r.method.call(y,g,b);
    

    答案是通过使用函数的内置方法 .call 我们可以重写默认的全局对象绑定以及点符号左侧的规则。我们可以传入任何我们想要的值并且把它绑定到关键词this上。
    那么当我们以点语法执行函数r.method.call**(y,g,b)的时候,this会被绑定到哪里呢?这个method已经是对象r的一个属性。
    答案是y, 关键词this被绑定到了y。因为被call函数重写
    关于参数this很多人觉得很困惑的一点是,在回调函数中它将如何被绑定。我们把fn作为参数传入函数

    setTimeout;
    setTimeout(fn, 1000);
    

    当setTimeout函数运行时,该函数的调用会与setTimeout的运行有1s的延时。正如你所见,我们并未在回调函数fn中,传入任何可作为实参的值,因此很难想象这段代码会如我们预期般运行。那么我先看看这些定位参数,即fn的one/two参数,因为它们更直观一些。你认为它们将绑定什么内容?记住在没有看到这个函数的具体调用之前,你是无法知道它的参数将会绑定什么值的。因此我们需要寻找函数fn的调用。
    单凭这行代码我们是看不到fn是如何被调用的。因此我们需要查看setTimeout的源代码或者文档说明。
    假设setTimeout是JavaScript中的原生函数,我们可以查找到它的文档,那就让我们来看看它是如何执行的。我们假设它被定义于一个名为timer.js的文件里,你认为这个文件的内容是什么?首先,需要一个函数定义,其中包含已定义的变量setTimeout,并指向某个函数。这个函数有两个实参,一个是你的回调函数,一个是调用回调函数之前等待的毫秒数。那么在函数内部会发生什么呢?首先就是系统延迟一定的毫秒数,然后再执行你的回调函数。此处注意,如果你了解JavaScript并发模型的话,你就会知道事实上不可能在JavaScript原生代码中实现。但是为了举例,我们暂时将其想象成这样。下一步setTimeout将引用你的函数,并用某种防水调用它。现在终于可以问一下自己,setTimeout传入到回调函数中的值到底是什么?由于setTimeout并无法知道你想要传入函数中的值,它可能不得不在没有任何参数的情况下调用这个函数。那么此时你认输输出结果中,后两位会被输出什么呢?

    var setTimeout = function(cb,ms){ waitSomehow(ms); cb();};//timer.js
    

    那么参数this绑定的内容是什么呢?为了回答这个问题,我们再想一下判断关键词this指代内容的参考规则,即找到调用的时间点,查找点符号并查看这个点符号左侧的内容。此时应该看fn执行的地方,即cb( ),然而这里并没有点符号,那么久应该运用默认绑定规则,也就是global。跟位置参数一样,参数this没有值被传入,这是因为在访问回调函数时,它并被没有作为属性访问。这个假设的setTimeout也并没有使用比如点语法符号的调用机制。
    那么如果我们是传入一个方法,而该方法又是某个对象的属性的时候,会发生什么?如下面这样
    setTimeout(r.method,1000);

    注意我们现在使用了点记法来查找这个方法,并将其传入setTimeout。
    首先来看看定位参数,跟之前一样,由于我们再调用回调函数的地方并没有值被传入括号中,它们被绑定为undefined。那么你认为此时参数this又将被绑定什么呢?想想我们应该查看哪里来决定参数this的绑定。并不是在函数的定义中,也不是在函数所处的对象中,而是在这个函数被调用的地方。由于我们在对象r中进行了属性查找,大家常会认为"查找"这个动作可能和函数中的关键词this有关。但其实此时是不相关的,因为只有在调用的时刻才会影响参数this的绑定。正如之前的示例,我们传入的是函数fn而非r.method。setTimeout的最后一行仍然是一个自由函数的调用,而非点记方法调用,因此绑定的内容仍然是默认值global。
    失去参数绑定的问题很普遍,是因为任何一个像setTimeout一样将另一个函数作为回调函数的函数,在调用函数时都可能与你的预期发生偏差。回调函数本身就被设计为,由它们被传入的系统调用,因此你基本无法控制传入的参数最终所绑定的内容。正因如此,每当你将一个函数作为参数传入另一个函数时,你需要对所以的参数绑定都保持谨慎,包括参数this。尽管在传入参数时,你看到有对象在点符号的左侧,但是当系统最终调用你的回调函数时,这个对象并不会被传入成为参数this的绑定。
    为了避免将参数绑定复杂化,可以采取传入另一个函数的方法,这个参数完全不接受任何参数,包括this,然后在这个函数体中腾出位置给你的自定义代码,在这个位置,引用你的方法并作出指示,传入你希望参数this绑定的内容。像这样

    setTimeout(function(){ r.method(g,b)},1000);
    

    那么还有一种情况,如果关键词出现在全局作用域,而非函数体内,又会是怎样的情况呢?我们知道在函数的作用域外访问函数作用域内定义的参数时,比如log(one);由于我们仅在函数fn内部定义了one,因此我们无法在全局作用域中访问它,因此这行代码会导致错误。由此可以推出this也是无法在全局作用域中访问的。 但是以前的时候this是可以通过全局作用域访问的,并且绑定为默认值全局对象global。所幸在新的语言规范中已经将这种设置删除,直接输出log(this)会是undefined。
    最后再讲一种绑定参数this的方式。这里我们在函数前面使用关键词new来调用它

    new r.method(g,b);
    

    这将会影响关键词this接受绑定的方式。这些定位参数自然不会受到关键词new的影响。此时关键词this将被绑定为一个在后台创建的全新对象。
    总结一下,关键词this使得我们可以仅创建一个函数对象,就可以将其作为方法用在一些其他的对象上,每次我们调用该方法时,它便可以访问任意使用它的对象,这对节省内存非常有用。而这都是因为我们能够访问参数this才得以实现。

    作者:长梦未央
    图片来源:优达学城付费课程
    个别文字摘自教学视频老师的讲解

    ** JavaScript

    相关文章

      网友评论

        本文标题:聊聊JavaScript核心之"this"关

        本文链接:https://www.haomeiwen.com/subject/guqspxtx.html