来自《你不知道的JavaScript(上卷)》第二部分
1.关于this
举例1:
foo被调用了几次?
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
举例2:
匿名函数无法指向自身
function foo() {
foo.count = 4; // foo 指向它自身
}
setTimeout( function(){
// 匿名(没有名字的)函数无法指向自身
}, 10 );
解决办法:
(1)治标不治本:创建另一个带有count属性的对象
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
data.count++;
}
var data = {
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( data.count ); // 4
(2)治标不治本:用foo标识符替代this来引用函数对象。
同样回避了this的问题,并且完全依赖于变量foo的词法作用域。
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
foo.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 ); // 4
(3)强制this指向foo函数对象
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以确保 this 指向函数对象 foo 本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 4
1.2关于this的误解
(1)误解this是指向自身的
(2)误解this一直都指向函数的作用域
this在任何情况下都不指向函数的词法作用域
2.this全面解析
2.1调用位置
要分析调用栈(就是为了到达当前执行位置所调用的所有函数)
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 的调用位置
查看调用栈的方法:
就本例来说,你可以在浏览器开发者工具中给 foo() 函数的第一行代码设置一个断点,或者直接在第一行代码之前插入一条 debugger; 语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。
2.2绑定规则
2.2.1默认绑定
独立函数调用
无法应用其他规则时的默认规则
function foo(){
console.log(this.a);
}
var a=2;
foo();//2
分析:
- 在全局作用域中声明变量var a=2;
- 调用foo()时,this指向全局对象,因为在本例中,函数调用时应用了this的默认绑定。
- foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
- 如果使用严格模式,那么全局对象将无法使用默认绑定,因此this会绑定到undefined
这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()的调用位置无关:
function foo(){
console.log(this.a);
}
var a=2;
(function(){
"use strict";
foo();//2
})();
2.2.2 隐式绑定
隐式绑定的一般适用情况:
调用位置有上下文对象,或被某个对象拥有或包含
function foo(){
console.log(this.a);
}
var obij={
a=2,
foo:foo
};
obj.foo();//2
分析:
- 无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。
-但调用位置会适用obj上下文来引用函数,因此可以说函数被调用时obj对象“拥有”或“包含”它。 - 当foo()被调用时,它的落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时,this被绑定到obj,因此this.a和obj.a是一样的。
注意: - 对象属性引用链中只有最定测或者最后一层会影响调用位置。
function foo(){
console.log(this.a);
}
var obj2={
a:42,
foo:foo
};
var obj1={
a:2,
obj2:obj2
};
obj1.obj2.foo();//42
隐式丢失
- 被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否严格模式。
例子1:
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实际上引用的是foo函数本身,因此此时bar其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
例子2
回调函数中的隐式丢失
function foo(){
console.log(this.a);
}
function doFoo(fn)