this是Javascript语言的一个关键字它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用。定义:this是包含它的函数作为方法被调用时所属的对象。总原则,this指的是,调用函数的那个对象,this
永远指向函数运行时所在的对象!而非创建时的。
以下是基于浏览器环境做的测试:
作为函数调用:
1 2 3 4 5 6 7 | function $(){ this .count = 1; return this ; } window.onload = function(){ console.info($()); } |
控制台返回结果如下:
一个window对象。
对于内部函数,即声明在另外一个函数体内的函数,这种绑定到全局对象的方式会产生一个问题,即它会隐式声明全局变量。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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关键字指向全局对象。如下:
1 2 3 4 5 6 7 8 | 'use strict' ; console.log( this === window); // true var foo = function() { console.log( this === window); console.log( 'this:' , this ); }; foo(); window.foo(); |
控制台打印结果是:
使用that的另一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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]下标表达式),那么它被当做一个方法来调用。另一段代码:
1 2 3 4 5 6 7 8 | var o = {}; o.fn = $; function $(){ console.log( this ); } window.onload = function(){ o.fn(); } |
控制台返回结果如下:
调用函数的o对象。
在事件中,this指向触发这个事件的对象,特殊的是,IE中的attachEvent中的this总是指向全局对象Window;
另外一种嵌套调用:
1 2 3 4 5 6 7 8 9 10 11 | var personA={ name: "xl" , showName:function(){ console.log( this .name); } } var personB={ name: "XL" , sayName:personA.showName } personB.sayName(); |
控制台输出结果:
从这里很明显的看出,在js中this关键字指向是直接调用它的对象,而非间接的。
作为函数及对象方法的混合调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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(); |
输出结果如下:
证明函数内部函数的调用是由全局对象引发的,这在上面有所阐述。
将构造函数作为函数调用:
1 2 3 4 5 6 7 8 9 | 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实例化一个函数:
1 2 3 4 | function $(){ console.log( this ); } var fn = new $(); |
控制台输出如下:
this就指这个新对象。
使用apply或call调用:
使用apply方法((当然使用Function.call也是可以的)),另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。
1 2 3 4 5 6 | 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的值设为一个对象,如下:
1 2 3 4 5 6 7 | 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;
代码如下:
1 | <p id= "p" >click me</p> |
执行结果如下:
很显然指向是触发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 | 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();
1 2 3 4 5 6 7 8 9 10 11 | 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进行了强制转向。
另一种手动转向的方法:
1 2 3 4 5 6 7 8 9 10 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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的灵活指向已经有了一个比较清晰的认识,不过还需在实际的工程中不断运用以达到完全掌握。不过经过此一番,没必要死记硬背以上多种场景,若需使用的时候,在对印位置打印出来看看不就知道了?