JavaScript中的this

来梳理一下JavaScript中this的脉络

一. this的概念

在Java中,this的概念很明确:指的就是该类对象,并可以通过this来操纵对象属性,比如:

  • 案例一

    class A{
        private int age;    
        public int getAge(int age){
            return this.age;
        }
        //...构造函数等
    }
    //***** 
    A a = new A(18).getAge(17); //result: 18
    

    由于创建一个对象是一个开辟内存空间的操作,所以一个对象的this是不可以修改的。甚至于我会冒出一句话:一个对象与它的this......

但是在JavaScript中,不同于Java中类与对象的概念,它更加强调于函数与对象的概念,所以我们要探讨的是函数中的this指向。

  • 案例二

    global.value = 2;
    var add = function(a, b) {
        return (a + b);
    };
    
    var myObject = {
        value:1,
        sum: function() {
            // this.value: 1
            function helper() {
                // this.value: 2
                return add(this.value,this.value);
            }
            return helper();
        }
    };
    
    console.log(myObject.sum()); //result: 4
    

    为什么结果不是2? 可能我们第一个想到的问题是为什么不是2,而后才会去想为什么会是4。因为这里的this似乎更像是指向的myObject,而this.value也应该指向的是myObject.value。

想要解答这个问题,我们需要对JavaScript中的this进行更深层次的探讨

二、函数中this的探讨

2. 1 决定this对象绑定的因素

我们将以上代码进行修改以更好的进行探讨

  • 案例三

    global.value = 2;
    var add = function(a, b) {
        return (a + b);
    };
    
    var myObject = {
        value:1,
        sum: function() {
            console.log(this.value);  //this.value: 1
            let that = this;
            function helper() {
                console.log(this.value); //this.value: 2  this改变了!!!
                return add(that.value,that.value);
            }
            return helper();
        }
    };
    
    console.log(myObject.sum()); //result: 2
    

    一个很重要的现象:this改变了!,或者我们可以用一个更严谨的语言:this绑定的对象改变了!

这个现象向我们证明了一件事:函数中的this不是固定的,它不像Java中那样在一个类中的this永远指向创建它的对象。结合我们上一篇作用域的知识,JavaScript引擎的两个阶段:

  1. 编译阶段
  2. 执行阶段

可以得出结论:函数中this绑定对象的确定是在执行阶段!

所以函数中this的对象绑定必然和该函数的执行密切相关。然而函数的执行也远远不是我们所想的那般简单,但总结一下就是在哪如何被执行,将其拆解就是两个重要信息

  1. 函数的调用位置

    在程序中的哪个位置执行,或者说在哪个位置被调用?我们将这个执行位置称为函数调用位置

  2. 函数的调用方式

    函数是怎么被调用的?是独立调用还是被其它对象调用?

2.2 寻找规律

我们已经知道了this对象绑定的决定性因素,现在我们对其进行尝试来寻找this对象绑定的规律。

第一条因素:函数的调用位置

执行是this绑定的先决条件,但是在哪调用也很重要,举个例子

  • 案例四

    global.a = 2;
    global.b = 2;
    function sum() {
        return this.a + this.b;
    }
    
    console.log(sum()); //result: 4
    global.a = 3;
    console.log(sum()); //result: 5
    

    可以看出调用位置的重要性,因为绑定的契机是函数调用而不是函数声明

当然执行或者调用也尤为重要,这也会引出我们后续会遇到的问题:多次的执行或者调用函数会使得该函数this绑定的对象不断改变,也就是this绑定对象的对象丢失问题。

第二条因素:函数的调用方式
1. 独立调用(默认绑定)
  • 案例五

    lobal.a = 2;
    global.b = 2;
    function sum() {
        return this.a+this.b;
    }
    console.log(sum()); //result: 4  //this绑定的对象是全局global!
    

    或许单看这个案例感受并不明显,因为没有其它元素的干扰,我们可以向上观察案例三,"单节点"helper()执行时this绑定的对象也是全局global。

由此我们可以得出:独立的函数调用this绑定的对象是全局global

当然也有例外:在函数声明使用严格模式的情况下,独立的函数调用this绑定的对象是undefined

  • 案例六

    function foo() {
        "use strict"; //在声明中使用严格模式无法将this绑定到全局
      console.log(this); // undefined
        console.log(this.a); //TypeError: Cannot read property 'a' of undefined
    }
    
    global a = 2;
    foo(); //报错
    

我们将这种函数独立调用的this绑定方式称为:默认绑定

2. 被其它对象调用(隐式绑定)

首先我们可以向上观察案例三,当中的myObject.sum()的操作后,函数sum的this被绑定到了myObject中,我们可以对这个代码进行扩展来进行规律探索

  • 案例七

    global.value = 2;
    const add = function(a, b) {
        return (a + b);
    };
    
    const inner = {
        value: 1,
        sum: function() {
            // this.value: 1
            return add(this.value, this.value);
        }
    };
    
    const outer = {
        value: 10, // this.value: 10
        inner: inner
    };
    
    console.log(outer.inner.sum()); //result: 2
    

    可以看到最终函数sum中的this还是绑定到了对象inner上。

由以上可以得出:被其它对象调用的函数会将该函数的this绑定到调用它的对象。

我们将这种this绑定方式称为:隐式绑定

而且我们也可以由outer.inner.sum()的this绑定结果知道隐式绑定的绑定优先级高于默认绑定,因为显示sum在这里其实也被体现了,但是最终的结果还是偏向于隐式绑定。

2.3 打破规律

1. 规律的本质

事实上,以上我们所摸索出的规律也不过只是规律罢了,如果我们探索其本质不过还是一个内存指针问题。

譬如:

  1. 默认绑定不过是因为它实际运行的区域是在全局,所以this指向的也是全局地址。

  2. 隐式绑定不过是因为它是被一个对象调用,运行的区域在对象,所以this指向的也是对象地址。

2. 使用apply和call打破规律(显示绑定)
  • 函数call的官方定义:

    function.call(thisArg, arg1, arg2, ...)
    

    thisArg:可选的:在 function 函数运行时使用的 this 值。

    arg1, arg2, ...:指定的参数列表。

  • 函数apply的官方定义:

    func.apply(thisArg, [argsArray])  
    

    thisArg:必选的。在 func 函数运行时使用的 this 值。

    argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。

由于我们可以明确的指定this绑定的对象,所以它又称为显示绑定

那么callapply这么做的目的是什么?难道是为了去修改this绑定而去修改this绑定?这显然是不合理的。

事实上它是有实际的存在意义的。不过在介绍意义之前我们得介绍一个概念:

“类似数组”arguments

函数被调用时,会获得一个“免费”配送的参数=>“类似数组”arguments,它接收了该函数的参数列表里的所有参数,并存有参数长度length。我们可以通过arguments来访问这些参数。

  • 案例八

    现在我们要根据argumens来设计一个函数,这个函数的功能是:返回传入的最大数,如果这个数不大于我们预先设定好的某个值则返回这个值。

    我们很容易就能想到以下方案:

    function getMax() {
        const Min_Max = 60;
        const result = Math.max(1, 2, 3);
        if (result <= Min_Max) {
            return Min_Max;
        } else {
            return result;
        }
    }
    console.log(getMax()); //result: 60
    

    问题这甚至连个健康的代码都算不上!因为它的输入参数从一开始就是写死的,这种代码可以说是毫无灵活性。那么导致它失去灵活性的原因是什么?我们来观察下Math.max的官方定义:

    Math.max(value1[,value2, ...]) 
    

    value1, value2, ...:一组数值

    可以看到,它的参数只能是一个一个的单个数值,我们可以试想一下,如果Math.max能接收数组参数并返回该数组内的最大值。那是不是能提高代码质量,举个错误的例子

    // 此为错误代码!!! 仅举例衍生
    function getMax() {
        const Min_Max = 60;
        const arr = new Array(arguments); //用法错误!!!
        arr.push(Min_Max);
        return Math.max(arr); //用法错误!!!
    }
    console.log(getMax(1,2,3)); //result: 60
    

    上述代码语法层面是错误的,但却代表了我们的美好展望,因为从这和前一个代码比较起来简直灵活很多了。要实现这个美好展望,我们需要解决两个问题

    1. arguments 如何转化为数组
    2. Math.max如何参数接收数组

幸运的是,apply能够解决这两个问题:

function getMax() {
    const Min_Max = 60;
    //因为arguments是一个“类似数组”而不是一个数组结构
    //所以我们需要将它转化成数组然后进行数组操作
    const arr = Array.prototype.slice.apply(arguments); //arguments:1,2,3
    arr.push(Min_Max);
    return Math.max.apply(this,arr);
}
console.log(getMax(1,2,3)); //result: 60

这里apply的作用显示的淋漓尽致。极大的利用到了函数Math.max本身的特质,精简了代码逻辑。如果你还有不懂,可以看我们对它的进一步剖析。

  • 问题一的解答:

    Array.prototype.slice.apply(arguments);是如何将“类似数组”转化成数组结构?其实我们通过2.3中apply的定义就已经知道apply会将函数slice中的this绑定到arguments上,但是仅仅这些我们可能还是不太能理解这个过程。

    对此,我自己实现了一下函数slice:(源码与此有很大不同,点此链接查看源码)

    Array.prototype._slice = function(start, end) {
        var result = new Array();
        start = start || 0;
        end = end || this.length;
        for (let i = start; i < end; i++) {
            result.push(this[i]);
        }
        return result;
    };
    
    let arr = [1,2,3,4];
    console.log(arr._slice(2)); // result: [3,4]
    

    怎么样,现在是不是就很好理解了,其实整个的转换数组分两个步骤:

    1. 将新数组中的this绑定到arguments

    2. 遍历this(也就是arguments)中的变量生成新数组

  • 问题二的解答:

    得益于apply的定义,apply直接就能将数组向下传递给max的arguments,所以这个问题也迎刃而解。

这就是apply中关于this的妙用,其实相应的call也能达到相同的效果。

2. ES6的进阶

其实综合整个案例八,最大的痛点还是这个arguments,如果arguments从一开始就是个数组,我们也无需进行这么繁琐的转换数组操作了。

于是在ES6中有了对于函数的新扩展:rest参数数组的扩展运算符

  • rest参数

    ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

  • 数组的扩展运算符

    扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

    现在我们来重写一下案例八:

  • 案例九

    function getMax(...args) { //...args 为rest参数,传入时直接为数组
        const Min_Max = 60;
        args.push(Min_Max);
        return Math.max(...args);//数组的扩展运算符传参
    }
    
    //普通传参
    const result = getMax(1,2,3);
    //数组的扩展运算符传参
    const result = getMax(...[1,2,3]);
    //result赋值二选一
    console.log((result)); // result:60
    

    怎么样,是不是方便了很多。

2.4 this的补充

new 中的this绑定

可能你会觉得我前三种形式已经把所有this绑定的情况说完了,但事实上,不要忘了本质,this绑定的本质在于函数在哪如何被执行,构造函数的调用也属于这个范畴。并且这也是我们生活中普遍用到的一种this绑定方式

  • 案例十

    function hello() { 
        console.log(`hello${this.name}`);
    }
    function Obj(name){
        this.name = name;
    }
    Obj.prototype.intorduce = hello;
    
    const pig = new Obj('大哥');
    const dog = new Obj('小弟');
    
    pig.intorduce(); // hello大哥
    dog.intorduce(); // hello小弟
    

    没错就是这样,可能乍一看会很容易理解,并且使用上也不会出现纰漏,但其实在这个new的过程中会涉及到一些JavaScript对象原型的知识。

    比如说上述:const person = new Obj('大哥',hello)

    我们将这个过程分为以下几个步骤:

    1. 在我们对一个构造函数使用new关键字时,javaScript在执行阶段执行到该语句时会根据这个函数创建一个对象。

    2. 随后这个对象会和函数的原型进行连接。

    3. 随后会把该构造函数调用的this指向该对象,并执行函数内相应逻辑

    4. 构造函数将这个对象返回

由此,我们得以改变了构造函数中的this指向以达成自己构建对象的目的

而且由于我们在第二步中对象同函数进行了原型连接,所以在上述案例中被同构造函数构造出的对象都能共享introduce方法,而不需要在每个对象中都去创建这个函数导致无谓的内存损耗。

那么为什么普通变量没有置于该函数的原型中呢?原因很简单,如果在这个个函数的原型中存放普通变量,那它就会成为一个所有对象的公有变量,但是问题在于,由于每个对象都可以像获取这个变量一样去轻而易举的改变这个变量,以至于它也不能被当作一个公共常量存在。所以它存在的意义几乎没有什么意义。

那为什么上述中的函数hello要置于构造函数的原型中呢?因为函数相对相比于一个变量而言更加灵活,事实上这也是封装函数的意义所在,即:重复的逻辑,不同的结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容