2

在 JavaScript 编程中,this 关键字总是让初学者感到迷惑,Function.prototype.call 和
Function.prototype.apply 这两个方法也有着广泛的运用。我们有必要在学习设计模式之前先理解
这几个概念。

2.1 this
跟别的语言大相径庭的是,JavaScript 的 this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

2.1.1 this的指向
除去不常用的 with 和 eval 的情况,具体到实际应用中,this 的指向大致可以分为以下 4 种。
** 作为对象的方法调用。
 作为普通函数调用。
 构造器调用。
 Function.prototype.call 或 Function.prototype.apply 调用。**

  1. 作为对象的方法调用

当函数作为对象的方法被调用时,this 指向该对象:

var obj = { 
 a: 1, 
 getA: function(){ 
 alert ( this === obj ); // 输出:true 
 alert ( this.a ); // 输出: 1 
 } 
}; 
obj.getA();
  1. 作为普通函数调用

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指
向全局对象。在浏览器的 JavaScript 里,这个全局对象是 window 对象。

window.name = 'globalName'; 
var getName = function(){ 
 return this.name; 
}; 
console.log( getName() ); // 输出:globalName
或者
window.name = 'globalName123'; 
function getName (){ 
 return this.name; 
}; 
console.log( getName() ); // 输出:globalName123

或者:

window.name = 'globalName'; 
var myObject = { 
 name: 'sven', 
 getName: function(){ 
 return this.name; 
 } 
}; 
var getName = myObject.getName; 
console.log( getName() ); // globalName

注解:因为getName()调用,只是普通函数的调用,所以这里函数内的this是指向全局的。var getName = myObject.getName就是把myObject对象的getName属性方法给getName。因为这里的this 是指向全局,所以结果是globalName。

有时候我们会遇到一些困扰,比如在 div 节点的事件函数内部,有一个局部的 callback 方法,
callback 被作为普通函数调用时,callback 内部的 this 指向了 window,但我们往往是想让它指向
该 div 节点,见如下代码:

<html> 
 <body> 
 <div id="div1">我是一个 div</div> 
 </body> 
 <script> 
 window.id = 'window'; 
 document.getElementById( 'div1' ).onclick = function(){ 
 alert ( this.id ); // 输出:'div1' 
 var callback = function(){ 
 alert ( this.id ); // 输出:'window' 
 } 
 callback(); 
 }; 
 </script> 
</html> 

此时有一种简单的解决方案,可以用一个变量保存 div 节点的引用:
图灵社区会员 轩辕 专享 尊重版权
26 第 2 章 this、call 和 apply

document.getElementById( 'div1' ).onclick = function(){ 
 var that = this; // 保存 div 的引用
 var callback = function(){ 
 alert ( that.id ); // 输出:'div1' 
 } 
 callback(); 
};

在 ECMAScript 5 的 strict 模式下,这种情况下的 this 已经被规定为不会指向全局对象,而
是 undefined:

function func(){ 
 "use strict" 
 alert ( this ); // 输出:undefined 
} 
func();
  1. 构造器调用

JavaScript 中没有类,但是可以从构造器中创建对象,同时也提供了 new 运算符,使得构造
器看起来更像一个类。
除了宿主提供的一些内置函数,大部分 JavaScript 函数都可以当作构造器使用。构造器的外
表跟普通函数一模一样,它们的区别在于被调用的方式。当用 new 运算符调用函数时,该函数总
会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象,见如下代码:

var MyClass = function(){ 
 this.name = 'sven'; 
}; 
var obj = new MyClass(); 
alert ( obj.name ); // 输出:sven 

注释:我们这里要知道new的操作符都为我们做了哪4件事。
new操作符干了以下三步:
1.先创建了一个新的空对象
2.然后让这个空对象的__proto__指向函数的原型prototype
3.将对象作为函数的this传进去,如果return 出来东西是对象的话就直接返回 return 的内容,没有的话就返回创建的这个对象

对应伪代码:

对于const a = new Foo();,new干了以下事情
 const o = new Object();//创建了一个新的空对象o
 o.__proto__ = Foo.prototype;//让这个o对象的 __proto__指向函数的原型prototype
 Foo.call(o);//this指向o对象
 a = o;//将o对象赋给a对象

但用 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
  1. 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 

call 和 apply 方法能很好地体现 JavaScript 的函数式语言特性,在 JavaScript 中,几乎每一次
编写函数式语言风格的代码,都离不开 call 和 apply。在 JavaScript 诸多版本的设计模式中,也
用到了 call 和 apply。在下一节会详细介绍它们。

2.1.2 丢失的this
这是一个经常遇到的问题,我们先看下面的代码:

var obj = { 
 myName: 'sven', 
 getName: function(){ 
 return this.myName; 
 } 
}; 
console.log( obj.getName() ); // 输出:'sven' 
var getName2 = obj.getName; 
console.log( getName2() ); // 输出:undefined 

当调用 obj.getName 时,getName 方法是作为 obj 对象的属性被调用的,根据 2.1.1 节提到的规
律,此时的 this 指向 obj 对象,所以 obj.getName()输出'sven'。

当用另外一个变量 getName2 来引用 obj.getName,并且调用 getName2 时,根据 2.1.2 节提到的
规律,此时是普通函数调用方式,this 是指向全局 window 的,所以程序的执行结果是 undefined。

2.2.1 call和apply的区别
Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法。它们的作用一模
一样,区别仅在于传入参数形式的不同。
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下
标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传
递给被调用的函数:

var func = function( a, b, c ){ 
 alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ] 
}; 
func.apply( null, [ 1, 2, 3 ] );

在这段代码中,参数 1、2、3 被放在数组中一起传入 func 函数,它们分别对应 func 参数列
表中的 a、b、c。
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 来传送参数。

  1. Function.prototype.bind

大部分高级浏览器都实现了内置的 Function.prototype.bind,用来指定函数内部的 this指向,
即使没有原生的 Function.prototype.bind 实现,我们来模拟一个也不是难事,代码如下:
图灵社区会员 轩辕 专享 尊重版权
32 第 2 章 this、call 和 apply

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(); 

我们通过 Function.prototype.bind 来“包装”func 函数,并且传入一个对象 context 当作参
数,这个 context 对象就是我们想修正的 this 对象。
在 Function.prototype.bind 的内部实现中,我们先把 func 函数的引用保存起来,然后返回一
个新的函数。当我们在将来执行 func 函数时,实际上先执行的是这个刚刚返回的新函数。在新
函数内部,self.apply( context, arguments )这句代码才是执行原来的 func 函数,并且指定 context
对象为 func 函数体内的 this。
这是一个简化版的 Function.prototype.bind 实现。

call是改变函数内部的this指向的。


用户bPQb0e
133 声望5 粉丝

下一篇 »
Webpack