在 JavaScript 编程中,this 关键字总是让初学者感到迷惑,Function.prototype.call 和Function.prototype.apply 这两个方法也有着广泛的运用。我们有必要在学习设计模式之前先理解这几个概念。
this
Javascript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
this的指向
this的指向大致可以分为以下4种:
1.作为对象的方法调用
2.作为普通函数调用
3.构造器调用
4.Function.prototype.call或Function.prototype.apply调用。
下面我们分别进行介绍。
当函数作为对象的方法被调用时,this 指向该对象
var obj = {
a: 1,
getA: function(){
alert ( this === obj ); // 输出:true
alert ( this.a ); // 输出: 1
}
}
obj.getA();
当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指
向全局对象。在浏览器的 JavaScript 里,这个全局对象是 window 对象。
window.name = 'globalName';
var getName = function(){
return this.name;
};
console.log( getName() ); // 输出:globalName
或者:
window.name = 'globalName';
var myObject = {
name: 'sven',
getName: function(){
return this.name;
}
};
var getName = myObject.getName;
console.log( getName() ); // globalName
在 ECMAScript 5 的 strict 模式下,这种情况下的 this 已经被规定为不会指向全局对象,而是 undefined:
function func(){
"use strict"
alert ( this ); // 输出:undefined
}
func();
JavaScript 中没有类,但是可以从构造器中创建对象,同时也提供了 new 运算符,使得构造器看起来更像一个类。
除了宿主提供的一些内置函数,大部分 JavaScript 函数都可以当作构造器使用。构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象,见如下代码:
var MyClass=function(){
this.name='sven';
};
var obj=new MyClass();
alert( obj.name ); // 输出:sven
但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:
var MyClass=function(){
this.name='sven';
return { // 显示地返回一个对象
name:'anne'
}
};
var obj=new MyClass();
alert( obj.name ); // 输出:anne
如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题
var MyClass=function(){
this.name='sven';
return 'anne'; // 返回string类型
};
var obj=new MyClass();
alert( obj.name ); // 输出:sven
4.Function.prototype.call或Function.prototype.apply调用
跟普通的函数调用相比,用Function.prototype.call或Function.prototype.apply可以动态地改变传入函数的this:
var obj1={
name:'sven',
getName:function(){
return this.name;
}
};
var obj2={
name:'anne'
};
console.log( obj1.getName() ); // 输出:sven
console.log( obj1.getName.call( obj2 ) ); // 输出 :anne
在JavaScript中,几乎每一次编写函数式语言风格的代码,都离不开call和apply,在JavaScript诸多版本的设计模式中,也会用到call和apply。
丢失的this
我们来看一个例子,document.getElementById这个方法名实在有点长,我们用一个短函数来代替:
var getId=function( id ){
return document.getElementById( id);
}
getId( 'div1');
我们也许思考过为什么不能用下面这种更简单的方式:
var getId = document.getElementById;
getId( 'div1' );
很明显这种方式会报错,document.getElementById 方法的内部实现中需要用到 this。这个 this 本来被期望指向document,当 getElementById 方法作为 document 对象的属性被调用时,方法内部的 this 确实是指向 document 的,但当用 getId 来引用document.getElementById 之后,再调用 getId,此时就成了普通函数调用,函数内部的 this 指向了 window,而不是原来的 document。
我们可以尝试利用 apply 把 document 当作 this 传入 getId 函数,帮助“修正”this:
document.getElementById=( function ( func ) {
return function(){
return func.apply(document,arguments);
}
})( document.getElementById );
var getId=document.getElementById;
var div=getId( 'div1' );
alert( div.id ); // 输出:div1
call和apply的区别在于传参的形式不同
apply接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数:
var func = function( a, b, c ){
alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] );
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数:
var func = function( a, b, c ){
alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.call( null, 1, 2, 3 );
当调用一个函数时,JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript 的参数在内部就是用一个数组来表示的。从这个意义上说,apply 比 call 的使用率更高,我们不必关心具体有多少参数被传入函数,只要用 apply 一股脑地推过去就可以了。
call 是包装在 apply 上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想一目了然地表达形参和实参的对应关系,那么也可以用 call 来传送参数。
当使用 call 或者 apply 的时候,如果我们传入的第一个参数为 null,函数体内的 this 会指向默认的宿主对象,在浏览器中则是 window:
var func = function( a, b, c ){
alert ( this === window ); // 输出 true
};
func.apply( null, [ 1, 2, 3 ] );
但如果是在严格模式下,函数体内的 this 还是为 null:
var func = function( a, b, c ){
"use strict";
alert ( this === null ); // 输出 true
}
func.apply( null, [ 1, 2, 3 ] );
有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null 来代替某个具体的对象:
Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5
1. 改变 this 指向
在实际开发中,经常会遇到 this 指向被不经意改变的场景,比如有一个 div 节点,div 节点
的 onclick 事件中的 this 本来是指向这个 div 的:
document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 输出:div1
};
假如该事件函数中有一个内部函数 func,在事件内部调用 func 函数时,func 函数体内的 this
就指向了 window,而不是我们预期的 div,见如下代码
document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 输出:div1
var func = function(){
alert ( this.id ); // 输出:undefined
}
func();
};
这时候我们用 call 来修正 func 函数内的 this,使其依然指向 div:
document.getElementById( 'div1' ).onclick = fuction(){
var func = function(){
alert ( this.id ); // 输出:div1
}
func.call( this );
};
2. Function.prototype.bind
大部分高级浏览器都实现了内置的 Function.prototype.bind,用来指定函数内部的 this指向,
即使没有原生的 Function.prototype.bind 实现,我们来模拟一个也不是难事,代码如下:
Function.prototype.bind = function( context ){
var self = this; // 保存原函数
return function(){ // 返回一个新的函数
return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的 context,当作新函数体内的 this }
}
}
var obj = {
name: 'sven'
};
var func = function(){
alert ( this.name ); // 输出:sven
}.bind( obj);
func();
3.借用其他对象的方法
借用方法的第一种场景是“借用构造函数”,通过这种技术,可以实现一些类似继承的效果:
var A = function( name ){
this.name = name;
};
var B = function(){
A.apply( this, arguments );
};
B.prototype.getName = function(){
return this.name;
};
var b = new B( 'sven' );
console.log( b.getName() ); // 输出: 'sven
函数的参数列表 arguments 是一个类数组对象,虽然它也有“下标”,但它并非真正的数组,
所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素。这种情况下,我们常常
会借用 Array.prototype 对象上的方法。比如想往 arguments 中添加一个新的元素,通常会借用
Array.prototype.push
(function(){
Array.prototype.push.call( arguments, 3 );
console.log ( arguments ); // 输出[1,2,3]
})( 1, 2 );
在操作 arguments 的时候,我们经常非常频繁地找 Array.prototype 对象借用方法
想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;想截去
arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法。
小结:call 和 apply 方法能很好地体现 JavaScript 的函数式语言特性,在 JavaScript 诸多版本的设计模式中,也
用到了 call 和 apply。