this 不指向函数本身。
人们很容易把this 理解成指向函数自身,事实上,this 并不像我们所想的那样指向函数本身。我们通过下面这段代码来解释为什么this 并是指向函数本身。
function foo(num) {
console.log( "foo: " + num );
// 记录foo 被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0
那么this.count++这个语句执行结果去了哪呢,其实,这里的this指向了全局对象,此时this.count值为NaN,因为在全局对象中,由于不存在count变量,JavaScript会为我们自动创建一个名为count的变量,其初始值为”undefied“,因此进行++操作后,this.count值变成了NaN
this 不一定指向函数的作用域
第二种常见的误解是,this 指向函数的作用域。在某种情况下它是正确的,但是在其他情况下它却是错误的。
事实上,this 在任何情况下都不指向函数的词法作用域。在JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JavaScript代码访问,它存在于JavaScript 引擎内部。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
首先,这段代码试图通过this.bar() 来引用bar() 函数。调用bar() 最自然的方法是省略前面的this,直接使用词法引用标识符。
此外,编写这段代码的开发者还试图使用this 联通foo() 和bar() 的词法作用域,从而让bar() 可以访问foo() 作用域里的变量a。这是不可能实现的,你不能使用this 来引用一个词法作用域内部的东西。
每当你想要把this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的
this的绑定
this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。
1、默认绑定
独立函数调用是最常用的函数调用类型。可以把这条规则看作是无法应用其他规则时的默认规则。此时,this 默认指向全局对象。需要注意的是,在全局作用域中声明的变量(var a = 2)就是全局对象的一个同名属性,它们本质上就是同一个东西。
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this 会绑定到undefined:
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
2、隐式绑定
隐式绑定考虑的是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。换句话说,就是某个函数被某个对象所拥有,则这个函数中的this一般情况下指向这个对象。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
需要注意的是:foo()的声明方式,无论是直接在obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj 对象。然而,调用位置会使用obj 上下文来引用函数,因此你可以说函数被调用时obj 对象“拥有”或者“包含”它。
需要注意的是,对象属性引用链中只有最顶层或者说最后一层会影响调用位置。比如:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式丢失:下面代码中,虽然bar 是obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
再看一个例子:
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"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。
3、显式绑定
通过apply、call、bind将this绑定到特定对象上,虽然bind可绑定到null上,但十分不推荐
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 绑定。
优先级
new绑定>显式绑定>隐式绑定>默认绑定
整理:《你不知道的JavaScript(上卷)》