this

跟别的语言不同的是,JavaScriptthis 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

this的指向

除去不常用的 evalwith 语句的情况,具体到实际应用中,this 的指向大致可分为以下4种:

  • 作为对象的方法调用

    当作为对象的方法进行调用的时候,this 指向该对象:

    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {
    name: 'bubuzou',
    getName: function() {
    console.log( this === obj ); // true
    console.log( this.name ); // 'bubuzou'
    }
    };
    obj.getName();
  • 作为普通函数调用

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    window.name = 'bubuzou.com';
    var getName = function() {
    return this.name;
    };
    console.log( getName() ); // 'bubuzou.com'
    // 或者
    window.name = 'bubuzou.com';
    var myObject = {
    name: 'bubuzou',
    getName: function() {
    return this.name;
    }
    };
    var getName = myObject.getName;
    console.log( getName() ); // 'bubuzou.com'

    再来看一个实际的应用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    window.id = 'bubuzou';
    document.getElementById( 'div' ).onclick = function() {
    var that = this;
    console.log( this.id ); // 'div'
    var callback = function() {
    console.log( this.id ); // 'bubuzou'
    console.log( that.id ); // 'div'
    };
    callback();
    };
    在 `ECMAScript 5` 的 `strict` 模式下,这种情况的 `this` 已经被规定不会指向全局对象了,而是 `undefined`
    1
    2
    3
    4
    (function func() {
    "use strict"
    console.log( this ); // undefined
    })();
  • 构造器调用

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

    1
    2
    3
    4
    5
    var MyClass = function() {
    this.name = 'bubuzou.com'
    };
    var obj = new MyClass();
    console.log( obj.name ); // 'bubuzou.com'

    但是使用构造器调用需要注意一个问题,如果构造器显示的返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们所期待的 this:

    1
    2
    3
    4
    5
    6
    7
    8
    var MyClass = function() {
    this.name = 'bubuzou.com';
    return {
    name: 'bubuzou'
    }
    };
    var obj = new MyClass();
    console.log( obj.name ); // 'bubuzou'
  • Function.prototype.callFunction.prototype.apply 调用

    跟普通的函数调用相比,用 Function.prototype.callFunction.prototype.apply 调用可以动态地改变传入函数的 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var obj1 = {
    name: 'bubuzou.com',
    getName: function() {
    return this.name;
    }
    };
    var obj2 = {
    name: 'bubuzou'
    };
    console.log( obj1.getName() ); // 'bubuzou.com'
    console.log( obj1.getName.call( obj2 ) ); // 'bubuzou'

丢失的this

document.getElementById 这个方法名有点长,所以我们会尝试用一个简单点的来替代:

1
2
3
4
var getId = function( id ) {
return document.getElementById( id );
};
getId( 'div' );

我们也许思考过为什么不能用下面的这个更简单的方式:

1
2
var getId = document.getElementById;
getId( 'div' );

当我们执行上面这段代码的时候,在 Chrome 浏览器中会抛出异常。这是因为许多浏览器引擎的 document.getElementById 方法的内部实现中需要用到 this。而这个 this 本来期望指向 document,当getElementById 方法作为 document 的属性来调用的时候,内部的 this 确实是指向 document 的。但当用getId 来引用 document.getElementById 之后,再调用 getId,此时就成了普通函数调用,函数内部的 this 指向了 window,而是不是原来的 document,所以会抛出异常。
不过我们可以尝试用 applydocument 当做 this 传入 getId 函数:

1
2
3
4
5
6
7
document.getElementById = (function( func ) {
return function() {
return func.apply( document, arguments );
}
})( document.getElementById );
var getId = document.getElementById;
console.log( getId( 'div' ).id ); // 'div'

call和apply的区别

两者的作用一模一样,区别只是传入的参数形式不一样。
apply接受2个参数,第一个参数是指定了函数体内的 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以是一个数组也可以是一个类数组。
call 的参数就不固定了,但是第一个参数和 apply 是一样的,从第二个参数开始往后,每个参数被依次传入函数。

1
2
3
4
5
var func = function( a, b, c ) {
console.log( [a, b, c] );
};
func.apply( null, [1, 2, 3] ); // [1, 2, 3]
func.call( null, 1, 2, 3 ); // [1, 2, 3]

当调用一个函数的时候,JavaScript 的解释器并不会计较形参和实参在数量、类型和顺序上的区别,从这个意义上来说,applycall 的使用率更高。但是如果我们明确指定参数的个数,也可以用 call 来传送参数。

如果第一个参数是 `null`,那么函数体内的 `this` 会指向默认的宿主对象,在浏览器中是 `window`,但如果是在严格模式下,则还是为 `null`

call和apply的用途

  • 改变 this 指向

    callapply 最常见的用途是改变函数内部的 this 指向:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    window.name = 'bubuzou';
    var obj = {
    name: 'bubuzou.com'
    };
    var getName = function() {
    console.log( this.name );
    };
    getName(); // 'bubuzou'
    getName.apply( obj ); // 'bubuzou.com'
  • Function.prototype.bind

    大部分高级的浏览器都实现内置的 Function.prototype.bind ,用来指定函数内部的 this 指向,即使没有原生的Function.prototype.bind 实现,我们来模拟实现一个也不是难事:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Function.prototype.bind = function( context ) {
    var self = this;
    return function() {
    return self.apply( context, arguments );
    }
    };
    var obj = {
    name: 'bubuzou.com'
    };
    var func = function() {
    console.log( this.name );
    }.bind( obj );
    func(); // 'bubuzou.com'

    这是一个简化版的实现,我们可以实现得更复杂一点,使得可以往 func 函数中预先填入一些参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Function.prototype.bind = function() {
    var self = this,
    context = [].shift.call( arguments ),
    args = [].slice.call( arguments );
    return function() {
    return self.apply( context, [].concat.call( args, [].slice.call( arguments )));
    }
    };
    var obj = {
    name: 'bubuzou.com'
    };
    var func = function( a, b, c, d ) {
    console.log( this.name ); // 'bubuzou.com'
    console.log( [a, b, c, d] ); // [1, 2, 3, 4]
    }.bind(obj, 1, 2);
    func(3, 4);
  • 借用其他对象的方法

    我们知道,杜鹃既不会筑巢,也不会孵雏,而是把自己的蛋寄托给云雀等其他鸟类,让他们代为孵化和养育。同样,在JavaScript中也存在类似的借用现象。
    借用的第一种场景是“借用构造函数”,通过这种技术,可以实现一些类似继承的效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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( 'bubuzou.com' );
    console.log( b.getName() ); // 'bubuzou.com'

    借用方法的第二种运用场景跟我们的关系更加密切。函数的参数列表 arguments 是一个类数组对象,所以不能像数组一样,进行排序操作或往集合里添加一个新的元素。在这种情况下,我们通常会借用 Array.prototype 对象上的方法。比如像往arguments 里添加一个元素:

    1
    2
    3
    4
    (function() {
    Array.prototype.push.call( arguments, 3);
    console.log( arguments ); // [1, 2, 3]
    })(1, 2);

    先来看一个例子:

    1
    2
    3
    4
    var a = {};
    Array.prototype.push.call( a, 'first' );
    console.log( a.length ); // 1
    console.log( a[0] ); // 'first'

    上面这段代码在低版本IE浏览器中执行的时候,必须显式地给对象a设置length属性:

    1
    2
    3
    var a = {
    length: 0
    };

    然而并不是所有的对象都可以传入Array.prototype.push,必须满足以下2个条件:

    • 对象本身可以存取属性

      如果不是一个 object 类型的对象而是 number 类型,我们来看看会怎么样:

      1
      2
      3
      4
      var a = 1;
      Array.prototype.push.call( a, 'first' );
      console.log( a.length ); // undefined
      console.log( a[0] ); // undefined
    • 对象的 length 属性可以读写
      函数的 length 就是只有一个只读的属性,表示形参的个数,我们尝试把一个函数当做 this 传入Array.prototype.push:

      1
      2
      3
      var func = function(){};
      Array.prototype.push.call( func, 'first' );
      console.log( func.length ); // 报错:Cannot assign to read only property 'length' of function