引言
关于this是面试和日常开发中非常常见的概念之一,也是最易弄混的概念之一,this这个名称本身有时也容易让人迷惑。自己之前面试时在this上也是栽过几次跟头,特地梳理一下,目的是彻底吃透,以绝后患。
常见错误理解
-
this是指向自身吗?
特别是在函数中使用this的时候,this是指的所在的这个函数对象吗?看下面示例:
function test() { console.log(this.name); } test.name='aaaa'; test();在上面这个示例里如果this是指向当前函数的话,执行test后是不是应该输出'aaaa',但实际上输出的是空字符串,因为此时this指向的是window(在浏览器里执行),所以this并不是来指向自身的。(输出的具体原因下面再分析)
-
this指向函数的作用域吗
这个我们同样可以通过代码来验证下,如下:
function test() { var name = 'bbbbb' console.log(this.name); } test();执行后发现输出的仍然是空字符串,原因和上面一样,this没有指向当前函数的作用域。但this一定会不指向当前函数作用域吗?也不一定,只需知道不能根据所在函数作用域来确定this的指向就对了,应该是确定this的指向,再确定是不是当前函数的作用域。
解决掉常见的理解错误后,我们看下this其实是在运行时(即被调用时)进行绑定的,并不是在声明时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。确定this指向的步骤应该是“确定调用位置->应用规则->确定this指向”。
寻找调用位置
this既然是函数调用是才绑定的,那么需要首先需要确定函数的调用位置。这个一般是比较容易的,先确认调用栈,然后当前调用栈的前一个就是调用位置了。示例如下:
function a() {
// 调用栈是c->b->a, 调用位置b
console.log(a);
}
function b() {
// 调用栈是c->b, 调用位置c
a()
}
function c() {
// 调用栈是c, 调用位置是全局作用域
b();
}
c(); // c的调用位置
应用绑定规则
确定调用位置后,需要应this的绑定规则,有四种绑定规则,判断条件如下:
默认绑定
一般可以理解为无法应用其他规则时的兜底默认规则,独立函数调用时一般适用。
function test(){
console.log(this.a);
}
var a = 'aaa'; // 或者window.a='aaa'
test(); // 输出2
上面这种方式便是默认绑定,test()不在任何对象内的独立调用,适用于默认绑定,默认绑定this指向的全局对象,在浏览器里面就是window,在node里面就是global, ps:杨哥模式下,全局对象无法使用默认绑定,默认绑定会绑定到undefined上
隐式绑定
隐式绑定存在于在调用位置有上下文对象或者说调用时被对象包含或拥有,示例如下:
const obj = {
name: 'oooo',
say: function(){
console.log(this.name);
}
}
obj.say();// oooo
看上面函数say的调用,不是say单独调用,而是被对象obj包含着调用,此时this是指向obj对象的。
隐式丢失
有一种情况是看似应该是隐式绑定,但实际却是默认绑定,有两个栗子如下:
栗子one:
var name = 'globallll';
var obj = {
name: 'oooo',
say: function(){
console.log(this.name);
}
}
var copy = obj.say;
copy();// globallll
栗子two:
var name = 'globallll';
var obj = {
name: 'oooo',
say: function(){
console.log(this.name);
}
}
function b(func){
func();
}
b(obj.say);// globallll
看起来say函数的确是obj对象的一部分呀,但为什么看起来this是指向的window呢?《you don't know JavaScript》里面把这种特殊对待称为是隐式丢失,但我理解是这种情况是不满足隐式函数绑定的,因为隐式函数绑定应当是调用是被对象包含着调用,而不是说只要是对象的其中一部分就可以了,重点在于调用时是否被函数包含着!
我们来看下上述的两个例子,第一个是把obj里的函数say的引用赋值给copy变量,再通过copy来调用,copy调用时并没有被obj包含着调用,这就适用默认绑定规则--独立函数调用,因此此时this是指向window的。第二个例子同理,只不过看起来是调用的obj.say(),但实际过程是:
func = obj.say;
func();
和第一个一样都有一个赋值的过程。
显式绑定
首先为啥需要显示绑定呢?因为以上两个规则会导致this的指向不稳定,但有时我们需要函数中this稳定指向某个对象的。比如下面这个:
var obj = {
name: 'ooooo',
say: function(){
console.log(this.name);
}
}
var name = 'globalllll';
setTimeout(obj.say, 1000); // globalllll
这个例子中我们其实是想让this指向obj然后输出‘ooooo’的,但实际上调用的过程中首先进行了赋值然后进行了调用,导致使用默认绑定,this指向了window。为了能固定this的绑定,才有了显示绑定。
显示绑定有三种方式:apply,call和bind。这三个函数大家应该用的比较多了,其中apply和call只有传参的区别,而apply和bind的区别在于apply绑定后立即执行,而bind可以返回绑定后的函数。上述例子可以这么解决:
var obj = {
name: 'ooooo',
say: function(){
console.log(this.name);
}
}
var test = obj.say.bind(obj);//绑定后this指向不可修改
//或者
//var test = function (){
// obj.say.call(obj);
//}
var name = 'globalllll';
setTimeout(test, 1000); // oooo
思考🤔:如何用apply或者call实现bind?
进阶:实现apply?
new绑定
new是一个由类新建示例的过程。使用new时会调用构造函数,但是过程中并不是实例化了一些类,而是通过新建一个对象,然后执行原型的链接,新对象绑定函数调用的this,返回这个对象,返回的这个对象我们就称为一个实例。比如: function Person(name){ this.name = name; } var student = new Person('LiMing'); console.log(student.name) // LiMing
上述代码的过程是:
- 新建一个对象
- 执行原型的链接(不是本文的重点)
- 将新建对象绑定到Person中的this上,也就是目前this指向新建对象
- 返回这个新建对象给student,即student等于新建对象
这种在new的过程中绑定this的方式称为是new绑定。
优先级
上面我们了解四种绑定规则,但问题是如果符合其中一种以上的情形时应该如果确定是哪种呢?这就要确定四种规则的优先级了。优先级如下: new 绑定>显式绑定>隐式绑定>默认绑定。
特殊情况
凡事有例外,this也一样。下面介绍下特殊情况。
箭头函数
es6中引入的箭头函数虽然也叫函数,但是却不适用于上面的四规则。先引入一个🌰:
function a(){
return ()=>{
console.log(this.name);
}
}
const obj1={
name: 11111,
}
const obj2={
name: 22222,
}
var test = a.call(obj1);
test.call(obj2); // 11111
上面例子中箭头函数理论上绑定的是obj2,但是实际输出的却是11111。所以箭头函数是不适用于上面的四规则的。箭头函数的具体规则时:箭头函数this是在声明时就确定了,其this就是声明时所在作用域的this确定的。比如上面的例子,箭头函数是在a函数中声明的,所以箭头函数中所用的this就是a的this,而a中的this是根据调用位置和规则确定是obj1,所以箭头函数中this也是指向obj1。
结语
面试时有很多问题都是考察this的,题目虽然变化万千,但是真正掌握了原理,还是能够做到胸有成竹,迎刃而解的。最近找工作面试的人比较多,分享出来希望能帮到大家!