01.呼叫位置
this的呼叫位置就是函式在程式码中被呼叫的位置(不是宣告的位置)。分析呼叫位置最重要的就是分析呼叫栈。下面一个简单例子,理解呼叫栈和呼叫位置:function baz() {
// 当前呼叫栈是:baz
// 因此,当前呼叫位置是全域性作用域
console.log( "baz" );
bar(); // }
function bar() {
this全面解析 | 83
// 当前呼叫栈是baz -> bar
// 因此,当前呼叫位置在baz 中
console.log( "bar" );
foo(); // }
function foo() {
// 当前呼叫栈是baz -> bar -> foo
// 因此,当前呼叫位置在bar 中
console.log( "foo" );
}
baz(); // 上例子中分析出了函式的呼叫位置,也就确定this的呼叫位置。
02.系结规则
2.1.预设系结
下面例子中的foo()是直接使用不带任何修饰的函式引用进行呼叫的,因此只能使用预设系结。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
然鹅,在严格模式(strict mode)下,全域性物件无法使用预设系结,因此下面的例子中,this会系结到undefined。
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
2.2.隐式系结
隐式系结会把函式呼叫中的this系结到上下文物件。下面的例子中的foo()的this系结在obj2上,所以输出是42.
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式丢失
一个最常见的this系结问题就是被隐式系结的函式会丢失系结物件,也就是说它会应用预设系结,从而把this 系结到全域性物件或者undefined 上。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函式别名!
var a = "oops, global"; // a 是全域性物件的属性
bar(); // "oops, global"
上例中,虽然bar 是obj.foo的一个引用,但是实际上,它引用的是foo函式本身,因此此时的bar() 其实是一个不带任何修饰的函式呼叫,因此应用了预设系结。
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是foo
fn(); // }
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全域性物件的属性
doFoo( obj.foo ); // "oops, global"
上例中的引数传递其实就是一种隐式赋值,所以结果跟前面的例子一样。
2.3.显式系结
JavaScript有函式都可以使用call(..)和apply(..) 方法,进行显式系结this。call(..)和apply(..)的区别就在于传入引数形式不同,
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
2.3.1.硬系结:
硬系结可以用来解决上面说到的隐式丢失问题。下面的例子中,实际上是在每次呼叫bar函式的时候都执行了硬系结,所以可以解决上述的问题。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬系结的bar 不可能再修改
bar.call( window ); // 2
由于硬系结是一种很常用的放大,所以在ES5中内建了bind()方法,作为辅助系结方法。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj ); //this硬系结在obj上
var b = bar( 3 ); // 2 3
console.log( b ); // 5
2.3.2.API呼叫的“上下文”
第三方库的许多函式,以及JavaScript 语言和宿主环境中许多新的内建函式,都提供了一个可选的引数,通常被称为“上下文”(context),其作用和bind(..) 一样,确保你的回拨函式使用指定的this。
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(..) 实现了显式系结,这样可以少些一些程式码。
2.4.new系结
使用new 来呼叫函式,或者说发生建构函式呼叫时,会自动执行下面的操作。
建立(或者说构造)一个全新的物件。这个新物件会被执行[[ 原型]] 连线。这个新物件会系结到函式呼叫的this。如果函式没有返回其他物件,那么new 表示式中的函式呼叫会自动返回这个新物件。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用new 来呼叫foo(..) 时,我们会构造一个新物件并把它系结到foo(..) 呼叫中的this上。new 是最后一种可以影响函式呼叫时this 系结行为的方法,我们称之为new 系结。
03.系结优先级
下面既代表系结的优先级,又是我们在实际应用中,判断this的顺序。函式是否在new 中呼叫(new 系结)?如果是的话this 系结的是新建立的物件。var bar = new foo()
函式是否通过call、apply(显式系结)或者硬系结呼叫?如果是的话,this 系结的是指定的物件。var bar = foo.call(obj2)
函式是否在某个上下文物件中呼叫(隐式系结)?如果是的话,this 系结的是那个上下文物件。var bar = obj1.foo()
如果都不是的话,使用预设系结。如果在严格模式下,就系结到undefined,否则系结到全域性物件。var bar = foo()
04.系结例外
上面介绍了几种系结方法,但总是有例外,这里就总结一下例外的几种情况。4.1.被忽略的this
如果你把null 或者undefined 作为this 的系结物件传入call、apply或者bind,这些值在呼叫时会被忽略,实际应用的是预设系结规则。
4.2.简介引用
间接引用最容易发生在赋值的时候:
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()。所以这时候会执行预设系结,把this系结在全域性。
05.this词法
我们之前介绍的四条规则已经可以包含所有正常的函式。但是ES6 中介绍了一种无法使用这些规则的特殊函式型别:箭头函式。箭头函式不使用this 的四种标准规则,而是根据外层(函式或者全域性)作用域来决定this。我们来看看箭头函式的词法作用域:
function foo() {
// 返回一个箭头函式
return (a) => {
//this 继承自foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3 !
foo() 内部建立的箭头函式会捕获呼叫时foo() 的this。由于foo() 的this 系结到obj1,bar(引用箭头函式)的this 也会系结到obj1,箭头函式的系结无法被修改。(new 也不行!)