理解掌握JavaScript的this的指向

学了JavaScript将近一年了,对于this的掌握却还是不太熟悉,甚至有时候都不知道它到底指向哪里,在看了《你不知道的JavaScript上》中关于this的指向的解析,对this的指向认识清晰了不少,接着又在网上查阅了一些资料,写下这篇博客来分享我对this的指向的理解。如果有什么不对请各位大佬指出我的错误。

在此之前我对this的理解是函数作用域中指向该函数,在对象中使用就指向该对象,但实际上,this真正的指向需要知道它所使用的位置,即它的调用位置。下面介绍this的四条绑定规则。(这四条规则的优先级是默认绑定<隐式绑定<显示绑定<new绑定,这里就不做优先级比较,下面介绍完后各位可以尝试优先级是否如上所说)

默认绑定


默认绑定在平时的使用中非常常见,我的理解是this的调用的位置在全局运行环境中,而全局环境下的this会指向全局对象(在使用Web Worker加载的文件中,this指向Web Worker对象),如下面这段代码。

function fn(){
    var a=1;
    console.log(this.a);
}
var a=2;
fn()
// 2

上面这行代码,可能会让人误以为会打印出1,但实际上却是打印出了2。这是因为this对象是在全局作用域下调用的,所以实际上调用了全局作用域下的变量a。要注意的是,如果在严格模式下的话,this会指向undefined,所以上面的代码在严格模式中是会报错的。

function fn(){
    'use strict'
    var a=1;
    console.log(this.a);
}
var a=2;
fn()
// VM2278:4 Uncaught TypeError: Cannot read property 'a' of undefined at fn

在判断为不是其他几种绑定形式时,就可以视为是默认绑定了。

隐式绑定


隐式绑定考虑的是调用位置是否有上下文对象,或者是否被某个对象拥有或包含。

(上下文对象包含变量对象作用域链this指针,如果调用位置有上下文对象的话,this指向其this指针指向的位置)

隐式绑定主要有下面几种情况,作为对象时的方法,原型链上的调用,getter和setter中的this以及作为一个DOM事件处理函数。

作为对象的方法

var obj={
    a:1,
    fn(){
        console.log(this.a);
    }
}
var a=2;
obj.fn()

// 1

这里要注意的是,最后的结果之所以是1,是因为调用时是对象obj调用了fn方法,并非是因为方法定义在obj对象中,可以用下面的代码来看。

var obj={
    a:1,
    fn(){
        console.log(this.a);
    }
}

var obj1={
    a:3,
    fn:obj.fn
}

obj1.fn();

// 3

在这段代码中可以看到,虽然方法fn定义在对象obj里面,但是是用obj1来调用方法fn的,所以最后this指向的是调用该方法的对象obj1。

如果在使用对象链式调用方法时,方法中的this指向链的最后一个对象。如下代码

function fn() {
    console.log(this.a);
}

var obj1 = {
    a: 1,
    fn: fn
}

var obj2 = {
    a: 2,
    obj1: obj1
}

console.log(obj2.obj1.fn());

// 1

可以看到,最后的this是指向对象链最后的obj1。若obj1中没有属性a的话,最后的结果会返回undefined。

原型链上的调用

原型链上的this指向的判断也是一样的,关键在于this是在哪里调用的,看看下面这段代码

function fn() {
    console.log(this.a);
}

var obj1 = {
    a: 1,
    fn: fn
}

var obj2 = Object.create(obj1);

obj2.a = 2;

console.log(obj2.fn());

//2

在这段代码中,方法fn虽然定义在对象obj1上,但是调用时是在obj2对象上调用的,所以调用的是obj2对象上的a。而这样实际上和上面的对象的调用是一样的,不一样的地方在于,如果这里的对象obj2没有属性a的话,会沿着原型链寻找属性a,所以下面这段代码最后会打印出1。

function fn() {
    console.log(this.a);
}

var obj1 = {
    a: 1,
    fn: fn
}

var obj2 = Object.create(obj1);


console.log(obj2.fn());

// 1

getter和setter中的this

如果将this写在getter和setter的方法中的话,this会指向getter和setter方法所属的对象。

var obj = {
    n: 1,
    get num() {
        return this.n;
    }
}

console.log(obj.num);

// 1

如上面代码,对象obj获取其属性num,调用了其getter方法,所以this指向了对象obj,最后返回了对象obj的属性n的值。setter也是同样的道理。

var obj = {
    n: 1,
    set num(val) {
        this.n = val;
    }
}

console.log(obj.n);
// 1

obj.num = 10;

console.log(obj.n);
// 10

DOM事件处理函数

在DOM事件处理函数上的this对象指向触发该事件的元素。

<body>
    <p id="dom">DOM</p>
</body>
<script>
    var p = document.getElementById('dom');

    function fn() {
        console.log(this.innerHTML);
    }

    p.onclick = fn;
    // DOM
</script>

上面代码中,点击了p标签后就会在控制台打印出DOM,这是因为this指向了p。

 

关于隐式绑定,我的理解是在使用时没有明确指出this指向哪个对象,通过在某个对象上调用函数,将该调用函数中的this指向做出调用这个操作的对象。

而隐式调用在某些情况下会出现隐式丢失的情况,即失去了对对象的绑定。如下代码

function fn() {
    console.log(this.a);
}

var obj = {
    a: 1,
    fn: fn
}

var f = obj.fn;

var a = 2;

f();
// 2

这里的f=obj.fn,看起来好像是在obj对象中调用该方法的,但仔细看一下就会发现,这条语句是将该函数fn赋值给变量f,即这里的f实际上严格等于fn,即是在全局环境下调用了fn,所以这其实是默认绑定的情况。setTimeout第一个参数传入函数名时也会出现这种情况。

function fn() {
    console.log(this.a);
}

var a = 1;

var obj = {
    a: 2,
    fn: fn
}

setTimeout(obj.fn, 100)
// 1
//等同于
setTimeout(function fn(){
    console.log(this.a);
},100)

显式绑定


显式绑定,顾名思义就是显式地将绑定写出来,与隐式绑定不同,显式绑定会明确地将要绑定的对象写出来。显式绑定主要和call()apply()bind()三个方法密切相关,而事实上这三个方法的第一个参数就是要来绑定this对象的。

不考虑其后面的参数,这三个方法在this绑定上是一样的,我们就call方法来进行解析。

function fn() {
    console.log(this.a);
}

var obj = {
    a: 1
}

var a = 2;

fn.call(obj);

// 2

如上代码,使用call方法显式地将this对象指向对象obj,所以最后打印出的是obj对象的属性a的值。像这种绑定的方式称为硬绑定,硬绑定的缺点在于会降低函数的灵活性,要绑定的对象必须显式地写出来,而且使用硬绑定之后就没法再使用隐式绑定和硬绑定来修改this对象的指向了。

function fn() {
    console.log(this.a);
}

var obj = {
    a: 1
}

var a = 2;

var obj1 = {
    a: 3,
    fn: fn.call(obj)
}

obj1.fn;// 尝试使用obj1隐式绑定

// 1  还是原来的硬绑定

function fn1() {
    fn.call(obj);
}

fn1.call(window);// 尝试使重用硬绑定绑定到全局

// 1  还是原来的硬绑定

对于这些硬绑定的缺点,JavaScript有软绑定来解决这些问题,下面用《你不知道的JavaScript上》里面的部分代码来解析

下面是软绑定softBind()的方法

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        var curried= Array.prototype.slice.call(arguments, 1);//获取绑定对象外的参数
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?obj : this, 
                //检查是否为undefined或者全局对象(默认绑定),是则将默认对象obj绑定到this,否则不修改this
                curried.concat.apply(curried, arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    };
}

软绑定关键在于若调用this时绑定到全局或者undefined(默认绑定),则将其绑定到之前传入的默认绑定对象,若不是默认绑定的情况,则不修改this,即使this绑定到新的对象上。

function foo() {
    console.log(this.name);
}

var obj1 = { name: "obj1" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind(obj1);
fooOBJ();// obj1 使用软绑定绑定,因为当前this为默认绑定的情况,所以this指向了传入的对象obj1

obj2.foo = foo.softBind(obj1);
obj2.foo();// obj2  使用隐式绑定成功修改this绑定的对象

fooOBJ.call(obj3);// obj3  使用硬绑定成功修改this绑定的对象

setTimeout(obj2.foo, 1000);// obj1  与上面的fooOBJ()一样,这里等同于默认绑定的情况,所以this指向了传入的对象obj1

new绑定


new绑定出现在使用new操作符构造对象的时候,它会将函数中的this指向构建的新对象。

function fn(val) {
    this.a = val;
}

var obj = new fn(1);

console.log(obj.a);

上面代码中,方法fn将传入的参数作为this指向的对象的属性a的值,而通过new运算符的构建,使this对象指向obj,所以obj的属性a被传入值1。

 

判断this指向


综上所述,绑定this的指向就分为以下几步

1.判断是否是new运算符创建的,如果是的话则指向新创建的对象

2.判断是否是显式绑定的(apply,call,bind或其他API调用的“上下文”),如果是则指向指定的对象

3.判断是否为隐式绑定的(在某个上下文对象中调用),如果是则指向该上下文对象

4.如果以上情况都不是的话,则为默认绑定,严格模式下指向undefined,非严格模式下指向全局

除了上面这几步外,还有例外情况,即ES6中的箭头函数,箭头函数比起普通函数更容易写,但存在的问题是箭头函数内的this会由外部的作用域来决定。

function fn() {
    return (a) => {
        console.log(this.a);// this继承自fn的this
    }
}

var obj1 = {
    a: 1
}

var obj2 = {
    a: 2
}

var o = fn.call(obj1);
o.call(obj2);
// 1

上面代码中,fn返回的箭头函数中的this实际上继承自fn中的this,所以在后面的代码中,fn绑定obj1,返回的箭头函数绑定obj2,最后打印出的是1,就是因为实际上指向的是fn绑定的对象。

 

以上即为我对JavaScript的this对象指向的理解,希望对大家有所帮助,也希望有大佬能指出我的不足。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值