了解this
this是什么
this是js 中的一个关键字,也叫调用上下文会自动定义在所有函数的作用域中,他不是一个变量,所以this不能通过赋值语句自定义,即this不能被左查询,只能通过this获取绑定的对象。
this和哪个对象进行绑定完全属于内部操作,绑定的结果取决于this的调用位置或者说调用方式,而不是函数声明所处的位置。this在运行时绑定,这一点this有点像动态作用域,而之前探讨的函数作用域属于静态作用域。
当一个函数被调用的时候,会创建一个活动记录,有时候也称为执行上下文。这个记录会包含函数在哪里调用(调用栈)、函数的调用方法、传入参数等信息。
可以这么说,this指向的是当前对象,谁调用就指向谁。我们通过this,可以获取一个调用者的对象。
为啥要用this获取
this可以给我们提供一个调用者的对象,那么问题来了,我们想用这个对象,为啥要通过this来获取,别的方式拿不到吗?this获取有啥好处呢?
首先第一个问题:别的方式拿不到吗?我们想想别的获取方式:通过参数传递进来或者通过作用域链从外层获取!是,的确有很多方式!但是这种方式必须我们显式的把对象的引用传递进来,而this,可以隐式的,悄悄地传递!不用通过参数、不用通过变量维护就可以传递进来。
第二个问题,好处,选用理由!用官方的语言,好几本书上都看到这样的描述:如果使用显示传递调用上下文,会让代码变得混乱且难以维护,this提供了一种优雅的方式避免了这种问题,使得API的设计更加的简洁和易于复用。
如果你了解对象和原型,你会知道自动的引用合适的上下文有多重要。一句话,this可以自动(隐式)引用到合适的上下文,其他方式需要手动(显示)传递。
看看下面的代码!虽然代码看起来好像改动不是很大,但这只是一个小小的数据返回,如果我们的函数封装了大量的业务逻辑呢?显示传递的参数是不是每次也要维护呢?
//使用this隐式传递对象引用
function getName() {
return this.name;
}
//使用参数显示传递对象引用
function getName(context) {
return context.name;
}
1.3 关于this的一些误解
误以为this指向自身
foo() {
this.count++;
}
foo.count = 0;
foo();
alert(foo.count) //0
上面这段代码如果按照this指向自身的说法,应该输出1,但是事实上却输出了0。什么原因呢?
由于this并不是指向了自身的foo,因此this.count并不是这种想法所预期的foo.count,所以foo.count并没有改变!
那this指向了哪里了呢,这就是我们这篇文章的重点!接着往下看,小小的透露一下,这里的this指向的的window,所以window.count 实际上创建了一个全局的变量,并且执行完后加加操作之后,变成了NAN。当然啦,这个并不是这篇文章的重点,想知道为什么可以看看之前的文章。
如果你觉得和你的预期不符,然后开始采用上文的两种显示传递(参数,外层变量),这实际上是跳过了this的问题,采用了词法作用域的方式,忽略了真正的问题所在。
所以那些想要通过this调用函数自身的想法,是不正确的。如果你想调用函数自身,可以通过指向函数自身的词法标识符来引用他,也就是函数名!当然匿名函数就没有咯。也可以用arguments.callee来指向函数自身,但是这种方式几乎要废弃了呢!所以我们使用foo标识符代替this,也可以解决上边的问题呢。当然,了解this的人可能会知道,可以强制绑定this达到效果。
一句话,this并不是指向函数本身的。
第二种误解是this指向函数的作用域,我们应该明确一点,this 在任何情况下都不指向函数的词法作用域。词法作用域对象在js引擎的内部,尽管可见的标识符是他的属性,但我们依然无法通过代码访问作用域对象本身。
上代码。
function fun1() {
var a = 1;
this.fun2();
}
function fun2() {
alert(this.a)
}
fun1()
上面这段代码的错误不止一个!
1、试图通过this.fun2 调用fun2函数,这是不会成功的!可以直接省略!之后解释原因。
2、试图通过this.a访问fun1的词法作用域,相当于连通1和2的词法作用域,无法实现。
一句话,你不能使用this来引用一个词法作用域内部的东西。
调用位置
上文中我们提到,this和哪个对象进行绑定完全属于内部操作,绑定的结果取决于this的调用位置或者说调用方式,那么我们首先理解一下调用位置或者说函数的调用方式。
寻找调用位置就是寻找函数被调用的位置,但是做起来有时候没那么简单,因为有些编程模式可能会隐藏真正的调用位置。分析调用位置的关键在于分析函数的调用栈,我们关心的调用位置就在当前函数的前一个调用栈中。
上代码,告诉你什么是调用位置和调用栈。
function fun1() {
//当前调用栈fun1
alert('fun1');
//fun2的调用位置:fun1中,调用栈:fun1
fun2();
}
function fun2() {
//当前调用栈fun1>fun2
alert('fun2');
//fun3的调用位置:fun2中,调用栈:fun1>fun2
fun3();
}
function fun3() {
//当前调用栈fun1>fun2>fun3
alert('fun3');
//fun4的调用位置:fun3中,调用栈:fun1>fun2>fun3
fun4();
}
function fun4() {
//当前调用栈fun1>fun2>fun3>fun4
alert('fun4');
}
//fun1的调用位置:全局作用域 调用栈:window
fun1();
绑定规则
默认绑定
这条规则是的函数调用类型:独立调用函数。this指向全局变量window!我们也可以把它看作是无法应用其他规则时的默认规则。其实在我们介绍调用位置时,里边的所有函数调用的this都是默认绑定,他们的this都会指向全局的window。
注意,这个规则只是适用在严格模式下,严格模式下,因为全局对象无法使用默认绑定,this指向undefined。严格模式下的与函数的调用位置无关。通常,严格与非严格模式不能混用的。
隐式绑定
隐式绑定的规则是使用了上下文对象调用。this指向这个上下文对象。
上代码。
function fun1() {
alert(this.a);
}
var obj = {a:1, foo:fun1};
//foo的调用位置,输出1
obj.foo()
我们发现foo的调用方式是通过obj对象的属性引用来调用的!他不是普通的调用方式。也就是说该函数使用obj上下文来引用的。此时隐式绑定规则将this绑定给了obj对象,因此this.a 就是obj.a 。这里也有一个注意点,就是对象属性引用链中,只有最有一层会影响调用位置。
再次上代码。
function fun1() {
alert(this.a);
}
var obj1 = {a:1, obj2:obj2};
var obj2 = {a:2, foo:fun1}
//foo的调用位置 输出2
obj1.obj2.foo()
此时this是绑定到了obj2而不会绑定到obj1哦。隐式绑定还会存在一个问题。就是this绑定可能会在传递的过程中丢失。其实这不仅是一个问题,可以说是另一个需要注意的地方。他是符合逻辑的。
那就是我们把隐式绑定的函数引用赋值给另一个变量或者说作为参数传递时,this的隐式绑定会消失,执行时按照新的变量的调用方式来确定。
又来上代码。
function fun1() {
alert(this.a);
};
var a = 'out';
var obj = {a:2, foo:fun1}
var fun2 = obj.foo;
//输出out
fun2();
function fun3(fun) {
fun();
}
//输出out
fun3(obj.foo)
在上边的代码中,fun2以一种别名的形式来执行,尽管赋值时传给他的是一个对象的函数引用,是有上下文对象的,但是它执行的时候,依然是一个不带任何修饰的独立函数的运行方式,因此执行的是默认绑定。
其实从赋值的角度来说,我拿到的是函数的地址,这个函数本身和obj一点关系都没有。只不过通过obj来获得的地址。我拿到地址就可以执行。这一部分已经和obj没有关系了。
作为函数参数其实也是赋值。所以结果一样的。这时this绑定到了window。输出了out。
无论哪种方式,this的改变都是意想不到的,实际上我们无法控制回调函数的执行方式。但是我们可以通过固定this来解决这个问题。其实有一些工具在监听事件中会把回调函数的this强制绑定到dom元素上,从而控制this绑定的对象。
显式绑定
隐式绑定需要一个对象的属性作为函数的引用,然后通过对象的属性访问函数。如果我们就是不想在对象中添加属性还要实现绑定怎么办呢?js给我们提供了新的方法,call()和apply()。
this 绑定在你手动传入的对象上,也就是他们的参数来控制。这种方式我们可以通过参数直接指定this的绑定对象,所以称之为显示绑定。
上代码。
function arr() {
console.log(this.a);
};
var obj = {a:2};
//输出2
arr.call(obj);
又有注意点啦,如果这里传入了一个原始值,会绑定他的包装对象。apply 和 call 只是参数不同。对this而言处理一样。可惜,显示绑定仍然没有解决this丢失的问题。但是显示绑定的一个变种可以解决。
上代码。
function arr() {
console.log(this.a);
}
var obj = {a:2};
function fun() {
arr.call(obj);
};
//输出2
fun();
//试图测试通过传参丢失this,但依然输出2
setTimeout(fun,100);
//试图修改this,但依然输出2
fun.call(window)
我们通过给this包裹了一层函数,达到了无法改变this绑定。因为无论如何调用函数,最后都是会走函数arr的显示绑定调用,也算是利用了this机制。这种绑定是一种显式的强制绑定,所以我们称之为硬绑定。
硬绑定的典型场景就是创建一个包裹函数。其实js提供了这样的方式:bind函数。他就是把this设置成参数的对象,然后调用原始函数!
用法:
function arr(b) {
console.log(this.a,b)
};
var obj = {a:2};
//硬绑定,第一个参数为this,其他参数跟在后边就好
arr.bind(obj,2);
其实有很多第三方库包括js都提供了可选的参数context来指定this。其实这些函数包括硬绑定都是实现显示绑定来实现的。
new绑定
new绑定的规则是使用new 关键字调用函数。new绑定的this会绑定在一个新的对象上。如果熟悉一些面向对象的语言,会发现new这个关键字通常用来调用类中的构造函数。创建一个对象的实例。
但是在js中,用new关键字调用的函数他不属于任何一个类,也不会进行实例化。他甚至可以说不是特殊的函数类型,他只是被new调用了的普通函数而已。我们现在只要知道,使用new来调用函数会构建一个新对象绑定到函数的this上。
上代码:
function foo(a) {
this.a = a;
};
var bar = new foo(2);
//输出2
alert(bar.a);
优先级
优先级顺序
new绑定>显示绑定>隐式绑定>默认绑定
优先级验证与分析
在下面的代码中,最后两行:显式+隐式=>输出显式绑定的结果,不再是隐式绑定的结果,即显式绑定优先级高于隐式绑定。
function fun1() {
alert(this.a)
};
var obj1 = {a:1,foo:fun1};
var obj2 = {a:2,foo:fun1};
//隐式
obj1.foo(); //输出1
obj2.foo(); //输出2
//显式+隐式=>输出显式,即显式绑定优先级高
obj1.foo.call(obj2); //输出2
obj2.foo.call(obj1); //输出3
隐式绑定和new绑定:
function fun1(a) {
this.a= a;
};
var obj1 = {foo:fun1};
//隐式
obj1.foo(1);
alert(obj1.a); //输出1
//new绑定 + 隐式 => new
var obj2 = new obj1.foo(2);
alert(obj2.a); //输出2
alert(obj1.a); //输出1
以上代码中,new绑定修改了已经绑定在fun2上的obj1,因此new绑定比硬绑定优先级更高。
教你判断this的绑定
step1:看是不是new绑定,如果是,this绑定到新的对象,如果不是,走step2。
step2:看函数是否通过apply call bind调用,如果是,this绑定到指定的对象。如果不是,则走step3。
step3:看函数是否存在隐式绑定,如果是,this绑定到上下文对象。如果不是,走step4。
step4:使用默认绑定,非严格模式下将this绑定到全局window,严格模式下将this绑定到undefined。
绑定例外
规则,总有例外。在一些场景下,this的绑定也会出人意料,你以为是其他绑定,但是实际上应用了默认绑定。
需要被忽略的this
如果把null undefined 没有包装类的值传入显示绑定的函数(call、bind、apply),实际上会指向默认的绑定。
如果不是异常的话,还有什么情况我们需要手动传入一个null值呢。其实我们通常用apply来展开一个数组,或者说用bind函数实现颗粒化。这个时候,我们不关心this绑定到什么地方,于是我们传一个null。
尽管现在已经有了,操作符可以代替apply展开数组,但是依然没有柯里化的实现。仍然需要bind。
当然如果总是这样用,也会造成难以分析和追踪的bug。这时候,又出现了另一种解决方式:更安全的this。
这种方式是我们创建一个特殊的对象,使得这个对象不会对程序产生任何的副作用。
可以创建一个空的非委托的对象!可以用var nullobj = Object.create(null)生成,他比{}更加空,因为没有Object.prototype这个委托。
我们在不关心this的指向时就把这个对象传进去。这时,this的操作就不会影响其他。
无意识的间接引用
其实严格来说这不算是例外,就是我们应该注意的点。我们可能有意无意的创建创建一个函数的间接引用,从而改变了函数的this,使用了默认绑定的规则。
软绑定
我们发现硬绑定的灵活度太低了,硬绑定之后就不能改变this的指向了。隐式和显式绑定都无法修改this。
于是我们想到了软绑定的方式。他的实现方式是给默认绑定实现一个全局变量或者undefined之外的值, 可以保留隐式和显式绑定修改this的能力。
大体逻辑是:先检查this是否是undefined 或者 window。如果是,就把this绑定到指定的对象。如果不是,不修改this的绑定。这样就可以再次用其他规则来修改this的绑定。
箭头函数
之前介绍的四条规则适用于正常的函数,在ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。 箭头函数可根据外层的作用域来决定this。外层作用域中的this绑定在哪里,箭头函数内部的this就绑定在哪里。 箭头函数的this无法被修改。以上哪种绑定方式都不行。箭头函数可以像bind一样确保函数的this绑定到了指定的对象而不被修改。此外,他的重要性还体现在他用更常见的词法作用域取代了this机制。