参考:阮一峰《javascript的this用法》及《JS中this关键字详解》
this是Javascript语言的一个关键字它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用。定义:this是包含它的函数作为方法被调用时所属的对象。总原则,this指的是,调用函数的那个对象,this
永远指向函数运行时所在的对象!而非创建时的。
以下是基于浏览器环境做的测试:
作为函数调用:
function $(){
this.count = 1;
return this;
}
window.onload = function(){
console.info($());
}
控制台返回结果如下:
一个window对象。
对于内部函数,即声明在另外一个函数体内的函数,这种绑定到全局对象的方式会产生一个问题,即它会隐式声明全局变量。代码如下:
var point = {
x: 0,
y: 0,
moveTo: function(x, y) {
var fn1 = function(x) {
this.x = this.x + x;
return this.x;
};
var fn2 = function(y) {
this.y = this.y + y;
};
return fn1();
}
}
console.log(point.moveTo());
结果是:
而若将fn1中return的值改为this的话,打印结果:
一个window全局对象。这属于 JavaScript 的设计缺陷,正确的设计方式是内部函数的 this 应该绑定到其外层函数对应的对象上,这个设计错误错误的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。为了规避这一设计缺陷,聪明的 JavaScript 程序员想出了变量替代的方法,约定俗成,该变量一般被命名为 that。如下:
var point = {
x: 0,
y: 0,
moveTo: function(x, y) {
var that = this;
var x = x;
var y = y;
var fn1 = function(x) {
that.x = that.x + x;
return that;
};
var fn2 = function(y) {
that.y = that.y + y;
};
return fn1(x);
}
}
console.log(point.moveTo(1,1));
返回结果:
函数调用中,this总是指向函数的直接调用者(而非间接调用者)。如果在严格模式下,有可能输出undefined,严格模式下,禁止this关键字指向全局对象。如下:
'use strict';
console.log(this === window); // true
var foo = function() {
console.log(this === window);
console.log('this:',this);
};
foo();
window.foo();
控制台打印结果是:
使用that的另一个例子:
var myObj = {
value: 0,
increment: function(inc) {
this.value += typeof inc === 'number' ? inc: 1;
}
}
myObj.increment();
document.writeln(myObj.value);
myObj.increment(2);
document.writeln(myObj.value);
function add(num1, num2){
return num1 + num2;
}
console.log(add(40,27));
myObj.double = function() {
var that = this;
var helper = function() {
that.value = add(that.value, that.value);
}
helper();
}
myObj.getValue = function() {
var that = this;
return that.value;
}
myObj.double();
document.writeln(myObj.getValue());
作为对象方法调用:
如果一个调用表达式包含一个属性存取表达式(即一个.点表达式或者[subscript]下标表达式),那么它被当做一个方法来调用。另一段代码:
var o = {};
o.fn = $;
function $(){
console.log(this);
}
window.onload = function(){
o.fn();
}
控制台返回结果如下:
调用函数的o对象。
在事件中,this指向触发这个事件的对象,特殊的是,IE中的attachEvent中的this总是指向全局对象Window;
另外一种嵌套调用:
var personA={
name:"xl",
showName:function(){
console.log(this.name);
}
}
var personB={
name:"XL",
sayName:personA.showName
}
personB.sayName();
控制台输出结果:
从这里很明显的看出,在js中this关键字指向是直接调用它的对象,而非间接的。
作为函数及对象方法的混合调用:
var myObject={
foo : "",
func : function(){
var self = this;
console.log("outer func : this.foo = " + this.foo );
console.log("outer func : self.foo = " + self.foo );
(function(){
console.log("inner func : this.foo = " + this.foo );
console.log("inner func : self.foo = " + self.foo );
}());
}
}
myObject.func();
输出结果如下:
证明函数内部函数的调用是由全局对象引发的,这在上面有所阐述。
将构造函数作为函数调用:
function Person(name) {
this.name = name;
}
var personA = Person("xl");
// console.log(personA.name);
console.log(window.name);
console.log(name);
var personB = new Person("xl");
console.log(personB.name);
控制台打印结果:
注释掉的console因为那么已经成为全局对象的属性,因此打印为未定义变量报错。第二个显式调用,第三个隐式调用。最后一个则是作为构造方法调用。
作为构造函数调用:
JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。相应的,JavaScript 中的构造函数也很特殊,如果不使用 new 调用,则和普通函数一样。作为又一项约定俗成的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。如果调用正确,this 绑定到新创建的对象上。
通过new实例化一个函数:
function $(){
console.log(this);
}
var fn = new $();
控制台输出如下:
this就指这个新对象。
使用apply或call调用:
使用apply方法((当然使用Function.call也是可以的)),另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。
function $(){
console.log(this);
}
var o = {};
o.m = $;
o.m.apply();
控制台打印结果:
注:Function.apply(obj,args)方法能接收两个参数
obj:这个对象将代替Function类里this对象
args:这个是数组,它将作为参数传给Function(args-->arguments)
apply()的参数为空时,默认调用全局对象。
如果将call的第一个destination的值设为一个对象,如下:
function $(){
console.log(this);
}
var u = {};
var o = {};
o.m = $;
o.m.apply(u,null);
控制台打印结果如下:
也即this指向了apply绑定的那个对象。
作为构造函数及apply/call的混用:
//下面这段代码模拟了new操作符(实例化对象)的内部过程
function person(name){ var o={}; o.__proto__=Person.prototype; //原型继承 Person.call(o,name); return o; } var personB=person("xl"); console.log(personB.name); // 输出 xl
在person
里面首先创建一个空对象o,将o的proto指向Person.prototype完成对原型的属性和方法的继承。Person.call(o,name)
这里即函数Person
作为apply/call
调用(具体内容下方),将Person
对象里的this
改为o,即完成了o.name=name
操作。返回对象o。
作为回调函数的this:
我们来看看 this 在 JavaScript 中经常被误用的一种情况:回调函数。JavaScript 支持函数式编程,函数属于一级对象,可以作为参数被传递。请看下面的例子 myObject.handler 作为回调函数,会在 onclick 事件被触发时调用,但此时,该函数已经在另外一个执行环境(ExecutionContext)中执行了,this 自然也不会绑定到 myObject 对象上。
button.onclick = obj.handler;
代码如下:
<p id="p">click me</p>
<script type="text/javascript">
var obj = {
handler: function() {
console.log(this);
}
}
var p = document.getElementById("p");
p.onclick = obj.handler;
执行结果如下:
很显然指向是触发click事件的dom元素对象,而非obj对象。这是 JavaScript 新手们经常犯的一个错误,为了避免这种错误,许多 JavaScript 框架都提供了手动绑定 this 的方法。比如 Dojo 就提供了 lang.hitch,该方法接受一个对象和函数作为参数,返回一个新函数,执行时 this 绑定到传入的对象上。使用 Dojo,可以将上面的例子改为:button.onclick = lang.hitch(myObject, myObject.handler);在新版的 JavaScript 中,已经提供了内置的 bind 方法供大家使用。
eval方法中this的指向:
JavaScript 中的 eval 方法可以将字符串转换为 JavaScript 代码,使用 eval 方法时,this 指向哪里呢?答案很简单,看谁在调用 eval 方法,调用者的执行环境(ExecutionContext)中的 this 就被 eval 方法继承下来了。
var name="XL";
var person={
name:"xl",
showName:function(){
eval("console.log(this.name)");
}
}
person.showName(); //输出 "xl"
var a=person.showName;
a(); //输出 "XL"
第一次的执行环境是person对象,第二个是global全局环境。
Function.prototype.bind()方法:
1.var name = "XL";
function Person(name) {
this.name = name;
console.log(this);
this.sayName = function() {
console.log(this);
setTimeout(function(){
console.log(this);
console.log("my name is " + this.name);
},50)
}
}
var person = new Person("xl");
person.sayName();
2.var name="XL";
function Person(name){
this.name=name;
this.sayName=function(){
setTimeout(function(){
console.log("my name is "+this.name);
}.bind(this),50) //注意这个地方使用的bind()方法,绑定setTimeout里面的匿名函数的this一直指向Person对象
}
}
var person=new Person("xl");
person.sayName(); //输出 “my name is xl”;
这里setTimeout(function(){console.log(this.name)}.bind(this),50);
,匿名函数使用bind(this)
方法后创建了新的函数,这个新的函数不管在什么地方执行,this
都指向的调用它的对象
,而非window。而如果不加bind在第一段代码中,window执行环境中创建了一个变量person,被赋值了Person的实例,此时的调用顺序是person调用了sayName这个方法,这个方法被赋值了一个函数(此处有问题,该赋值函数是匿名还是非匿名?下篇讨论),创建了一个执行环境,此时setTimeOut函数开始执行,创建一个环境。匿名函数及setTimeout/setInterval在非手动改变指向额情况下都在全局作用域当中。
SO,第一段代码的打印结果是:
sf的解释:setTimeout/setInterval/匿名函数执行
的时候,this
默认指向window对象
,除非手动改变this的指向。在《javascript高级程序设计》当中,写到:“超时调用的代码(setTimeout
)都是在全局作用域中执行的,因此函数中的this的值,在非严格模式下是指向window对象,在严格模式下是指向undefined”。本文都是在非严格模式下的情况。
第二段代码的打印结果是:
这相当于手动将this进行了强制转向。
另一种手动转向的方法:
var name="XL";
function Person(){
this.name="xl";
var that=this;
this.showName=function(){
console.log(that.name);
}
setTimeout(this.showName,50)
}
var person=new Person(); //输出 "xl"
借用了上面提到的that保存this指针值进行复用的技巧。
匿名函数中的this:
var name="XL";
var person={
name:"xl",
showName:function(){
console.log(this.name);
}
sayName:function(){
(function(callback){
callback();
})(this.showName)
}
}
person.sayName(); //输出 XL
var name="XL";
var person={
name:"xl",
showName:function(){
console.log(this.name);
}
sayName:function(){
var that=this;
(function(callback){
callback();
})(that.showName)
}
}
person.sayName() ; //输出 "xl"
此处采用了that技巧保存this指针。
箭头函数(点击这里):
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定。示例代码:
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
console.log(obj.getAge(2015)); // 25
控制台打印结果:
this的四种使用场景
面向对象的语言中,this 关键字的含义是明确且具体的,即指代当前对象。一般在编译期确定下来,或称为编译期绑定。而在 JavaScript 中,this 是动态绑定,或称为运行期绑定,这就导致 JavaScript 中的 this 关键字有能力具备多重含义,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式,带来灵活性也带来困惑。js“超级”迟绑定( very late binding)使得函数可以对this高度复用。通过this可取得它们所属对象的上下文的方法称为公共方法。使用this关键字在面向对象语言中多数情况下是为了避免命名冲突。总的来说,JavaScript 中函数的调用有以上几种方式:作为对象方法调用,作为函数调用,作为构造函数调用,和使用 apply 或 call 调用。JavaScript 中的函数既可以被当作普通函数执行,也可以作为对象的方法执行,这是导致 this 含义如此丰富的主要原因。
函数的执行环境
JavaScript 中的函数既可以被当作普通函数执行,也可以作为对象的方法执行,这是导致 this 含义如此丰富的主要原因。一个函数被执行时,会创建一个执行环境(ExecutionContext),函数的所有的行为均发生在此执行环境中,构建该执行环境时,JavaScript 首先会创建 arguments
变量,其中包含调用函数时传入的参数。接下来创建作用域链。然后初始化变量,首先初始化函数的形参表,值为 arguments
变量中对应的值,如果 arguments
变量中没有对应值,则该形参初始化为 undefined
。如果该函数中含有内部函数,则初始化这些内部函数。如果没有,继续初始化该函数内定义的局部变量,需要注意的是此时这些变量初始化为 undefined
,其赋值操作在执行环境(ExecutionContext)创建成功后,函数执行时才会执行,这点对于我们理解 JavaScript 中的变量作用域非常重要,鉴于篇幅,我们先不在这里讨论这个话题。最后为 this
变量赋值,如前所述,会根据函数调用方式的不同,赋给 this
全局对象,当前对象等。至此函数的执行环境(ExecutionContext)创建成功,函数开始逐行执行,所需变量均从之前构建好的执行环境(ExecutionContext)中读取。
本文是综合了互联网多篇文章结论之后亲测的结论,目的也在于细化自己的知识体系,并进而更深刻的理解js的内核实现。经过此番梳理,对this的灵活指向已经有了一个比较清晰的认识,不过还需在实际的工程中不断运用以达到完全掌握。不过经过此一番,没必要死记硬背以上多种场景,若需使用的时候,在对印位置打印出来看看不就知道了?