本篇文章讲解一下执行上下文中的第三个属性 this。
一、全局代码中的 this
在全局代码中,this 始终是全局对象本身,这样就有可能间接的引用到它了。
// 显示定义全局对象的属性
this.a = 10; // global.a = 10
console.log(a); // 10
// 通过赋值给一个无标识符隐式
b = 20;
console.log(this.b); // 20
// 通过变量声明隐式声明的
// 因为全局上下文的变量对象是全局对象自身
var c = 30;
console.log(this.c); // 30
二、函数代码中的 this
在函数代码中使用 this 时很有趣,这种情况很难且会导致很多问题,这种类型的代码中,this 值的首要特点(或许是最主要的)是它不是静态的绑定到一个函数。
正如之前提到的那样,this 是进入上下文时确定,在一个函数代码中,这个值在每一次完全不同, 不管怎样,在代码运行时的 this 值是不变的,也就是说,因为它不是一个变量,就不可能为其分配一个新值。
var foo = {
x: 10
};
var bar = {
x: 20,
test: function() {
console.log(this === bar); // true
console.log(this.x); // 20
// this = foo; // 报错,任何时候不能改变 this 的值
// console.log(this.x); // 如果不出错,应该是 10
}
};
bar.test();
foo.test = bar.test;
// 这里 this 不会是 foo,尽管调用的是相同的 function
foo.test(); // false, 10
那么,影响了函数代码中 this 值的变化有几个因素:
- this 取决于调用函数的方式。
首先,在通常的函数调用中,this 是由激活上下文代码的调用者来提供的,即调用函数的父上下文(parent context ), this 取决于调用函数的方式,为了在任何情况下准确无误的确定this值,有必要理解和记住这重要的一点。
正是调用函数的方式影响了调用的上下文中的this值,即使是正常的全局函数也会被调用方式的不同形式激活,这些不同的调用方式导致了不同的 this 值。
function foo(){
console.log(this);
};
foo(); // global
console.log(foo == foo.prototype.constructor); // true
// 但是同一个 function 的不同的调用表达式,this 是不同的
foo.prototype.constructor(); // constructor: function foo()
- 有可能作为一些对象定义的方法来调用函数,但是 this 将不会设置为这个对象。
var foo = {
bar: function() {
console.log(this);
console.log(this === foo);
}
};
foo.bar(); // Object { bar: bar() }, true
var exampleFunc = foo.bar;
console.log(exampleFunc === foo.bar); // true
// 同一个 function 的不同的调用表达式,this 是不同的
exampleFunc(); // global, false
那么,调用函数的方式如何影响 this 值?
为了充分理解 this 值的确定,需要详细分析其内部类型:引用类型(Reference type)。
三、引用类型
通过调用方式动态确定this值:
function foo() {
console.log(this.bar);
};
var x = {
bar: 10
};
var y = {
bar: 20
};
x.test = foo;
y.test = foo;
x.test(); // 10
y.test(); // 20
四、函数调用和非引用类型
当调用括号的左边不是引用类型而是其它类型,这个值自动设置为null,结果为全局对象,再思考这种表达式:
(function() {
console.log(this); // global
})();
(function(){}());,这是一个匿名函数,后面的 () 是立即调用的意思,在这个例子中,我们有一个函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this 值最终设为全局对象。
更复杂的例子:
var foo = {
bar: function() {
console.log(this);
}
};
foo.bar(); // Object { bar: bar() }
(foo.bar)(); // Object { bar: bar() }
(foo.bar = foo.bar)(); //global
(false || foo.bar)(); // global
(foo.bar, foo.bar)(); //global
为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的 this 值不是 base 对象,而是 global 对象?
问题在于后面的三个调用,在应用一定的运算操作之后,在调用括号的左边的值不再是引用类型。
- 第一个例子很明显是引用类型,结果是 this 为 base 对象,即foo。
- 在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如 GetValue,相应的,在组运算的返回中,我们得到仍是一个引用类型,这就是 this 值为什么再次设为 base 对象,即 foo。
- 第三个例子中,与组运算符不同,赋值运算符调用了 GetValue 方法,返回的结果是函数对象(但不是引用类型),这意味着 this 设为 null,结果是 global 对象。
- 第四个和第五个也是一样用逗号运算符和逻辑运算符调用了 GetValue 方法,相应地,我们失去了引用而得到了函数,并再次设为 global。
五、引用类型和 this 为 null
有一种情况是这样的:当调用表达式限定了括号左边的引用类型的值, 尽管 this 被设定为 null,但结果被隐式转化成 global,当引用类型值的 base 对象是被活动对象时,这种情况就会出现。
下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况:
function foo() {
function bar() {
console.log(this); // global
};
bar();
};
foo();
活动对象总是作为 this 返回,值为 null(即伪代码的 AO.bar() 相当于 null.bar())。
这里我们再次回到上面描述的例子,this 设置为全局对象。
有一种情况除外:如果 with 对象包含一个函数名属性,在 with 语句的内部块中调用函数,With 语句添加到该对象作用域的最前端,即在活动对象的前面,相应地,也就有了引用类型(通过标示符或属性访问器),其 base 对象不再是活动对象,而是 with 语句的对象。
顺便提一句,它不仅与内部函数相关,也与全局函数相关,因为 with 对象比作用域链里的最前端的对象(全局对象或一个活动对象)还要靠前。
var x = 10;
with({
foo: function() {
console.log(this.x);
},
x: 20
}) {
foo(); // 20
};
// var fooReference = {
// base: __withObject,
// propertyName: 'foo'
// };
同样的情况出现在 catch 语句的实际参数中函数调用,在这种情况下,catch 对象添加到作用域的最前端,即在活动对象或全局对象的前面,但是,这个特定的行为被确认为 ECMA-262-3 的一个 bug,这个在新版的 ECMA-262-5 中修复了。
这样,在特定的活动对象中,this 指向全局对象,而不是 catch 对象。
try {
throw function() {
console.log(this); // global
};
} catch (e) {
e(); // ES3标准里是 catchObject, ES5标准里是 global
};
// var eReference = {
// base: __catchObject,
// propertyName: 'e'
// };
// ES5新标准里已经修复了这个 bug
// 所以 this 就是全局对象了
// var eReference = {
// base: global,
// propertyName: 'e'
// };
同样的情况出现在命名函数的递归调用中,在函数的第一次调用中,base 对象是父活动对象(或全局对象),在递归调用中,base 对象应该是存储着函数表达式可选名称的特定对象,但是,在这种情况下,this 总是指向全局对象。
(function foo(bar) {
console.log(this); // global
!bar && foo(1);
})();
六、作为构造器调用的函数中的 this
还有一个与 this 值相关的情况是在函数的上下文中,这是一个构造函数的调用。
function A() {
console.log(this); // Object { }
this.x = 10;
};
var a = new A();
console.log(a.x); // 10
在这个例子中,new 运算符调用 “A” 函数的内部的 [[construct]] 方法,接着,在对象创建后,调用内部的 [[call]] 方法,所有相同的函数 “A” 都将 this 的值设置为新创建的对象。
七、函数调用中手动设置 this
在函数原型中定义的两个方法(因此所有的函数都可以访问它)允许去手动设置函数调用的 this 值,它们是 .apply 和 .call 方法。
他们用接受的第一个参数作为 this 值,this 在调用的作用域中使用。
这两个方法的区别很小,对于 .apply,第二个参数必须是数组(或者是类似数组的对象,如 arguments),.call 能接受任何参数。
两个方法必须的参数是第一个 this。
var b = 10;
function a(c) {
console.log(this.b);
console.log(c);
};
a(20); // this === global, this.b == 10, c == 20
a.call({
b: 20
}, 30); // this === {b : 20}, this.b == 20, c == 30
a.apply({
b: 30
}, [40]); // this === {b : 30}, this.b == 30, c == 40

网友评论