JS中this指向问题

96 阅读3分钟

什么是this指向?

  首先我们需要明确的一点是,this指向它是在运行时绑定的,并不是在编写的时候绑定,this的绑定和函数声明没有任何关系(箭头函数除外),只取决于函数的调用时机。

  当一个函数被调用的时候,会创建一个记录(有的地方也称做函数执行上下文),这个记录会包含函数在哪里调用(调用栈)、函数的调用方式、传入的参数信息等。this就是这个记录当中的一个属性。用来确定是哪个对象调用的这个函数!!!

  我们先来看一段代码来感受一下:

function foo(num){
    this.count++;
    console.log("foo:",num);
}
foo.count=0;
for(let i=0;i<5;i++){
    foo(i);
}
//可以看到们利用了for循环调用了函数五次
//输出:
//foo:0
//foo:1
//foo:2
//foo:3
//foo:4
//我们再来看看foo.count是多少
console.log(foo.count);//0----?????

  首先,我们可以看到for循环,确实调用了foo函数5次,产生了5条输出,但是却并没有改变foo.count的值。这是因为在foo函数里面,this指向的并不是foo,而是指向的window,改变的是window.count。(这里的window.countNaN,和预编译有关系,有兴趣的可以自行了解一下,本文章不涉及)。

绑定规则

  关于this指向大致可以分为四种绑定规则。

默认绑定

  首先介绍一种最常用的默认绑定规则:独立函数调用。这种情况会默认绑定当前函数作用域的this指向。

例如:

function foo(){
    console.log(this.a);
}
var a=0;
foo();//0
console.log(this);//window

  这里我们可以注意到我们用var a=0;声明了一个a的变量并给了一个初始值0,如果在看的你对预编译有了解的话,这其实就是相当于给window对象添加了一个a变量,然后我们再看函数是如何调用的,foo被直接调用,并没有通过其他对象对象调用,所以当JS引擎执行foo函数是,this会默认绑定调用位置的this指向。因为当前的函数作用域为全局作用域,所以this指向window,最后一行console已验证。(一般情况下,函数直接调用,就是指向window)。

  需要注意的是,默认规则绑定到window需要一个前提条件,就是需要在非严格模式下。在严格模式下,不能将this默认绑定成window,会指向undefined

隐式绑定

这种绑定规则就是看是谁调用的,那么this就是指向的谁。我们来看一下例子:

function foo(){
    console.log(this.name);
}
const obj1={
    name:'obj1'
    foo:foo
};
const obj2={
    name:'obj2',
    foo:foo,
    obj1:obj1
}
var name='window';
var bar=obj1.foo;
obj1.foo();//obj1
obj2.foo();//obj2
obj2.obj1.foo();//obj1
bar();//window

  首先我们可以看到var name='window';,相当于是给window对象添加一个name属性,这一点需要知悉,然后var bar=obj1.foo;,相当于是foo的函数地址赋值给bar了,来看第一行函数执行obj1.foo();,相当是obj1对象去调用foo函数,根据隐式绑定规则,this指向的是obj1,输出obj1obj2.foo();同理,我们再来看,obj2.obj1.foo();相当于是调用obj2里面obj1对象,然后再去调用foo函数,等价于obj1.foo(),所以输出obj1。再看最后一行,我们可以看到,obj1.foo的函数地址赋值给了bar,而调用bar,是不是相当于函数独立调用?也就是等价于foo(),而这不就是相当于我们刚刚提到的第一条默认绑定规则吗?所以理所应当输出于window

显示绑定

  在我们刚刚提到的两点规则,其实都是相当于是间接把this绑定到一个对象里面,现在有一个问题,就是,假如我们需要强制一个对象调用一个函数该怎么做呢?

  在函数原型链(prototype)上有三种方法,分别是callapplybind,他们的第一个参数都是一个对象,也就是你需要绑定的this指向,函数传参方式略有不同,想了解的读者可自行了解,本文不涉及。因为这种方法是认为绑定函数运行时的this指向。因此也被称为显示绑定。

  接下来我们来看一个例子:

function foo(){
    console.log(this);
}
var obj={
    name:'obj',
    foo:foo
}
var obj1={
    name:'obj1',
    foo:foo
}
foo.call(obj);//obj
obj.foo.call(obj1);//obj1
obj1.foo.call(obj);//obj
obj.foo.call(1);//Number {1}
var a=foo.bind(1);
var b=a.bind(2);
var c=b.bind(3);
c();//Number {1}

  通过obj.foo.call(obj1)可以看出来显示绑定规则是要优先于隐式绑定规则的。假如使用call等强制绑定函数,第一个参数传入一个基本数据类型是会被包装成一个对象的。因为bind函数会包装一个函数,并且假如这个函数已经被强制绑定了一个this指向,再进行绑定是不起作用的。通过c()的执行可以看出来。

new绑定

  这是第四条里面的最后一条this绑定规则,探讨new 绑定规则,我们需要了解一下,new关键字做了什么事情。new关键字调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象(空对象)。
  2. 这个新对象会被执行prototype链接,比如{}.__proto__=foo.prototype
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象。那么new表达式的函数调用会自动返回这个新对象。

  我们来看一段代码:

function foo(){
    console.log(this);
}
new foo();//{}
var obj={ name:obj };
var b=foo.bind(obj);
new b();//{}

  可以看到。new绑定规则是要优先于显示绑定规则。自此我们得到一个结论。绑定规则优先顺序:new绑定>显示绑定>隐式绑定>默认绑定

  最后我们来介绍另外特殊的情况,箭头函数。它是ES6新定义的一种函数类型,它特殊在什么地方呢?箭头函数本身没有this,这一点一定一定一定要清楚,箭头函数的this只它的父作用域有关。因为它本身没有this,但是在函数里面如果使用了this,那么它就只能去父级作用域去获取this指向。而且四种绑定规则都对箭头函数不起作用!!!箭头函数this只和父级作用域的this有关系。我们来看一段代码:

function foo(){
    return ()=>{
        console.log(this);
    }
}
var obj={
    name:'obj',
    bar:()=>{
        console.log(this);
    },
    foo:foo
}
obj.bar();//window 隐式绑定规则无效 
obj.foo()();//obj 默认绑定规则无效
foo().call(obj);//window 显示绑定规则无效
var a=foo();
new a();//type Error 报错!!new 不能构造箭头函数

  是不是觉得很有意思?我们来分析一下!obj.bar()这句函数执行相当于拿到obj对象里面的箭头函数来进行调用,但是我们之前说过,箭头函数没有this指向,它只会去获取父级作用域的this指向。而此时的父级作用域是window!!!所以指向window。再看obj.foo()(),这里相当于拿到了foo函数的返回值(箭头函数)来进行调用,而这个箭头函数的父级作用域是在哪?是在foo函数里面,而foo函数的this指向是obj,所以箭头函数this指向也是obj。再看foo().call(obj)foo()相当于独立函数调用(默认绑定规则),所以它的this指向为window,而箭头函数的this指向只和父级作用域有关(也就是foo函数内的this指向),所以也为window,最后,new关键字不能构造箭头函数,语法错误!!!箭头函数不能作为构造函数!!!

  我们来看一道终极题目,如果能够理解,恭喜,你对this指向已经基本掌握。

var name='window';
var obj1={
    name:'1',
    fn1:function (){
        console.log(this.name);
    },
    fn2:() => console.log(this.name),
    fn3:function (){
        return function(){
            consoloe.log(this.name);
        }
    },
    fn4:function (){
        return ()=> console.log(this.name);
    }
}
var obj2={
    name:'2'
}
obj1.fn1();
obj1.fn1.call(obj2);

obj1.fn2();
obj1.fn2.call(obj2);

obj1.fn3()();
obj1.fn3().call(obj2);
obj1.fn3.call(obj2)();

obj1.fn4()();
obj1.fn4().call(obj2);
obj1.fn4.call(obj2)();

答案为:1、2、window、window、window、2、window、1、1、2