【JS】JavaScript 中 this 是如何工作的?
更新日期:
this关键字的含义
简单说,this就是指当前函数的运行环境。由于JavaScript支持运行环境的动态切换,所以this的指向是动态的。
所谓“运行环境”其实就是对象。可以理解成,this指函数运行时所在的那个对象。
例:有一个函数f,它同时充当a对象和b对象的方法。JavaScript允许函数f的运行环境动态切换,即一会属于a对象,一会属于b对象,这就要靠this关键字来办到。
|
|
当f属于a对象时,this指向a;当f属于b对象时,this指向b,因此打印出了不同的值。由于this的指向可变,所以达到了运行环境动态切换的目的。
this在不同情况下,指向各不相同
全局环境/函数调用
在全局环境使用this,它指的就是顶层对象window。
|
|
不管是不是在函数内部,只要是在全局环境下运行,this就是指全局对象window。
构造函数
构造函数中的this,指的是实例对象。
|
|
上面代码定义了一个构造函数O。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性;然后m方法可以返回这个p属性。
方法调用
当a对象的方法被赋予b对象,该方法就变成了普通函数,其中的this就从指向a对象变成了指向b对象。这就是this取决于运行时所在的对象的含义,所以要特别小心。如果将某个对象的方法赋值给另一个对象,会改变this的指向。
|
|
f是o1的方法,但是如果在o2上面调用这个方法,f方法中的this就会指向o2。这就说明JavaScript函数的运行环境完全是动态绑定的,可以在运行时切换。
如果不想改变this的指向,可以将o2.f改写成下面这样。
|
|
上面代码表示,由于f方法这时是在o1下面运行,所以this就指向o1。
有时,某个方法位于多层对象的内部,这时如果为了简化书写,把该方法赋值给一个变量,往往会得到意想不到的结果。
|
|
上面代码表示,m属于多层对象内部的一个方法。为求简写,将其赋值给hello变量,结果调用时,this指向了全局对象。
为了避免这个问题,可以只将m所在的对象赋值给hello,这样调用时,this的指向就不会变。
|
|
Node.js
在Node.js中,this的指向又分成两种情况。全局环境中,this指向全局对象global;模块环境中,this指向module.exports。
|
|
在浏览器环境下,最高作用域是global作用域,这意味着变量会定义在全局变量下。
而在Node下,最高最高作用域不是global作用域,定义的变量属于Node模块。
在Node.js中怎样使用全局变量:http://stackoverflow.com/questions/10987444/how-to-use-global-variable-in-node-js
使用this你需要注意
一个常见的误解
避免多层this-例一
|
|
一个常见的误解是 test 中的 this 将会指向 Foo 对象,实际上不是这样子的。
大家也可以自己去试下,我直接贴我试的结果—
可以看到 Foo.method 这个函数中的 this 指向的是 Foo 这个对象,
而 Foo.method 中 test 函数的 this 指向的并不是Foo这个对象,而是 window 这个对象。
解决方法:为了在 test 中获取对 Foo 对象的引用,我们需要在 method 函数内部创建一个局部变量指向 Foo 对象。
|
|
that 只是我们随意起的名字,不过这个名字被广泛的用来指向外部的 this 对象。
避免多层this-例二
与例一类似的:
|
|
两层this,结果运行后,第一层指向该对象,第二层指向全局对象。
解决方法(同例一):在第二层改用一个指向外层this的变量, 定义变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。
|
|
避免数组处理方法中的this
数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。
|
|
上面代码中,foreach方法的参数函数中的this,其实是指向window对象,因此取不到o.v的值。
解决方案一:使用中间变量 - 这与避免多层this解决方法一样。
|
|
解决方案二:将this当作foreach方法的第二个参数,固定它的运行环境。
|
|
方法的赋值表达式
另一个看起来奇怪的地方是函数别名,也就是将一个方法赋值给一个变量。
|
|
上例中,test 就像一个普通的函数被调用;因此,函数内的 this 将不再被指向到 Foo 对象。
虽然 this 的晚绑定特性似乎并不友好,但是这确实基于原型继承赖以生存的土壤。
|
|
当 method 被调用时,this 将会指向 Bar 的实例对象。
避免回调函数中的this
回调函数中的this往往会改变指向,最好避免使用。
|
|
上面代码表示,如果调用o对象的f方法,其中的this就是指向o对象。
但是,如果将f方法指定给某个按钮的click事件,this的指向就变了。
$(“#button”).on(“click”, o.f);
点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的DOM对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。
为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性。
固定this的方法
this的动态切换,固然为JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this固定下来,反正出现意想不到的情况。
JavaScript提供了call、apply、bind这三个方法,来固定this的指向。
call方法
函数的call方法,可以改变指定该函数内部this的指向,然后再调用该函数。它的使用格式如下。
func.call(thisValue, arg1, arg2, …)
thisValue:第一个参数是this所要指向的那个对象。(如果this所要指向的那个对象,设定为null或undefined,则等同于指定全局对象。)
arg1,arg2, …:函数调用时所需的参数
例
|
|
上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。
如果使用call方法,将this关键字指向o对象,返回结果为456。
apply方法
apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。它的使用格式如下。
func.apply(thisValue, [arg1, arg2, …])
thisValue:第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。
[arg1, arg2, …]:第二个参数是一个数组,该数组的所有成员依次作为参数,传入原函数。
原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。
例
|
|
上面的f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。
利用这一点,可以做一些有趣的应用。
应用一:找出数组最大元素
JavaScript不提供找出数组最大元素的函数。结合使用apply方法和Math.max方法,就可以返回数组的最大元素。
|
|
上例等同于这样写
|
|
关于Math.max:
定义和用法:
max() 方法可返回两个指定的数中带有较大的值的那个数。语法:
Math.max(x…)参数:
0 或多个值。在 ECMASCript v3 之前,该方法只有两个参数。返回值:
参数中最大的值。如果没有参数,则返回 -Infinity。如果有某个参数为 NaN,或是不能转换成数字的非数字值,则返回 NaN。
应用二:将数组的空元素变为undefined
通过apply方法,利用Array构造函数将数组的空元素变成undefined。
|
|
关于Array构造函数:
创建 Array 对象的语法:
new Array();
new Array(size);
new Array(element0, element1, …, elementn);参数:
参数 size 是期望的数组元素个数。返回的数组,length 字段将被设为 size 的值。
参数 element …, elementn 是参数列表。当使用这些参数来调用构造函数 Array() 时,新创建的数组的元素就会被初始化为这些值。它的 length 字段也会被设置为参数的个数。返回值:
返回新创建并被初始化了的数组。
如果调用构造函数 Array() 时没有使用参数,那么返回的数组为空,length 字段为 0。
当调用构造函数时只传递给它一个数字参数,该构造函数将返回具有指定个数、元素为 undefined 的数组。
当其他参数调用 Array() 时,该构造函数将用参数指定的值初始化数组。
当把构造函数作为函数调用,不使用 new 运算符时,它的行为与使用 new 运算符调用它时的行为完全一样。
空元素与undefined的差别在于,数组的foreach方法会跳过空元素,但是不会跳过undefined。
因此,遍历内部元素的时候,会得到不同的结果。
|
|
应用三:转换类似数组的对象
利用数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组。
|
|
关于slice方法:
定义和用法:
slice() 方法可从已有的数组中返回选定的元素。语法
arrayObject.slice(start,end)参数
start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。返回值
返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。说明
请注意,该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法 Array.splice()。
slice例一:
|
|
slice例二:
|
|
应用四:绑定回调函数的对象
之前的按钮点击事件的例子,可以改写成
|
|
点击按钮以后,控制台将会显示true。由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。
更简洁的写法是采用下面介绍的bind方法。
bind方法
bind方法就是单纯地将函数体内的this绑定到某个对象,然后返回一个新函数。它的使用格式如下。
func.bind(thisValue, arg1, arg2,…)
它比call方法和apply方法更进一步的是,除了绑定this以外,还可以绑定原函数的参数。
请看下面的例子。
|
|
上面代码使用bind方法将o1.m方法绑定到o1以后,在o2对象上调用o1.m的时候,o1.m函数体内部的this.p就不再到o2对象去寻找p属性的值了。
如果bind方法的第一个参数是null或undefined,等于将this绑定到全局对象,函数运行时this指向全局对象(在浏览器中为window)。
|
|
使用bind时需注意
每一次返回一个新函数
bind方法每运行一次,就返回一个新函数,这会产生一些问题。
比如,监听事件的时候,不能写成下面这样。
|
|
上面代码表示,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。
|
|
正确的方法是写成下面这样:
|
|
bind方法的自定义代码
对于那些不支持bind方法的老式浏览器,可以自行定义bind方法。
|
|
jQuery的proxy方法
除了用bind方法绑定函数运行时所在的对象,还可以使用jQuery的$.proxy方法,它与bind方法的作用基本相同。
|
|
上面代码表示,$.proxy方法将o.f方法绑定到o对象。
结合call方法使用
利用bind方法,可以改写一些JavaScript原生方法的使用形式,以数组的slice方法为例。
|
|
上面的代码中,数组的slice方法从[1, 2, 3]里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]上面调用Array.prototype.slice方法,因此可以用call方法表达这个过程,得到同样的结果。
call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。
|
|
可以看到,利用bind方法,将[1, 2, 3].slice(0, 1)变成了slice([1, 2, 3], 0, 1)的形式。
这种形式的改变还可以用于其他数组方法。
|
|
如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写。
|
|
上面代码表示,将Function.prototype.call方法绑定Function.prototype.bind以后,bind方法的使用形式从f.bind(o),变成了bind(f, o)。
参考文献:
[1] 面向对象编程概述 - JavaScript 标准参考教程(alpha)
[2] this 的工作原理 - JavaScript 秘密花园
[3] Global Objects - Node.js v0.10.31 Manual & Documentation
[4] How to use global variable in node.js? - stackoverflow