这一次彻底搞懂this

179 阅读5分钟

写在前面

this关键字可能是Javascript中最复杂的机制之一了,哪怕是对于非常有经验的前端开发者来说,都不一定能够完全说明白,在许多前端书籍中,比如《你不知道的Javascript上卷》等更是花了非常大的篇幅从原理上来讲述this的问题,但是很多看了书的人还是无法完全理解,更多的人最终还是死记硬背网上流传的一些this确定口诀,比如:

  • 全局环境执行函数时,this指向window
  • 调用对象的方法时,this指向该对象
  • 箭头函数的this指向外面的this
  • 用new调用函数时,this指向创建的对象
  • call/apply/bind可以修改this

这些口诀毫无疑问能够覆盖绝大部分场景,但是一旦遇到变形的问题,我们可能就无法完全确定this的指向了,尤其是在一些变态的js面试题中,通常能够把你绕晕,最后只能稀里糊涂地根据感觉来确定。那么,有没有一种方法能够完全让你不去了解this的底层原理,比如上下文、优先级等,就能够熟练地知道this的指向。 这就是本文的主要内容,讲述的是你没有听过的奇淫技巧,但是却非常高效地能够帮助你快速掌握this的方法。

从一道常见的编程题出发

let foo = function(){
    console.log(this);
}
let obj = {
    foo:foo
}

obj.foo()     // 打印出的this为obj
let bar = obj.foo;
bar();        // 打印出的this为window 
foo.call(obj) // 打印出的this为obj

这道题目很简单,很多人看到第一印象就是直接用口诀就能够得到this的值。但是求this的值很简单,更重要的是我们知道this是依赖于函数的调用的,上面的三种函数调用其实是JS中的三种函数调用方式:

fn(p)
obj.fn(p)
fn.call(context,p)

我们平常在开发过程中,用的最多的就是前两种方式,平常确定this时也是这两种方式最能够绕晕我们,但是对于第三种方式,它的this始终指向call后面的第一个参数context。那么我们是否可以思考把所有的函数调用都转化成第三种调用方式,这样的话,就无需去分析this了,而是直接找到call后面的第一个参数即可。

转化成fn.call(obj)调用方式

将其他两种方式等价地转化为call的形式

fn(p)  等价于
fn.call(undefined,p)

obj.fn(p)  等价于
obj.fn.call(obj)

至此,我们的函数只有一种调用方式

fn.call(context,p)

我们的this也始终是确定的了,就是call后面的第一个参数。

转换成obj.fn.call(obj)调用方式

上面我们确定了context就是this的值,但是这个context又是如何得到的了?
这时候,又需要另外的技巧了,我们都知道obj.fn.call(context),这种调用方式,大家都知道context就是obj,也就是说一个对象的函数通常会指向这个对象(就像很少有人会把你的房子产权弄成别人的吧)。因此我们是不是可以把所有的调用方式,都转换成obj.fn.call(obj),这样的话this就始终指向obj了。 转换方法:

我们首先判断函数前面是否有对象在调用他,即obj.fn()这种形式,如果有直接转化成call调用的形式即可。

obj.fn.call(obj)

如果没有人调用,直接是fn(),那么就在前面添加undefined,同时call的第一个参数是undefined,最终转换成

undefined.fn.call(undefined)

这时候的this就指向undefined,但是在严格模式下,浏览器默认如果thisundefined,那么this会指向window总结:到目前为止,我们的函数就只有一种调用方式了,即obj.fn.call(obj),所有的函数都可以转换成这种调用方式。 进一步将上面的两种函数调用方式进行转换:

fn(p)  等价于
undefined.fn.call(undefined,p)   // 如果函数前面没有谁进行调用,那么context就是undefined

obj.fn(p)  等价于
obj.fn.call(obj)  // 函数的调用就是obj,那么context就是obj

转换代码求解

接下来我们用我们的方式转换代码,再看这道编程题目:

let foo = function(){
    console.log(this);
}
let obj = {
    foo:foo
}

obj.foo()     

根据上面的分析,我们可以将obj.foo()转换为:

obj.foo.call(obj)

那么,很容易就确定this指向obj。同理:

let foo = function(){
    console.log(this);
}
let obj = {
    foo:foo
}

let bar = obj.foo;
bar();        // 打印出的this为window 

我们可以将bar()转换成:

undefined.bar.call(undefined)

那么,可以确定this指向undefined,在浏览器中如果this指向undefined,那么会默认指向window。因此,这里的this就是window了。 你看,通过这种简单的转化我们就能够很简单地得到this的值。但是可能有人会疑惑这种情况对于其他的情况能够适用吗?比如一些特殊的情况,如数组,复杂的函数。因此,接下来我们看一下数组和复杂的函数如何进行转换。

数组转换

function fn (){ console.log(this) }
var arr = [fn1, fn2]
arr[0]() // 这里面的 this 又是什么呢?

我们可以看到实际上arr0就是数组arr的第一个元素是函数,然后调用函数,那么我们可不可以转换成:

arr.[0].call(arr)   // 对应于obj.fn.call(arr)

那么我们很容易就可以确定,这里的this就是指向数组arr。

闭包

let obj = {
  number: 3,
  db1: (function () {
    console.log(this);
    this.number *= 4;  
    return function () {
      console.log(this);
      this.number *= 5;    
    };
  })()
};

对于闭包,我们关键还是看调用时的函数是谁,然后改写这个函数。

let fn = function () {
    console.log(this);
    this.number *= 4;  
    return function () {
      console.log(this);
      this.number *= 5;    
    };
  }
let obj = {
  number: 3,
  db1: (fn)()
};

因此,我们可以看到最终执行的函数就是fn,因此我们将fn改写成undefined.fn.call(undefined),因此,这个this还是指向window。

总结

本文主要讲述了一种奇淫技巧(不被业内认可的),用来确定this的指向问题,即将所有的函数调用都转换成:

Obj.fn.call(obj)

最终的this就是你call一个函数时传入的第一个参数。通过这种方式无论多么复杂的this问题,都可以很清楚地进行确定。

最后欢迎大家使用这种方式去尝试做各种编程题,如有问题大家一起讨论。

完结撒花。

参考

方应杭