在上一次原生javascript分享时,我发现自己对this的理解仍然不够准确。在翻看了很多现有的文章后,我很失望的发现基本全是在讲种种类型的场景下this是怎样怎样,我需要的不是这些,我想看到更深入的一些解释,例如this在函数中从何而来。后来,我准备自己查阅资料后总结一篇,就在准备资料的时候我欣喜的看到了下面这篇文章,当时的心情只能用相见恨晚来表达。我认为自己不会写出更好的文章,所以就勉强翻译过来给一些E文不太好的童鞋分享,E文好的童鞋请移步,原著更加准确生动些。
this在Javascript中应用广泛,但对它的误解却比比皆是。
你需要知道
每个运行环境 (execution context,简称环境) 都含有一个与之关联的ThisBinding常量,它们具有相同的生命周期。运行环境分为三类:
1. 全局环境
this指向全局对象,在浏览器中为window对象。
alert(this); // window
2. 函数环境
至少有5种调用函数的方式,this的值取决于具体的调用方式。
a) 作为属性调用
this的值为将函数作为属性调用的基本对象 (baseValue)
var a = {
b: function() {
return this;
}
};
a.b(); // a
a['b'](); // a
var c = {};
c.d = a.b;
c.d(); // c
b) 作为变量调用
this指向全局对象。
var a = {
b: function() {
return this;
}
};
var foo = a.b;
foo(); // window
var a = {
b: function() {
var c = function() {
return this;
};
return c();
}
};
a.b(); // window
自执行函数 (self-invoking functions) 也是如此:
var a = {
b: function() {
return (function() { return this; })();
}
};
a.b(); // window
c) 通过Function.prototype.call调用
this的值由call的第一个参数决定。
d) 通过Function.prototype.apply调用
this的值由apply的第一个参数决定。
var a = {
b: function() {
return this;
}
};
var d = {};
a.b.apply(d); // d
e) 通过new作为构造器调用
this指向新生成的对象。
var A = function() {
this.toString = function() { return "I'm an A"; };
};
new A(); // "I'm an A"
3. eval环境
this的值等于调用eval方法的执行环境中的this。
alert(eval('this == window')); // true - (except firebug, see above)
var a = {
b: function() {
eval('alert(this == a)');
}
};
a.b(); // true
你也许想知道
本节以ECMA 5 262为参考,对在函数环境下this获取值的过程做深入探究。
我们从ECMA中的this定义开始:
关键字this等于当前执行环境中ThisBinding的值。 ECMA 5, 11.1.1
ThisBinding是如何设定的呢?
每个函数都定义了一个内部方法[[Call]] (ECMA 5, 13.2.1 [[Call]]) ,用来将 invocation values 传给该函数的执行环境:
当控制器进入函数对象F的函数代码(function code)的执行环境时,依据调用对象提供的参数thisValue和argumentsList,执行以下步骤:
- 若函数代码 (function code) 为严格代码 (strict code),令ThisBinding等于thisValue
- 否则,若thisValue为null或undefined,令ThisBinding等于全局对象
- 否则,若thisValue不是Object类型,令thisBinding等于ToObject(thisValue)
- 否则,令thisBinding等于thisValue
- ⋯⋯
ECMA 5, 10.4.3 Entering Function Code
也就是说,ThisBinding在thisValue为基本类型时设定为其强制转化对象,或者当thisValue为undefined、null时,设定为全局对象(运行于严格模式时除外,这种情况下ThisBinding等于thisValue)。
那thisValue从何而来?
这里我们需要回到之前提到的五种调用函数的方式:
1. 作为属性调用
2. 作为变量调用
用ECMAScript的说法,这两种方式称为Function Calls,包含两个要素:MemberExpression和Arguments list。
1. 令ref为执行MemberExpression后得到的结果
2. 令func为GetValue(ref)
6. 若Type(ref)是引用,则
a) 若IsPropertyReference(ref)为true
i. 令thisValue为GetBase(ref)
b) 否则,ref的基本对象是一个Environment Record
i. 令thisValue为执行GetBase(ref)的具体方法ImplicitThisValue得到的结果
7. 否则,Type(ref)不是引用,则
i. 令thisValue为undefined
8. 令this等于thisValue,argument values等于argList,调用func内部方法[[Call]],并将结果返回
ECMA 5, 11.2.3 Function Calls
那么,从本质来讲,thisValue成为函数表达式的baseValue (见上面第6步。译者注:baseValue为GetBase方法得到的结果)。
当函数作为属性调用时,baseValue就是在点号(或中括号)前面的标识符。
var foo = {
bar: function() {
// (Comments apply to example invocation only)
// MemberExpression = foo.bar
// thisValue = foo
// ThisBinding = foo
return this;
}
};
foo.bar(); // foo
foo['bar'](); // foo
对于作为变量调用的情况,baseValue则是变量对象 (VariableObject,即上面提到的Environment Record),变量对象属于声明式Environment Record。ECMA 10.2.1.1讲解道,声明式Environment Record的ImplicitThisValue为undefined。
var bar = function() { ... };
bar(); // thisValue is undefined
重温上面提到过的10.4.3 Entering Function Code后,我们可以看到,除非在严格模式下,thisValue为undefined会使ThisBinding的值为全局对象。因此this在一个作为变量调用的函数中指向全局对象。
var bar = function() {
// (Comments apply to example invocation only)
// MemberExpression = bar
// thisValue = undefined
// ThisBinding = global object (e.g. window)
return this;
};
bar(); // window
3. 通过Function.prototype.apply调用
4. 通过Function.prototype.use调用
(规范参见15.3.4.3 Function.prototype.apply,15.3.4.4 Function.prototype.sue)
这两个小节描述了在call和apply调用函数时,函数中的this参数 (它的第一个参数) 的实际值是如何作为thisValue传递给10.4.3 Entering Function Code的。(注意,这一点不同于ECMA 3,后者规定thisArg的值为基本类型时需要转换为对象类型,为null或undefined时需要转化为全局对象——但这些区别通常可以忽略,因为thisArg的值会在目标函数调用时进行相同的转换过程(参见已讲过的10.4.3 Entering Function Code))
5. 通过new作为构造器调用
当函数对象F的内部方法[[Construct]]被调用时,执行以下步骤:
1. 令obj为新生成的原生ECMAScript对象
8. 令thisValue等于obj,args为传入[[Construct]]的参数列表,调用F内部方法[[Call]],并将结果保存为result
10. 返回obj
ECMA 5, 13.2.2 [[Construct]]
显而易见,作为构造器调用函数会生成一个新的对象,它被赋给thisValue。这种方式与其它this的使用方式截然不同。
释疑
严格模式
在ECMAScript的严格模式下,thisValue不会强制转化为一个对象。this的值为null或undefined时不会转化为全局对象,并且基本类型的值不会转化为包装类型对象。
bind函数
Function.prototype.bind是ECMAScript 5新添加的一个方法,使用主流框架的开发者对它已经非常熟悉。基于call/apply,bind可以通过简单的语法预设执行环境中thisValue的值。这在事件响应函数中非常有用,例如一个监听按钮点击事件的函数,它的ThisBinding默认为onclick属性的baseValue,即按钮元素:
// Bad Example: fails because ThisBinding of handler will be button
var sorter = function() {
sort: function() {
alert('sorting');
},
requestSorting: function() {
this.sort();
}
};
$('sortButton').onclick = sorter.requestSorting;
// Good Example: sorter baked into ThisBinding of handler
var sorter = function() {
sort: function() {
alert('sorting');
},
requestSorting: function() {
this.sort();
}
};
$('sortButton').onclick = sorter.requestSorting.bind(sorter);
延伸阅读
- 11.1.1 Definition of this
- 10.4.3 Entering Function Code
- 11.2.3 Function Calls
- 13.2.1 [[Call]]
- 10.2.1.1 Declarative Environment Record (ImplicitThisValue)
- 13.2.2 [[Construct]]
- 15.3.4.3 Function.prototype.apply
- 15.3.4.4 Function.prototype.call
- 15.3.4.5 Function.prototype.bind
- Annex C The Strict Mode of ECMAScript
赞!
2. 函数环境 —–b) 作为变量调用 中
这段代码貌似有问题
var a = {
b: function() {
var c = function() {
return this;
};
return c();
}
};
a.b(); // window
最后a.b(); 作用域应该是a 吧~
http://jsfiddle.net/zhiyelee/pGTKG/
在a.b中执行的代码确实处在a作用域中,但是请注意,b中含有一个c函数,而b恰恰是返回c函数的执行结果,也即c函数代码的运行环境,这里是window
贴了个图,用来展示如何 this 是如何设置的
http://hi.csdn.net/attachment/201110/25/0_13195205673Qqj.gif
哈哈,好样的!