JavaScript中的this指向


this 是JavaScript中很重要的一个指针,但是往往也是最容易产生bug 的地方,因为稍不留神this的指向就和你以为的指向根本不是一个对象,让多数新手懵逼,部分老手觉得恶心,这是因为this的绑定 [难以捉摸],出错的时候还往往不知道为什么,相当反逻辑。熟悉Java的对this应该很熟悉,在Java中tthis在编码阶段不能确定出它指向谁,而是在运行时确定,指向的是当前调用它的对象。那么在JavaScript中这句话同样适用,记住一句话。在JavaSript中this指向的是最后调用它的那个对象,这句话在大部分场景是对的,但是还有几种特殊情况会改变this的指向。本文对this指向的所有情况做一个总结

默认绑定

分几种情况谈论下

普通函数调用

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

// 结果
undefined

foo()的这个写法熟悉吗,就是我们刚刚写的默认绑定,代码中直接调用了foo()其等价于 window.foo()调用,因此foo方法中的this指向的是window对象,等价于打印window.a,而代码中没有定义全局变量a,故输出undefined,如果写成下面这样。就会输出全局变量5

function foo() {
    console.log( this.a );
} 
var obj = {
    a: 10,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = 5; // a是全局对象的属性


bar(); // 5
obj.foo();// 10

//等价的两个函数,输出不一样的结果。为什么?
//其实var bar === window.bar; 它隐式绑定了window 对象,所以它访问到的自然是 window.a

对于普通调用有一种情况会存在this指向丢失,那就是回调,看下面这个例子

function foo() {
console.log( this.a );
}
function doFoo(fn) {
    // fn其实引用的是foo
    fn(); // <-- 调用位置!
} 
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo( obj.foo ); // "oops, global"

注意function有自己的函数作用域,function默认绑定的父级作用域,而在这个例子中传入的是foo()方法,foo()此时属于window作用域的方法,因此调用它的是window对象

总结:对于普通的函数调用,实际上等价于window.foo()。在浏览器中最外层的对象就只能是window,因此可以简写成foo()调用。对于回调函数则要注意找到回调函数的父级作用域

对象函数调用

情况一:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。如下情况,o.fn()调用时,fn()指向对象o。因此输出对象o中的user变量值

var o = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
        console.log(this); 
    }
}
o.fn(); 

// 结果
追梦子
{user: "追梦子", fn: ƒ}

情况二:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,形成调用链,this指向的是最后一个调用它的对象,如下情况,o.b.fn()尽管fn调用链有两个对象o和b,但是fn指向的是最后一个调用它的对象,因此fn指向对象b,而在对象b中没有变量a,因此输出undefined

var o = {
    a:10,
    b:{
        //a:12,
        fn:function(){
            console.log(this.a); //undefind  有两个对象b和o,所以此this.a指向它的上一级
        }
    },
    fn1:function(){
        console.log(this.a);  //10 
    }
}
o.b.fn();

// 结果
undefined

构造函数调用

对于普通调用和对象调用,都可以总结成一句话 this指向的是最后调用它的那个对象。但在JS中还有一种特殊的调用,学过面向对象的小伙伴对new肯定不陌生,JS的new和传统的面向对象语言的new的作用都是创建一个新的对象,但是他们的机制完全不同。创建一个新对象少不了一个概念,那就是构造函数,传统的面向对象 构造函数 是类里的一种特殊函数,要创建对象时使用new 类名()的形式去调用类中的构造函数,而JS中就不一样了。在JS中的只要用new修饰的 函数就是’构造函数’,准确来说是 函数的构造调用,因为在JS中并不存在所谓的 [构造函数]。那么用new 做到函数的构造调用后,JS做了什么呢

  1. 创建一个新对象。
  2. 把这个新对象的__proto__属性指向 原函数的prototype属性。(即继承原函数的原型)
  3. 将这个新对象绑定到 此函数的this上
  4. 返回新对象,如果这个函数没有返回其他对象

对于函数通过构造调用this的指向问题,分为两种情况。

情况一:这种情况仍然适用于this指向的是最后调用它的那个对象,这种方式也是new方式的常规用法,代码如下

function foo(){
    this.a = 10;
    console.log(this);
}
foo();                    // window对象
console.log(window.a);    // 10   默认绑定

var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名
                          // 然后等价于 foo { a : 10 };  var obj = foo;
console.log(obj.a);       // 10    new绑定

情况二:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。下面列举了五种返回对象和返回值的情况

// 1.
function fn()  
{  
    this.user = '追梦子';  
    return {};  
}
var a = new fn;  
console.log(a.user); //undefined

// 2.
function fn()  
{  
    this.user = '追梦子';  
    return function(){};
}
var a = new fn;  
console.log(a.user); //undefined

// 3.
function fn()  
{  
    this.user = '追梦子';  
    return 1;
}
var a = new fn;  
console.log(a.user); //追梦子

// 4.
function fn()  
{  
    this.user = '追梦子';  
    return undefined;
}
var a = new fn;  
console.log(a.user); //追梦子

// 5.
function fn()  
{  
    this.user = '追梦子';  
    return null;
}
var a = new fn;  
console.log(a.user); //追梦子

显示绑定

在JS中提供了几个方法可以给函数强制性绑定this。它们就是callapplybind三个方法。这里我们就要用到 js 给我们提供的函数 call 和 apply,它们的作用都是改变函数的this指向第一个参数都是 设置this对象。两个函数的区别:

  1. call从第二个参数开始所有的参数都是 原函数的参数。
  2. apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。

举个例子

function foo(a,b){
    console.log(a+b);
}
foo.call(null,'海洋','饼干');        // 海洋饼干  这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] );     // 海洋饼干

bind函数有点特殊,在React中常常用bind方法为组件绑定事件 ,它和call,apply都不同。使用bind函数为对象绑定方法时,不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回,例如

function foo(){
    console.log(this.a);
}
var obj = { a : 10 };

foo = foo.bind(obj);
foo();                    // 10

总结: call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别

箭头函数绑定

ES6 提供了箭头函数,增加了我们的开发效率,但是在箭头函数里面,没有 this ,箭头函数里面的 this 是继承外面的环境,即箭头函数绑定规则不是上面介绍的规则,而是完全根据外部作用域来决定this,且绑定无法被修改。因此箭头函数的this对象判断比较简单也比较特殊,举个例子

let obj={
    a:222,
    fn:function(){    
        setTimeout(function(){console.log(this.a)})
    }
};
obj.fn();//undefined

不难发现,虽然 fn() 里面的 this是指向 obj ,但是,传给 setTimeout 的是普通函数, this 指向是 window , 但是window下面没有 a ,所以这里输出 undefined。换成箭头函数

let obj={
    a:222,
    fn:function(){    
        setTimeout(()=>{console.log(this.a)});
    }
};
obj.fn();//222

这次输出 222 是因为,传给 setTimeout 的是箭头函数,然后箭头函数里面没有 this ,所以要向上一层作用域查找,在这个例子上, setTimeout 的上层作用域是 fn。而 fn 里面的 this 指向 obj ,所以 setTimeout 里面的箭头函数的 this ,指向 obj 。所以输出 222

再举个例子,使用普通调用函数和箭头函数对比一下。下面是普通调用

var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

很明显调用bar的是window,而window中没有Name变量,故输出undefined,如果改成箭头函数,如下

var people = {
    Name: "海洋饼干",
    getName : function(){
        return ()=>{
            console.log(this.Name);
        }
    }
};
var bar = people.getName(); //获得一个永远指向people的函数,不用想this了,岂不是美滋滋?

bar();    // 海洋饼干 

getName()方法永远指向外一层对象people(箭头函数比较特殊,并不需要在运行时判断是谁调用它,而是在代码阶段就能判断出this指向一定是外一层调用对象),可能会有人不解为什么在箭头函数外面再套一层,直接写不就行了吗,搞这么麻烦干嘛,其实这也是箭头函数很多人用不好的地方。改成如下

var obj= {
    bar : function(){
        return ()=>{
            console.log(this);
        }
    },
    baz : ()=>{
        console.log(this);
    }
}

obj.bar()();            // obj
obj.baz();              // window

为什么obj.baz()方法输出的是window

  1. 我们先要搞清楚一点,obj的当前作用域是window
  2. 如果不用function(function有自己的函数作用域)将其包裹起来,那么baz()是箭头函数,寻找外一层调用对象,那就只能是window了。(箭头函数不会绑定函数所在的作用对象,而是向上找一层调用对象)
  3. 用function包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this,即obj。

最终总结

对于this的指向问题,大部分情况可以用文章开头那句话适用this指向的是最后调用它的那个对象。对于少部分几种情况,就是使用new调用方法时,方法存在返回值需特别注意一下,还有就是箭头函数中不存在this对象,需要继续向外面一层的上下文寻找this指向,最后一点就是函数的回调会改变this的执行。

其实最终所有的this指向的还是最后调用它的那个对象,只不过这个调用对象在有些情况下和我们预期的不一致,让我们摸不着头脑。所以还是把JS的基础打扎实就不会有这些疑惑了,哈哈。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
广度优先搜索算法BFS 广度优先搜索算法BFS
最近在看到力扣上一道推箱子算法题时,思考了很久也想不到解题思路。于是查了一下推箱子的解题思路,便查到了广度优先搜索算法(BFS),广度优先搜索算法专门用于解决最短路径问题,其算法思路也很简单。而推箱子问题就可以用广度搜索算法解决,与它类似的
2020-05-27
Next 
大前端跨平台技术 大前端跨平台技术
在移动互联网时代,任何一个公司的产品都少不了APP,这是人们生活水平提高的结果。随着智能手机越来越普及,移动端成为兵家必争之地。正所谓“得移动端者得天下”,移动端已成为互联网领域最大的流量分发入口,一大批互联网公司正是在这大趋势下崛起。但在
2020-05-23
  TOC