this是在运行时绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
1. 调用位置
调用位置是函数被调用的位置(不是声明的位置),只有仔细分析调用位置才能知道this到底引用什么?
- 寻找调用位置最重要的是:分析调用栈(调用位置就在当前正在执行函数的前一个调用中)。
function baz(){
// 当前调用栈是:baz
// 当前调用位置是全局作用域
console.log('baz');
bar(); // bar的调用位置
}
function bar(){
// 当前调用栈是baz --> bar
// 当前调用位置在baz中
console.log('bar');
foo(); // foo的调用位置
}
function foo(){
// 当前调用栈baz --> bar --> foo
// 当前调用位置在bar中
console.log('foo');
}
baz(); // baz的调用位置
- 开发者模式中设置断点,或者代码中插入debugger;调试器在那个位置暂停,同时会展示当前位置的函数调用列表,这就是调用栈。
2. 绑定规则
2.1 默认绑定
独立函数调用:无法应用其他规则时的默认规则。
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
this.a被解析成了全局变量a,函数调用时应用了this的默认绑定,因此this指向全局变量。
严格模式下,全局对象无法使用默认绑定,this会绑定到undefined。
function foo() {
'use strict';
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
// --------------------------------------
// 严格模式下调用函数不影响默认绑定。
function foo(){
console.log(this.a);
}
var a = 2;
(function(){
'use strict';
foo(); // 2;
});
2.2 隐式绑定
当函数引用有上下文对象时,隐式绑定规则会默认把this绑定到这个上下文对象。
当foo被调用后,this绑定到这个上下文对象obj上。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
obj.foo(); // 2
对象属性引用链中只有最顶层或者说是最后一层会影响调用位置。
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
obj2: obj2
}
var obj2 = {
a: 3,
foo: foo
}
obj.foo(); // 3
隐式丢失,被隐式绑定的函数会丢失绑定对象,它会应用默认绑定,从而把this绑定到全局对象上或者undefined(取决于是否严格模式)
function foo(){
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo; // 函数别名
var a = "oops, globel!" // a是全局对象的属性
bar(); // oops, globel!
虽然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, globel!" // a是全局对象的属性
doFoo(obj.foo) // oops, globel!
/**
* 把函数传入内置的函数而不是自己声明的函数也是一个结果
*/
function foo(){
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var a = "oops, globel!" // a是全局对象的属性
setTimeout(obj.foo, 100) // oops, globel!
参数传递就是一种隐式赋值,所以传入函数也会被隐式赋值。回调函数丢失this是非常常见的。
2.3 显式绑定
通过call(...)和apply(...)方法,第一个参数是this绑定的对象。因此,直接指定this的绑定对象,称之为显式绑定。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
}
foo.call(obj); // 2
但显式绑定无法解决之前提出的绑定丢失问题
- 硬绑定
创建函数bar,在函数内部调用foo.call(obj),强制把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
// 硬绑定不可能在修改它的this
bar.call( window ); // 2
典型的应用场景是创建一个包裹函数,负责接收参数并返回值
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, arguments );
}
}
var obj = {
a: 2
}
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
ES5内置了Function.prototype.bind,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
- API调用的上下文
javascript许多内置的函数提供了一个可选参数,被称之为“上下文(context)”,作用和bind(...)一样,确保回调函数使用指定的this,
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
}
var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
2.4 new 绑定
- 构造函数只是使用
new操作符时被调用的普通函数。 - 包括内置对象函数(比如
Number(...))在内的所有函数d都可以用new来调用,这种函数称为构造函数借用。 - 实际上并不存在所谓的‘构造函数’,只有对函数的‘构造调用’。
使用new来调用函数,或者说发生构造函数调用时,会执行下面的操作
- 创建(构造)一个全新的对象。
- 这个新对象会被执行[[prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象。
使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。
function foo(a) {
this.a = a;
}
var obj = new foo(2);
console.log(obj.a); // 2
3. 优先级
this优先级顺序
- 函数是否在
new中调用(new绑定),如果是的话,this绑定的是新创建的对象。 - 函数是否通过
call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。 - 函数是否在某个上下文对象调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
4. 绑定例外
4.1 被忽略的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: 2
// ---------------------------
// 使用bind(..)进行柯里化
var bar = foo.bind(null, 2)
bar(3); // a: 2 b: 3
总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。
更安全的this
创建一个空对象最简单的方法是Object.create(null),这个和{}很像,但并不会创建Object.prototype委托,把这个对象绑定到你的程序不会产生任何副作用。
function foo(a, b) {
console.log("a: " + a + " b: " + b);
}
// 空对象
var ø = Object.create(null);
// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3
// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3
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();所以这里应用默认绑定。
4.3 软绑定
- 硬绑定可以强制把this绑定到指定的对象上(new除外),防止函数调用应用默认规则。但是会降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或者显式绑定来修改this
- 如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this
if(!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有curried参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
使用:软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
function foo() {
console.log("name:" + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
// 隐式绑定规则
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!!
// 显式绑定规则
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!!
// 绑定丢失,应用软绑定
setTimeout( obj2.foo, 10 ); // name: obj
5. this词法
ES6中新增了一种无法使用规则的特殊函数类型:箭头函数。 箭头函数的词法作用域:
function foo() {
return (a) => {
// this继承自foo()
console.log(a);
}
}
var obj1 = { a: 2 };
var obj2 = { a: 3 };
var bar = foo.call(obj1);
bar.call(obj2); // 2
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法修改。(new 也不行)。
ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。
function foo() {
var self = this; // lexical capture of this
setTimeout( function() {
console.log( self.a ); // self只是继承了foo()函数的this绑定
}, 100 );
}
var obj = {
a: 2
};
foo.call(obj); // 2
- 只使用词法作用域并完全抛弃错误this风格的代码;
- 完全采用
this风格,在必要时使用bind(..),尽量避免使用self = this和箭头函数