写在前面
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
,但是在严格模式下,浏览器默认如果this
是undefined
,那么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问题,都可以很清楚地进行确定。
最后欢迎大家使用这种方式去尝试做各种编程题,如有问题大家一起讨论。
完结撒花。