【JS】JS中的this绑定规则总结

学习《你不知道的JavaScript》中关于this的讲解,然后总结而来,有理解不对的地方,请大家指正!

前言

1. 对this的误解

1.1 误解一——this指向自身

我们先看一段代码:

function foo(num){
    console.log("foo:"+num);

    //记录foo被调用的次数
    this.count++;
}

foo.count = 0;
var i;

for(i=0;i<10;i++){
    if(i>5){
        foo(i);
    }
}

//foo:6
//foo:7
//foo:8
//foo:9

//foo被调用了多少次
console.log(foo.count); //0

从代码来看,foo(..)确实被调用了4次,因为console.log语句有4条输出,但是foo.count仍然是0。显然,从字面意思理解this指向自身是错误的!

执行foo.count=0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向那个 函数对象,所以虽然属性名相同,根对象却不相同。为什么呢?

1.2 误解二——this指向函数的作用域

这个问题有点复杂,因为某种情况下它是正确的,但是在其他情况下它是错误的。这个我们将在接下来进行讲解。

1.3 this到底是什么

  1. this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
  2. 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

正文

1. 函数调用位置

我们先看一段代码,就能大体了解什么是调用栈和调用位置了

function baz() {
       //当前调用栈是:baz
       //因此,当前调用位置是全局作用域

       console.log("baz");
       bar();   //  <--bar调用位置
   }

   function bar() {
       //当前调用栈是:baz->bar
       //因此,当前调用位置在baz中

       console.log("bar");
       foo();   //<--foo调用位置
   }

   function foo() {
       //当前调用栈是:baz->bar->foo
       //因此,当前调用位置在bar中

       console.log("foo");
   }

   baz();   //<--baz调用位置

注意是如何(从调用栈中)分析出真正的调用位置的,因为它决定了this的绑定。

接下来,本文的重点终于要到来啦!this的绑定规则到底是怎样的呢,我们感觉来看看吧!

2. this的绑定规则

要想知道this的绑定对象,我们必须要找到调用位置,然后才能判断需要用到this的哪条绑定规则。而this有4条绑定规则,分别为:
1. 默认绑定
2. 隐式绑定
3. 显示绑定
4. new绑定
接下来,我们分别来介绍这4条绑定规则。然后再介绍多条规则都可用时这4条绑定规则的优先级顺序。

2.1 默认绑定

当一个函数没有明确的调用对象的时候,也就是单纯作为独立函数调用的时候,将对函数的this使用默认绑定:绑定到全局的window对象
看如下例子:

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

   var a = 2;
   foo();   //2

这个例子相信对大多数人来说都很简单啦,下面我们看一个更具迷惑性的例子:

function foo () {
       var a = 1;
       // 我是被定义在函数内部的函数哦!
       function innerFoo() {
           console.log(this.a);
       }
       innerFoo(); // 独立函数调用
   }
   var a = 2;
   foo(); // 2

函数 innerFoo在一个外部函数foo里面声明且调用,那么它的this是指向谁呢?
答:仍然是全局对象window
所以最后输出了2,而不是1。

在这里,很多人可能会考虑foo函数的作用域对innerFoo 函数的影响,从而得出了this的错误绑定。
我们只要记住一点:没有明确的调用对象时,将对函数的this使用默认绑定——绑定到全局的window对象
这样我们就不会搞错this的绑定了。

通过上面的梳理,我们再来看个例子,做个检验

var obj = {
        a: 1,
        foo: function () {
            function innerFoo() {
                console.log(this.a);
            }
            innerFoo();   // 独立函数调用
        }
    };
    var a = 2;
    obj.foo(); //输出 2

【注意】上面这个例子中的obj.foo()用到了this的隐式绑定。接下来我们就来看隐式绑定问题。

【总结】凡是函数作为独立函数调用,无论它的位置在哪里,它的行为表现,都和直接在全局环境中调用无异

2.2 隐式绑定

当函数调用位置有上下文对象,或者说函数被某个对象拥有或者包含的时候,我们称函数的this被隐式绑定到这个对象里面了,这时候,通过this可以直接访问所绑定的对象里面的其他属性。

我们看如下的两段代码:

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

    var obj = {
        a: 1,
        foo: foo
    };

    obj.foo(); // 输出1
 var obj = {
    a: 1,
    foo: function() {
        console.log(this.a);
     }
 };

obj.foo(); // 输出1

foo函数并不会因为它被定义在obj对象的内部和外部而有任何区别,也就是说在上述隐式绑定的两种形式下,foo通过this还是可以访问到obj内的a属性,这告诉我们:
1. this是动态绑定的,或者说是在代码运行期绑定而不是在书写期
2. 函数于对象的独立性, this的传递丢失问题

this的隐式丢失问题

看下面的代码:

function foo() {
        console.log(this.a)
    }
var obj = {
    a: 1,    // a是定义在对象obj中的属性   1
    foo: foo
};

var a = 2;  // a是定义在全局环境中的变量    2
var bar = obj.foo;
bar(); //  输出 2

虽然barobj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用(即:独立函数调用),因此应用了默认绑定。

再看一个更微妙,更常见,更出乎意料的情况:

function foo() {
    console.log(this.a)
}

function doFoo(fn){
    //fn其实引用的是foo
    fn();
}
var obj = {
   a: 1,    // a是定义在对象obj中的属性   1
   foo: foo
};

var a = 2;  // a是定义在全局环境中的变量    2
doFoo(obj.foo); // 2

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。

如果我们把函数传入语言内置的函数而不是传入自己声明的函数,会发生什么呢?结果是一样的,没有区别。

function foo() {
    console.log(this.a)
}

var obj = {
   a: 1,    // a是定义在对象obj中的属性   1
   foo: foo
};

var a = 2;  // a是定义在全局环境中的变量    2
setTimeOut(obj.foo,100);    // 2

正如我们所见,回调函数丢失this绑定非常常见。

在一串对象属性链中,this绑定的是最内层的对象

在隐式绑定中,如果函数调用位置是在一串对象属性链中,this绑定的是最内层的对象。
举例来说:

var obj = {
      a: 1,
      obj2: {
           a: 2,
           obj3: {
                a:3,
                getA: function () {
                    console.log(this.a)   
                 }
           }
       }
}

obj.obj2.obj3.getA();  // 输出3

2.3 显示绑定

上面this隐式绑定存在绑定丢失的问题,也就是var bar = obj.foobar()调用和obj.foo()的结果是不同的,因为这个函数别名的赋值无法把foo所绑定的this也传递过去。这时候,call(...)apply(...)方法就派上用场啦。

call的基本使用方式: fn.call(object)
 fn是你调用的函数,object参数是你希望函数的this所绑定的对象。
fn.call(object)的作用:
 1. 即刻调用这个函数fn;
 2. 调用这个函数的时候函数的this绑定object对象

因为我们可以直接指定this的绑定对象,因此我们称之为显示绑定。

看如下代码:

var obj = {
      a: 1,    // a是定义在对象obj中的属性
      foo: function () {
         console.log(this.a)
      }
}

var a = 2;  // a是定义在全局环境中的变量  
var foo= obj.foo;
foo();   // 输出2
foo.call(obj); // 输出1
2.3.1 硬绑定

但是,我们其实不太喜欢这种每次调用都要依赖call的方式,我们更希望:能够一次性 返回一个this被永久绑定到objfoo函数,这样我们就不必每次调用foo都要在尾巴上加上call那么麻烦了.

聪明如你,一定可以想到,在foo.call(obj)外面创建一个包裹函数不就ok了嘛!

var obj = {
      a: 1,    // a是定义在对象obj中的属性
      foo: function () {
         console.log(this.a)
      }
}

var a = 2;  // a是定义在全局环境中的变量  
var foo= obj.foo;
//硬绑定
var bar = function(){
    foo.call(obj);
}

bar();  //  输出1

还有种更简单的方式——使用bind()方法
可以将:

var bar = function(){
    foo.call(obj);
}

简化为:

var bar = foo.bind(obj);

注: 在绑定this到对象参数时,callbind的区别是:
1. call将立即执行该函数
2. bind不执行函数,只返回一个可供执行的函数,它会把参数设置为this的上下文并调用原始函数

2.3.2 API调用的“上下文”

第三方库的许多函数,以及JS语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”,其作用和bind(…)一样,确保你的回调函数使用指定的this。

举例来说:

function foo(el) {
        console.log(el, this.id);
    }
    var obj = {
        id: "awesome"
    };

    //调用foo(...)时把this绑定到obj
    [1,2,3].forEach(foo, obj);
    // 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过call(…)或apply(…)实现了显示绑定。

2.4 new绑定

在讲解new绑定之前,先说明下JS 中的“构造函数”
在JS中,构造函数只是一些使用new操作符时被调用的函数。它们不会属于某个类,也不会实例化一个类。它们知识被new操作符调用的普通函数而已。

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
 1. 创建(或者说构造)一个全新的对象
 2. 这个新对象会被执行[[原型]]连接
 3. 这个新对象会绑定到函数调用的this
 4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

举例来看:

function foo(a){
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo(…)时,我们会构造一个新对象并把它绑定到foo(…)调用中的this上。

this绑定规则的优先级

看如下代码:

function foo() {
        console.log(this.a);
    }
    var obj1 = {
        a: 2,
        foo: foo
    };

    var obj2 = {
        a: 3,
        foo: foo
    };
    //隐式绑定
    obj1.foo();// 2
    obj2.foo();// 3
    //显示绑定
    obj1.foo.call(obj2);// 3
    obj2.foo.call(obj1);// 2

可以看出,显示绑定优先级比隐式绑定高。

再来确定new绑定和隐式绑定谁的优先级更高:

function foo(something) {
        this.a = something;
    }
    var obj1 = {
        foo: foo
    };

    var obj2 = {};

    //隐式绑定
    obj1.foo(2);
    console.log(obj1.a);// 2

    //显示绑定
    obj1.foo.call(obj2,3);
    console.log(obj2.a);// 3

    //new绑定
    var bar = new obj1.foo(4);
    console.log(obj1.a);// 2
    console.log(bar.a);// 4

可以看出,new绑定优先级比隐式绑定高。

接下来,我们再来看看显示绑定和new绑定谁的优先级更高

function foo(something) {
        this.a = something;
    }
    var obj1 = {};

    //硬绑定
    var bar = foo.bind(obj1);
    bar(2);
    console.log(obj1.a);// 2

    //new绑定
    var baz = new bar(3);
    console.log(obj1.a);// 2
    console.log(baz.a);// 3

new修改了硬绑定(到obj1的)调用bar(...)中的this
以上代码说明,new绑定的优先级高于显示绑定

this绑定规则的总结

如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象。

  1. 函数是否在new中调用?是的话,this绑定到新创建的对象。var bar = new foo();
  2. 函数是否通过由call、apply(显示绑定)或者硬绑定调用?是的话,this绑定到指定的对象。var bar = foo.call(obj2);
  3. 函数是否在某个上下文对象中调用(隐式调用)?是的话,this绑定到那个上下文对象。var bar = obj1.foo();
  4. 如果都不是的话,使用默认绑定:在严格模式下绑定到undefined,否则绑定到全局对象。var bar = foo();

例外情况

凡是都有例外的情况嘛,我们的this绑定规则也一样。下面我们总结下this的绑定例外情况。

1. 被忽略的this

如果你把null或者indefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

function foo(){
    consile.log(this.a);
}

var a = 2;
foo.call(null);// 2

问题来了,什么时候会传入null呢?

一种常见的做法是使用apply(…)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(…)可以对参数进行柯里化(预先设置一些参数),看如下代码:

function foo(){
    consile.log("a:"+a+",b:"+b);
}

//数组“展开”成参数
foo.apply(null,[2,3]);// a:2 b:3

//使用bind(...)进行柯里化
var bar = foo.bind(null,2);
bar(3);// a:2 b:3

这两种方法都需要传入一个参数当作this的绑定对象。如果函数并不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择。

然而问题来了,总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象,这将导致不可预计的后果。

更安全的this

“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对程序产生任何副作用。
这个特殊的对象是一个空的非委托的对象,任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。
举例来说:

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}

//我们的空对象
var a = Object.create(null);

//把数组展开成参数
foo.apply(a, [2,3]);//a:2 b:3

//使用bind(...)进行柯里化
varbar = foo.bind(a,2);
bar(3);//a:2 b:3

2. 间接引用

另一个需要注意的是,我们可能有意无意中就创建了一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。
看如下代码:

function foo(a,b){
    console.log(this.a);
}

var a=2;
var o={a:3,foo:foo};
var p={a:4};

o.foo();// 3
(p.foo = o.foo)();// 2

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo(),因此会应用默认绑定。

3. 软绑定

前面我们学习了,硬绑定可以把this强制绑定到指定的对象(除了使用new时),防止函数调用应用默认绑定规则。那么问题来了,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this

解决方法:
如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。

可以通过一种被称为软绑定的方法来实现我们想要的效果:

if(!Function.prototype.softBind){
        Function.prototype.softBind = function (obj) {
            var fn = this;
            //捕获所有curried参数
            var curried = [].slice.call(arguments,1);
            var bound = function () {
                return fn.apply(
                    (!this || this===(window||global))?
                        obj : this,
                    curried.concat.apply(curried, arguments)
                );
            };
            bound.prototype = Object.create(fn.prototype);
            return bound;
        };
    }

下面我们举例来看,softBind是否实现了软绑定功能:

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

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

    var fooOBJ = foo.softBind(obj);
    fooOBJ();//name: obj

    obj2.foo = foo.softBind(obj);
    obj2.foo();// name: obj2

    fooOBJ.call(obj3);// name: obj3

    setTimeout(obj2.foo, 10);  // name:obj  应用了软绑定

特殊情况

ES6中的箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this

首先我们来看下箭头函数的词法作用域:

function foo() {
       //返回一个箭头函数
       return (a) => {
           // this 继承自foo()
           console.log(this.a);
       };
   }

   var obj1 = {
       a:2
   };

   var obj2 = {
       a:3
   };

   var bar = foo.call(obj1);
   bar.call(obj2);//2,不是3!

原因是:
foo()内部创建的箭头函数会捕获调用时foo()this。由于foo()this绑定到obj1bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

箭头函数的应用:
箭头函数最常用于回调函数中,例如时间处理器或者定时器:

function foo() {
       setTimeout(() => {
           //这里的this在此法上继承自foo()的this绑定
           console.log(this.a);
       },100);
   }

   var obj = {
       a:2
   };

   foo.call(obj); // 2

this小结

如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象。

  1. 由new调用?绑定到新创建的对象。
  2. 由call或者apply(或者bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。

需要注意的是:有些调用可能无意中使用默认绑定规则。注意使用非委托的空对象来保护全局对象。

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前代码中的self = this机制一样。


参考资料

  1. 《你不知道的JavaScript》
  2. http://www.cnblogs.com/penghuwan/p/7356210.html
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值