this
this既不指向自身也不指向函数的词法作用域。
this实际上是在函数被调用时发生的变化,它指向什么完全取决于函数在哪里被调用。
调用位置
函数在代码中被调用的位置。
绑定规则
默认绑定
最常用的函数调用类型:独立函数调用。(无法应用其他规则时的默认规则)
function foo(){
console.log(this.a);
}
var a = 2;
foo();//2
this指向全局window对象。
PS:在严格模式下,this会指向undefined。
隐式绑定
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
obj.foo();//2
foo()函数在obj对象中被调用。调用位置会使用obj上下文来引用函数。
当函数引用有上下文对象时,隐式绑定规则会将函数中调用的this绑定到这个上下文对象。
以为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
隐式丢失
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo;//函数别名!
var a = 3;
bar();//3
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时bar()就是一个不带任何修饰的函数调用,应用默认绑定。相当于在全局作用域下调用了foo函数。this指向全局window对象。
在看一个例子:
var a = 3;
function foo(){
console.log(this.a);
}
function doFoo(fn){
var a = 4;
//fn引用的其实就是foo
fn();//调用位置
}
var obj = {
a:2,
foo:foo
}
doFoo(obj.foo); //3
参数传递实际上就是一个隐性赋值。
还有一个更加直观的例子需要理解:
var a = 3;
function doFoo() {
var a = 4;
//fn引用的其实就是foo
function foo() {
console.log(this.a);
}
foo(); //调用位置
}
doFoo();//3
为什么是3不是4?
形成的调用栈(到达调用位置所调用的所有函数)中,foo在doFoo被执行时中被调用,doFoo()在全局下被调用,doo中的this指向全局window。(丢失了this绑定)应用的是默认绑定。
显式绑定
分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接绑定到这个对象上。
不想在函数内部包含函数引用,想在某个对象上强制调用函数,怎么做?
可以使用call(…)和apply(…)方法。
第一个参数是一个对象,是给this准备的,在调用函数时将其绑定到this。
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
foo.call(obj);//2
通过call方法将foo函数内部的this强制指向了obj。
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var bar = function(){
foo.call(obj);
}
bar();//2
setTimeout(bar,100);//2
//硬绑定的bar不可能再修改它的this
bar.call(window);//2
上面的例子中,创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此foo的this强制绑定到了obj上。
之后无论怎么调用bar函数,它总会手动在obj上调用foo。称为硬绑定。
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值。
function foo(something){
console.log(this.a,something);
return this.a + something;
}
var obj = {
a:2
}
var bar = function(){
return foo.apply(obj,arguments);
}
var b = bar(3);//2,3
console.log(b);//5
还有一种使用方法是创建一个可以重复使用的辅助函数。
function foo(something){
console.log(this.a,something);
return this.a + something;
}
//简单的辅助绑定函数
function bind(fn,obj){
return function(){
return fn.apply(obj,argunments);
}
}
var obj = {
a:2
}
var bar = bind(foo,obj);
var b = bar(3);//2 3
console.log(b);//5
ES5提供了内置的方法Function.prototype.bind。
function foo(something){
console.log(this.a,something);
return this.a + something;
}
var obj = {
a:2
}
var bar = foo.bind(obj);
var b = bar(3);//2 3
console.log(b);//5
bind()方法会返回一个硬编码的新函数,它会将你制定的参数设置为this的上下文并调用原始函数。
API中调用的上下文
function foo(el){
console.log(el,this,id);
}
var obj = {
id:'awesome'
}
//调用foo()时把this绑定到obj
[1,2,3].forEach(foo,obj);//1 awesome 2 awesome 3 awesome
这些函数实际上就是通过call()或者apply()实现了显示绑定。
new绑定
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,并不属于某个类,也不会实例化一个类。
使用new来调用函数或者说发生构造函数调用时,会自动执行以下的操作。
- 创建一个全新的对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a);//2
优先级
function foo(){
console.log(this.a);
}
var obj1 = {
a:2,
foo:foo
}
var obj2 = {
a:3,
foo:foo
}
obj1.foo();//2
obj2.foo();//3
obj1.foo.call(obj2);//3
obj2.foo.call(obj1);//2
从上面的例子可以看出显式绑定的优先级高于隐式绑定。
function foo(something){
this.a = something;
}
var obj1 = {
foo: foo
}
var obj2 = {};
obj1.foo(2);
console.log(obj1.a);//2
obj1.foo.call(obj2, 3);
console.log(obj2.a);//3
var bar = new obj1.foo(4);
console.log(obj1.a);//2
console.log(bar.a);//4
从上面的例子可以看出new绑定的优先级高于隐式绑定。
比较显式绑定和new绑定的优先级。
硬绑定如何工作:Function.prototype.bind(..)会创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。
bar被硬绑定到obj1上,但是new bar(3)病没有把obj1.a修改为3。
new修改了硬绑定(到obj1的)调用bar(..)中的this。
因为使用了new绑定,得到一个名为baz的新对象,并且baz.a的值为3。
为什么要在new中使用硬绑定的函数呢?主要目的是预先设置函数的一些参数,这样在new进行初始化的时就可以只传入其余的参数。
bind()的功能之一就是可以把除了第一个参数(用于绑定this)之外的其他参数都传给下层 的函数。
function foo(p1,p2){
this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
baz.val;//p1p2
判断this
- 函数是否在new中调用?是的话this绑定的时新创建的对象。 var bar = new foo();
- 函数是否通过call、apply或者硬绑定调用?是的话this绑定的是指定的对象。var bar = foo.call(obj);
- 函数是否在某个上下文对象中调用(隐式绑定)?是的话this绑定到那个上下文对象中。var bar = obj1.foo();
- 如果以上都不是,则使用默认绑定。严格模式下绑定到undefined,否则绑定到全局对象。
例外情况
被忽略的this
如果将null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用的时候会被忽略,实际应用的是默认绑定规则。
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null);//2
什么时候传入null?
- 使用apply来“展开”一个数组,并将参数传入一个函数。
- bind()可以预先设置一些参数。
function foo(a, b){
console.log("a:" + a + ",b+" + b);
}
//把数组展开成参数
foo.apply(null, [2,3]);//a:2,b:3
//使用bind()预先设置一些参数
var bar = foo.bind(null, 2);
bar(3);//a:2,b:3
这两个方法都需要传入一个参数当做this的绑定对象。如果函数不关心this的话,可以传入null。
在ES6中,可以用…操作符代替apply(..)来“展开”数组,foo(..[1,2])和foo(1,2)是一样的。
总是忽略this,传入null可以会产生一些问题。如果某个函数使用到了this,此时默认绑定规则会将this绑定到全局变量。
更安全的this
一种更加安全的做法是传入一个特殊的对象,把this绑定到这个不会出现任何副作用的对象上。不影响全局。
function foo(a, b){
console.log("a:" + a + ",b+" + b);
}
var obj = Object.creat(null);
//把数组展开成参数
foo.apply(obj, [2,3]);//a:2,b:3
//使用bind()预先设置一些参数
var bar = foo.bind(obj, 2);
bar(3);//a:2,b:3
Object.creat(null)和{ }很像,但是并不会创建Object.prototype这个委托,它比{ }更加空。
间接引用
有时候在无意中可能会创建一个函数的“间接引用”,这种情况下调用这个函数会应用默认绑定原则。
间接引用在最容易在赋值时发生:
function foo(){
console.log(this.a);
}
var a = 2;
var o = {a: 3, foo: foo};
var p = {a: 4};
o.foo(); //3
(p.foo = o.foo)();//2
赋值表达式p.foo = o.foo的返回值时目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。这里会应用默认绑定。
PS:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this绑定到undefined,否则绑定到全局对象。