JS基础系列 - this对象

与其迷失在无止境的业务代码中,不如踏踏实实打好基础。(唔…这个逼装的可以…)


前言

刚接触学习JS的时候,总是会被其中的一些概念搞得晕头转向,蹂躏到不行,比如this对象、闭包、作用域、原型等等。。。今天就先拿this对象开刀,好好地捋一遍。


思考

首先,在搞明白this对象之前,我觉得有必要先问自己一个问题: 为什么有些时候我们分不清this对象指向的是什么?

对我而言,是因为最一开始学的时候,不同的情况下this对象的表现都不一样。而在印象中这种不同的情况太多了,又没有好好总结过,所以一旦碰到复杂的情况时就懵逼了。。。不过吧,今天一总结才发现总共也就3种情况,只要记住了这个规则,那真的是想忘都难。。。


分析与讨论

虽然印象中this对象的指向令人捉摸不定,但是总结过后会发现,其实也就只有以下3种情况:

  1. 作为对象的方法被调用;
  2. 作为普通方法被调用;
  3. new对象时作为构造器被调用

接下来,我们就将对每一种情况进行讨论与分析。


1. 作为对象的方法被调用

准则1: 当某个方法作为某个对象的方法被调用时,该方法中的this对象指的就是这个对象。

在日常写代码中,这种情况其实是最常见的,比如:

// 例1
var obj = {
    name: '小石头若海',
    sayHello: function() {
        console.log('Hello, my name is ' + this.name);
    }
};

obj.sayHello();                 // 输出 Hello, my name is 小石头若海

从上面这段代码的输出结果中我们可以看到,name的值是小石头若海,也就是obj.name。
这时,我们再回过头来看准则1,因为sayHello这个方法是被obj这个对象所调用的,所以sayHello方法中的this对象指向的就是obj,没毛病吧?正因为sayHello方法中的this对象就是obj,所以在输出的时候this.name获取到的值自然就是obj.name了。

在上面的例子中,我们是获取了this.name的值。其实只要this指向的是obj,我们还可以修改obj.name值,比如:

// 例2
var obj = {
    name: '小石头若海',
    changeName: function(name) {
        this.name = name;
    },
    sayHello: function() {
        console.log('Hello, my name is ' + this.name);
    }
};

obj.changeName('若海小石头');
obj.sayHello();                 // 输出 Hello, my name is 若海小石头

没有骗你吧,我们成功地修改了obj的name值。这一切只因为changeName是作为obj对象的方法被调用的,所以changeName中的this对象指向了obj,而改变this.name值就是改变obj.name值,最终sayHello的时候name值自然也就变了。

怎么样,准则1是不是很简单,瞬间就懂了?要是觉得懂了的话,那就看看下面的例3和例4:

// 例3
var obj = {
    name: '小石头若海',
    sayHello: function() {
        return function() {
            console.log('Hello, my name is ' + this.name);
        }
    }
};

obj.sayHello()();           // 输出 Hello, my name is undefined

// 例4
var obj = {
    name: '小石头若海',
    sayHello: function() {
        console.log('Hello, my name is ' + this.name);
    }
};

var name = '若海小石头';
var sayHello = obj.sayHello;
sayHello();                 // 输出 Hello, my name is 若海小石头

咋样?是不是和预想的结果不一样,如果不一样就对了。因为这两个例子根本就不符合准则1,而是接下来马上要讨论的准则2…


2. 作为普通方法被调用

准则2: 当某个普通方法被正常调用时,非严格模式下,该方法中的this对象指向window(浏览器环境中,其他环境中是全局对象); 严格模式下this对象是undefined。

注意上面的措辞,换一种说法就是: 当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式。这里要注意哦,匿名函数是不是没有调用者,所以在正常情况下,匿名函数中的this对象就是window(非严格模式且浏览器环境中)。接下来,我们再回过头来看上面的例3和例4。

// 例3
var obj = {
    name: '小石头若海',
    sayHello: function() {
        return function() {
            console.log('Hello, my name is ' + this.name);
        }
    }
};

obj.sayHello()();           // 输出 Hello, my name is undefined

在例3中,为什么最终的this.name是undefined呢?其实原因很简单,因为看最里层的function,这就是一个匿名函数,所以其中的this指向的就是window。而window下没有name的属性,所以不管外层怎么折腾,最终输出的就是undefined。

不过我们也可以从另一个角度来分析,obj.sayHello()()可以分成两步来看。第一步是执行obj.sayHello(),返回一个方法(由于是匿名函数,所以没有名字,但为了方便姑且叫它方法A);第二步是执行方法A,输出相应结果。这里需要注意的是,方法A并不是作为obj的方法被调用哦,可不要被obj.sayHello()()这种形式所欺骗就认为是obj调用了方法A,所以不符合准则1。相反地,这时候方法A应该是作为一个普通方法被调用,所以应该符合准则2。

// 例4
var obj = {
    name: '小石头若海',
    sayHello: function() {
        console.log('Hello, my name is ' + this.name);
    }
};

var name = '若海小石头';
var sayHello = obj.sayHello;
sayHello();                 // 输出 Hello, my name is 若海小石头

我们再来看例4,我们在全局的环境中定义了一个sayHello方法,并把obj.sayHello赋给了它。注意哦,在调用全局的这个sayHello方法的时候,我们同样没有给它指定调用者,所以算是作为普通方法被调用。这个时候this对象自然是指向全局的window对象了,所以最终输出this.name的时候实际上访问的就是window.name。


3. new对象时作为构造器被调用

准则3: 当某个方法作为构造方法被调用时,其内部的this对象指向新创建的这个对象。

虽然js不像java这类语言一样有类的概念,但是它也提供了关键字new来创建一个对象。就比如下面的这个例子,注意其中的this对象:

// 例5
function People() {
    this.name = '小石头若海';
    this.sayHello = function() {
        console.log('Hello, my name is ' + this.name);
    };
}

var xst = new People();
xst.sayHello();              // 输出 Hello, my name is 小石头若海

如上面的代码所示,xst是通过People构造方法new出来的一个对象。我们在People方法中把name和sayHello都挂在了this对象上,但是最后的结果这些属性全出现在了xst对象中。

这说明了什么?说明People方法最后返回的对象就是this,并把它赋值给了xst。注意哦,上面的代码中故意把最后的return this给省略了,但这并不影响最终的结果。这是因为默认return了this这个对象。那么我们可以改变return的值吗?这对xst又有什么影响呢?why not have a try?

// 例6
function People() {
  this.name = '小石头若海';
  this.sayHello = function() {
    console.log('Hello, my name is ' + this.name);
  };
  return null;
}

var xst = new People();
xst.sayHello();              // 输出 Hello, my name is 小石头若海

// 例7
function People() {

  this.name = '小石头若海';
  this.sayHello = function() {
    console.log('Hello, my name is ' + this.name);
  };

  return {
    name: '若海小石头',
    sayHello: function () {
      console.log('Hello, my name is ' + this.name);
    }
  };
}

var xst = new People();
xst.sayHello();              // 输出 Hello, my name is 若海小石头

对比例6和例7的结果,我们有了惊喜的发现。

  • what? 在例6中,我们明明return了一个null,那xst.sayHello()的时候不就是调用了null的sayHello方法吗?难道不是应该会报错吗?为什么输出的结果丝毫没有受到影响?原来,构造函数最后在返回的时候,如果发现你返回的是一个非对象的时候,会忽视你指定的返回值,仍然返回的是this。为啥要这么做呢?我是这样觉得的,因为通过new构造出来的最后不得是一个对象吗,但你指定返回的是一个非对象的值,那不是瞎搞吗。。。
  • 再来看看例7,这次无视前面做了什么操作,反正最后直接返回了一个对象,而从最终的输出结果来看,xst对应的就是我们return的这个对象。

所以,综合以上两个例子来看,并不是我们不能改变return的值,而是必须return一个对象,如果返回的不是对象,则会被忽略。

彩蛋

其实分析到这里,this对象的指向都已经介绍的差不多了,真的只有这3种情况。以后不管遇到什么情况,只要套到以上3种情况中去就再不会迷失方向了。不过,既然说是彩蛋了,那自然还有好货在文末了。

前面介绍的都是this对象的指向,其实还有很重要的一块没有讲,那就是apply, call, bind。因为这三个都可以改变方法在运行时的this指向。下面我们就分别介绍下这三种方法及其区别。

1. apply和call方法

为什么要把apply和call放在一起呢,因为这两者真的很像,只是使用方式略有不同罢了,功能都是改变方法内的this指向。话不多说,直接看下面的代码吧。

// 例8
function sayHello(scope, work) {
  console.log('[' + scope + '] Hello, my name is ' + this.name + ' and my work is ' + work);
}

var name = '全局';
sayHello('window', 'front-end');    // [window] Hello, my name is 全局 and my work is front-end

var obj1 = {
  name: 'obj1'
};
sayHello.call(obj1, 'obj1', 'front-end');     // [obj1] Hello, my name is obj1 and my work is front-end

var obj2 = {
  name: 'obj2'
};
sayHello.apply(obj2, ['obj2', 'front-end']);  // [obj2] Hello, my name is obj2 and my work is front-end

比较上面的三个输出,我们可以看到:

  1. 第一个输出是普通函数调用,符合准则2,this指向的是全局的window;
  2. 第二个输出用到了call,我们发现sayHello在调用的时候,this指向了obj1;
  3. 第三个输出用到了apply,我们发现sayHello在调用的时候,this指向了obj2。

call和apply都成功地改变了sayHello函数中的this指向,唯一的区别就是两者的传参形式发生了变化。仔细观察call的调用方式,第一个参数是指定的新this对象,而后面的参数就是要传进sayHello中的参数;而apply的调用方式稍有区别,它只有两个参数,第一个参数同样是指定的新this对象,第二个参数是一个数组,传进sayHello中的参数都被放进了这个数组中。

为什么要这么设定呢?这主要是适应不同的应用场景吧。有的时候call方便,有的时候apply方便。反正怎么方便就调用哪个,最终的效果都是一样的。

2.bind方法

bind是一个方法,它也能够改变一个函数中的this指向。和apply/call不同的地方在于:bind是返回一个改变了this的新的方法,而apply/call则是直接执行了这个新的方法。还是看下面的代码吧:

// 例9
function sayHello(scope, work) {
  console.log('[' + scope + '] Hello, my name is ' + this.name + ' and my work is ' + work);
}

var obj3 = {
  name: 'obj3'
};
var newSayHello = sayHello.bind(obj3);
newSayHello('obj3', 'front-end');       // [obj3] Hello, my name is obj3 and my work is front-end

通过上面的代码我们可以看到,sayHello.bind(obj3)的返回值赋给了newSayHello(其实就是一个方法),然后再正常调用newSayHello方法。其实我们也可以不用这么麻烦分两步来调用,可以直接用sayHello.bind(obj3)(‘obj3’, ‘front-end’)就可以了。

除此之外,其实只要有了apply和call,我们完全自己可以实现一个bind。

if(Function.prototype.bind === undefined) {

  Function.prototype.bind = function() {

    var _this = this;
    var ctx = arguments[0];
    var args = [].slice.call(arguments, 1);

    return function() {
      _this.apply(ctx, [].concat.call(args, [].slice.call(arguments)));
    }
  }
}

写在最后

本文主要介绍了this对象在各种场景中的指向。其实只要掌握了相应的规律,就可以轻松应对啦~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值