第二章 this、call 和 apply
2.1 this
JavaScript 的 this
总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
2.1.1 this 的指向
除去不常用的 with
和 eval
的情况:
- 作为对象的方法调用
- 作为普通函数调用
- 构造器调用
Function.prototype.call
或Function.prototype.apply
调用
1. 作为对象的方法调用
1 | var obj = { |
2. 作为普通函数调用
指向全局对象,在浏览器的 JavaScript 中,这个全局对象是 window
对象。
1 | window.name = 'globalName'; |
或者:
1 | window.name = 'globalName'; |
有时,在 div
节点的事件函数内部,有一个局部的 callback
方法,callback
被作为普通函数调用时,callback
内部的 this
指向了 window
,但我们往往是想让它指向该 div
节点,此时可以用一个变量保存 div
节点的引用。在 ECMAScript 5 的 strict
模式下,这种情况下的 this
已经被规定为不会指向全局对象,而是 undefined
。
3. 构造器调用
除了宿主提供的一些内置函数,大部分 JavaScript 函数都可以当作构造器使用。构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用 new
运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this
就指返回的这个对象。
1 | var MyClass = function () { |
但用 new
调用构造器时,如果构造器显式地返回了一个 sven
类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this
:
1 | var MyClass = function () { |
如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题。
5. Function.prototype.call 或 Function.prototype.apply 调用
跟普通的函数调用相比,用 Function.prototype.call
或 Function.prototype.apply
可以动态地改变传入函数的 this
:
1 | var obj1 = { |
2.1.2 丢失的 this
1 | var obj = { |
当调用 obj.getName
时,getName
方法是作为 obj
对象的属性被调用的,此时的 this
指向 obj
对象,所以 obj.getName()
输出 sven
。当用另外一个变量 getName2
来引用 obj.getName
,并且调用 getName2
时,此时是普通函数调用方式,this
是指向全局 window
的,所以程序执行的结果是 undefined
。
1 | var getId = document.getElementById; |
上面这段代码抛出了一个错误,因为许多浏览器引擎的 getElementById
方法的内部实现中需要用到 this
。这个 this
本来被期望指向 document
,当 getElementById
方法作为 document
对象的属性被调用时,方法内部的 this
确实是指向 document
的。但当用 getId
来引用 document.getElementById
之后,再调用 getId
,此时就成了普通函数调用,函数内部的 this
指向了 window
,而不是原来的 document
。我们可以尝试利用 apply
把 document
当作 this
传入 getId
函数,帮助“修正” this
:
1 | document.getElementById = (function (fun) { |
2.2 call 和 apply
2.2.1 call 和 apply 的区别
Function.prototype.call
和 Function.prototype.apply
都是非常常用的方法。它们的作用一模一样,区别仅在于传入参数形式的不同。
apply
接受两个参数,第一个参数指定了函数体内 this
对象的指向,第二个参数为一个带下表的集合(数组或类数组),apply
方法把这个集合中的元素作为参数传递给被调用的函数:
1 | var func = function ( a, b, c ) { |
call
传入的参数数量不固定,跟 apply
相同的是,第一个参数也是代表函数体内的 this
指向,从第二个参数开始往后,每个参数被依次传入函数:
1 | var func = function ( a, b, c ) { |
当调用一个函数时,JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript 的参数在内部就是用一个数组来表示的。从这个意义上说,apply
比 call
的使用率更高。call
是包装在 apply
上的一颗语法糖。
当传入的第一个参数为 null
,函数体内的 this
会指向默认的宿主对象,在浏览器中就是 window
:
1 | var func = function ( a, b, c ) { |
但如果在严格模式下,函数体内的 this
还是为 null
:
1 | var func = function ( a, b, c ) { |
有时候我们使用 call
或者 apply
的目的不在于指定 this
指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null
来代替某个具体的对象:
1 | Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5 |
2.2.2 call 和 apply 的用途
1. 改变 this 指向
call
和 apply
最常见的用途是改变函数内部的 this
指向:
1 | var obj1 = { |
2. Function.prototype.bind
大部分高级浏览器都实现了内置的 Function.prototype.bind
,用来指定函数内部的 this
指向,即使没有原生的实现,也可以模拟一个:
1 | Function.prototype.bind = function ( context ) { |
通常会实现得稍微复杂一些:
1 | Function.prototype.bind = function () { |
3. 借用其他对象的方法
借用构造函数可以实现一种类似继承的效果:
1 | var A = function ( name ) { |
函数的参数列表 arguments 是一个类数组对象,虽然它也有“下标”,但它并非真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素。在操作 arguments
时,我们常常会借用 Array.prototype
对象上的方法。
1 | (function () { |
以 Array.prototype.push
为例,看看 V8 引擎中的具体实现:
1 | function ArrayPush () { |
Array.prototype.push
实际上是一个属性复制的过程,把参数按照下标一次添加到被 push
的对象上面,顺便修改了这个对象的 length
属性。至于被修改的对象是数组还是类数组对象,并不重要。
1 | var a = {}; |
如果在低版本的 IE 浏览器中执行,必须显式地给对象 a
设置 length
属性:
1 | var a = { |
可以借用 Array.prototype.push
方法的对象还要满足以下两个条件:
- 对象本身要可以存取属性;
- 对象的
length
属性可读写。