一、this
JavaScript的 this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
this 的指向
除去不常用的 with 和 eval 的情况,具体到实际应用中, this 的指向大致可以分为以下 4种。
作为对象的方法调用。
作为普通函数调用。
构造器调用。
Function.prototype.call 或 Function.prototype.apply 调用。
1. 作为对象的方法调用
当函数作为对象的方法被调用时, this 指向该对象:
1 var obj = {
2 a: 1,
3 getA: function(){
4 alert ( this === obj ); // 输出:true
5 alert ( this.a ); // 输出: 1
6 }
7 };
8 obj.getA();
2. 作为普通函数调用当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指向全局对象。在浏览器的 JavaScript里,这个全局对象是 window 对象。
1 window.name = 'globalName';
2 var getName = function(){
3 return this.name;
4 };
5 console.log( getName() ); // 输出:globalName
6 //或者:
7 window.name = 'globalName';
8 var myObject = {
9 name: 'sven',
10 getName: function(){
11 return this.name;
12 }
13 };
14 var getName = myObject.getName;
15 console.log( getName() ); // globalName
有时候我们会遇到一些困扰,比如在 div 节点的事件函数内部,有一个局部的 callback 方法,callback 被作为普通函数调用时, callback 内部的 this 指向了 window ,但我们往往是想让它指向该 div 节点,见如下代码:
1 <html>
2 <body>
3 <div id="div1">我是一个 div</div>
4 </body>
5 <script>
6 window.id = 'window';
7 document.getElementById( 'div1' ).onclick = function(){
8 alert ( this.id ); // 输出:'div1'
9 var callback = function(){
10 alert ( this.id ); // 输出:'window'
11 }
12 callback();
13 };
14 </script>
15 </html
此时有一种简单的解决方案,可以用一个变量保存 div 节点的引用: (方法2:ES6中的箭头函数)
1 document.getElementById( 'div1' ).onclick = function(){
2 var that = this; // 保存 div 的引用
3 var callback = function(){
4 alert ( that.id ); // 输出:'div1'
5 }
6 callback();
7 };
在 ECMAScript 5的 strict 模式下,这种情况下的 this 已经被规定为不会指向全局对象,而是 undefined :
1 function func(){
2 "use strict"
3 alert ( this ); // 输出:undefined
4 }
5 func();
3. 构造器调用
JavaScript 中没有类,但是可以从构造器中创建对象,同时也提供了 new 运算符,使得构造器看起来更像一个类。
除了宿主提供的一些内置函数,大部分 JavaScript函数都可以当作构造器使用。
构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。
当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象,见如下代码:
1 var MyClass = function(){
2 this.name = 'sven';
3 };
4 var obj = new MyClass();
5 alert ( obj.name ); // 输出:sven
但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this :
1 var MyClass = function(){
2 this.name = 'sven';
3 return { // 显式地返回一个对象
4 name: 'anne'
5 }
6 };
7 var obj = new MyClass();
8 alert ( obj.name ); // 输出:anne
如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题:
1 var MyClass = function(){
2 this.name = 'sven'
3 return 'anne'; // 返回 string 类型
4 };
5 var obj = new MyClass();
6 alert ( obj.name ); // 输出:sven
4. Function.prototype.call 或 Function.prototype.apply 调用
跟普通的函数调用相比,用 Function.prototype.call 或 Function.prototype.apply 可以动态地改变传入函数的 this :
1 var obj1 = {
2 name: 'sven',
3 getName: function(){
4 return this.name;
5 }
6 };
7 var obj2 = {
8 name: 'anne'
9 };
10 console.log( obj1.getName() ); // 输出: sven
11 console.log( obj1.getName.call( obj2 ) ); // 输出:anne
call 和 apply 方法能很好地体现 JavaScript的函数式语言特性,在 JavaScript中,几乎每一次编写函数式语言风格的代码,都离不开 call 和 apply 。在 JavaScript 诸多版本的设计模式中,也用到了 call 和 apply 。
丢失的 this
1 var obj = {
2 myName: 'sven',
3 getName: function(){
4 return this.myName;
5 }
6 };
7 console.log( obj.getName() ); // 输出:'sven'
8 var getName2 = obj.getName; //普通函数调用方式
9 console.log( getName2() ); // 输出:undefined
再看另一个例子, document.getElementById 这个方法名实在有点过长,我们大概尝试过用一个短的函数来代替它,如同 prototype.js 等一些框架所做过的事情:
1 var getId = function( id ){
2 return document.getElementById( id );
3 };
4 getId( 'div1' );
5 //不采用以下方式
6 var getId = document.getElementById;
7 getId( 'div1' );
我们可以尝试利用 apply 把 document 当作 this 传入 getId 函数,帮助“修正” this :
1 document.getElementById = (function( func ){
2 return function(){
3 return func.apply( document, arguments );
4 }
5 })( document.getElementById );
6 var getId = document.getElementById;
7 var div = getId( 'div1' );
8 alert (div.id); // 输出: div1
二、call 和 apply
1.区别 ---- 仅在于传入参数形式的不同。
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组, apply 方法把这个集合中的元素作为参数传递给被调用的函数:
1 var func = function( a, b, c ){
2 alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
3 };
4 func.apply( null, [ 1, 2, 3 ] );
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数:
1 var func = function( a, b, c ){
2 alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
3 };
4 func.call( null, 1, 2, 3 );
当使用 call 或者 apply 的时候,如果我们传入的第一个参数为 null ,函数体内的 this 会指向默认的宿主对象,在浏览器中则是 window :
1 var func = function( a, b, c ){
2 alert ( this === window ); // 输出 true
3 };
4 func.apply( null, [ 1, 2, 3 ] );
但如果是在严格模式下,函数体内的 this 还是为 null :
1 var func = function( a, b, c ){
2 "use strict";
3 alert ( this === null ); // 输出 true
4 }
5 func.apply( null, [ 1, 2, 3 ] );
有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null 来代替某个具体的对象:
Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5
2.用途
1) 改变 this 指向
1 var obj1 = {
2 name: 'sven'
3 };
4 var obj2 = {
5 name: 'anne'
6 };
7 window.name = 'window';
8 var getName = function(){
9 alert ( this.name );
10 };
11 getName(); // 输出: window
12 getName.call( obj1 ); // 输出: sven
13 getName.call( obj2 ); // 输出: anne
在实际开发中,经常会遇到 this 指向被不经意改变的场景,比如有一个 div 节点, div 节点的 onclick 事件中的 this 本来是指向这个 div 的:
1 document.getElementById( 'div1' ).onclick = function(){
2 alert( this.id ); // 输出:div1
3 };
假如该事件函数中有一个内部函数 func ,在事件内部调用 func 函数时, func 函数体内的 this就指向了 window ,而不是我们预期的 div ,见如下代码:
1 document.getElementById( 'div1' ).onclick = function(){
2 alert( this.id ); // 输出:div1
3 var func = function(){
4 alert ( this.id ); // 输出:undefined
5 }
6 func();
7 };
这时候我们用 call 来修正 func 函数内的 this ,使其依然指向 div :
1 document.getElementById( 'div1' ).onclick = function(){
2 var func = function(){
3 alert ( this.id ); // 输出:div1
4 }
5 func.call( this );
6 };
以下也是:
1 document.getElementById = (function( func ){
2 return function(){
3 return func.apply( document, arguments );
4 }
5 })( document.getElementById );
6 var getId = document.getElementById;
7 var div = getId( 'div1' );
8 alert ( div.id ); // 输出: div1
2)Function.prototype.bind
1 Function.prototype.bind = function( context ){
2 var self = this; // 保存原函数
3 return function(){ // 返回一个新的函数
4 return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的 context
5 // 当作新函数体内的 this
6 }
7 };
8 var obj = {
9 name: 'sven'
10 };
11 var func = function(){
12 alert ( this.name ); // 输出:sven
13 }.bind( obj);
14 func();
在 Function.prototype.bind 的内部实现中,我们先把 func 函数的引用保存起来,然后返回一个新的函数。当我们在将来执行 func 函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部, self.apply( context, arguments ) 这句代码才是执行原来的 func 函数,并且指定 context对象为 func 函数体内的 this 。
这是一个简化版的 Function.prototype.bind 实现,通常我们还会把它实现得稍微复杂一点,使得可以往 func 函数中预先填入一些参数:
1 Function.prototype.bind = function(){
2 var self = this, // 保存原函数
3 context = [].shift.call( arguments ), // 需要绑定的 this 上下文
4 args = [].slice.call( arguments ); // 剩余的参数转成数组
5 return function(){ // 返回一个新的函数
6 return self.apply( context, [].concat.call( args, [].slice.call( arguments ) ) );
7 // 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this
8 // 并且组合两次分别传入的参数,作为新函数的参数
9 }
10 };
11 var obj = {
12 name: 'sven'
13 };
14 var func = function( a, b, c, d ){
15 alert ( this.name ); // 输出:sven
16 alert ( [ a, b, c, d ] ) // 输出:[ 1, 2, 3, 4 ]
17 }.bind( obj, 1, 2 );
18 func( 3, 4 );
3. 借用其他对象的方法
借用构造函数
1 var A = function( name ){
2 this.name = name;
3 };
4 var B = function(){
5 A.apply( this, arguments );
6 };
7 B.prototype.getName = function(){
8 return this.name;
9 };
10 var b = new B( 'sven' );
11 console.log( b.getName() ); // 输出: 'sven'
函数的参数列表 arguments 是一个类数组对象,虽然它也有“下标”,但它并非真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素。这种情况下,我们常常会借用 Array.prototype 对象上的方法。比如想往 arguments 中添加一个新的元素,通常会借用Array.prototype.push :
1 (function(){
2 Array.prototype.push.call( arguments, 3 );
3 console.log ( arguments ); // 输出[1,2,3]
4 })( 1, 2 );
想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;想截去arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法。那么这种机制的内部实现原理是什么呢?我们不妨翻开 V8的引擎源码,以 Array.prototype.push 为例,看看 V8引擎中的具体实现:
1 function ArrayPush() {
2 var n = TO_UINT32( this.length ); // 被 push 的对象的 length
3 var m = %_ArgumentsLength(); // push 的参数个数
4 for (var i = 0; i < m; i++) {
5 this[ i + n ] = %_Arguments( i ); // 复制元素 (1)
6 }
7 this.length = n + m; // 修正 length 属性的值 (2)
8 return this.length;
9 };
通过这段代码可以看到, Array.prototype.push 实际上是一个属性复制的过程,把参数按照下标依次添加到被 push 的对象上面,顺便修改了这个对象的 length 属性。至于被修改的对象是谁,到底是数组还是类数组对象,这一点并不重要。
由此可以推断,我们可以把“任意”对象传入 Array.prototype.push :
1 var a = {};
2 Array.prototype.push.call( a, 'first' );
3 alert ( a.length ); // 输出:1
4 alert ( a[ 0 ] ); // first
----《JavaScript设计模式与开发实践》读书笔记
浙公网安备 33010602011771号