一、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设计模式与开发实践》读书笔记

posted on 2018-07-10 16:25  小小慧house  阅读(85)  评论(0)    收藏  举报