对于this指向的理解中,有这样一种说法:谁调用它,this就指向谁。
一、执行上下文
在这之前,我们需要来回顾一下执行上下文。
在执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。
首先我们需要得出一个非常重要一定要牢记于心的结论,this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。因此,一个函数中的this指向,可以是非常灵活的。比如下面的例子中,同一个函数由于调用方式的不同,this指向了不一样的对象。
var a = 10;
var obj = {
a: 20
}
function fn () {
console.log(this.a);
}
fn(); // 10
fn.call(obj); // 20
fn.call(this) // 10
二、函数中的this
在总结函数中this指向之前,我想我们有必要通过一些奇怪的例子,来感受一下函数中this的捉摸不定。
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
console.log(obj.c); // 40
console.log(obj.fn()); // 10
console.log(fn()); // 20,等价 windon.fn()
分析之前,我们先直接了当抛出结论。
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
从结论中我们可以看出,想要准确确定this指向,找到函数的调用者以及区分他是否是独立调用就变得十分关键。
在上面的例子中,对象 obj
中的 c
属性使用 this.a + 20
来计算。这里我们需要明确的一点是,单独的 {}
是不会形成新的作用域的,因此这里的 this.a
,由于并没有作用域的限制,所以它仍然处于全局作用域之中。所以这里的 this
其实是指向的 window
对象。
三、构造函数与原型方法上的this
在封装对象的时候,我们几乎都会用到 this
,但是,只有少数人搞明白了在这个过程中的 this
指向,就算我们理解了原型,也不一定理解了 this
。所以这一部分,我认为将会为这篇文章最重要最核心的部分。理解了这里,将会对你学习JS面向对象产生巨大的帮助。
结合下面的例子,我在例子抛出几个问题大家思考一下。
function Person(name, age) {
// 这里的this指向了谁?
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
// 这里的this又指向了谁?
return this.name;
}
// 上面的2个this,是同一个吗,他们是否指向了原型对象?
var p1 = new Person('Nick', 20);
p1.getName();
我们已经知道,this
是在函数调用过程中确定,因此,搞明白 new
的过程中到底发生了什么就变得十分重要。
通过 new
操作符调用构造函数,会经历以下4个阶段。
- 创建一个新的对象;
- 将构造函数的
this
指向这个新对象; - 指向构造函数的代码,为这个对象添加属性,方法等;
- 返回新对象。
因此,当 new
操作符调用构造函数时,this
其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象 p1
接收。因此,我们可以说,这个时候,构造函数的 this
,指向了新的实例对象 p1
。
而原型方法上的 this
就好理解多了,根据上边对函数中 this
的定义, p1.getName()
中的 getName
为调用者,他被 p1
所拥有,因此 getName
中的 this
,也是指向了 p1
。
四、绑定 this 的方法
1. function.prototype.call()
函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
var obj = {};
var f = function () {
return this;
};
f() === this // true
f.call(obj) === obj // true
上面代码中,在全局环境运行函数f时,this指向全局环境;call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f。
call方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。如果使用call方法将this关键字指向obj对象,返回结果为456。可以看到,如果call方法没有参数,或者参数为null或undefined,则等同于指向全局对象。
如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法。
var f = function () {
return this;
};
f.call(5)
// Number {[[PrimitiveValue]]: 5}
上面代码中,call的参数为5,不是对象,会被自动转成包装对象(Number的实例),绑定f内部的this。
call方法还可以接受多个参数。
func.call(thisValue, arg1, arg2, ...)
call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
上面代码中,call方法指定函数add内部的this绑定当前环境(对象),并且参数为1和2,因此函数add运行后得到3。
call方法的一个应用是调用对象的原生方法。
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false
上面代码中,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。
2. function.prototype.apply()
apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。
请看下面的例子。
function f(x,y){
console.log(x+y);
}
f.call(null,1,1) // 2
f.apply(null,[1,1]) // 2
上面的f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。
利用这一点,可以做一些有趣的应用。
(1)找出数组最大元素
JavaScript不提供找出数组最大元素的函数。结合使用apply方法和Math.max方法,就可以返回数组的最大元素。
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a)
// 15
(2)将数组的空元素变为undefined
通过apply方法,利用Array构造函数将数组的空元素变成undefined。
Array.apply(null, ["a",,"b"])
// [ 'a', undefined, 'b' ]
空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined。因此,遍历内部元素的时候,会得到不同的结果。
var a = ['a', , 'b'];
function print(i) {
console.log(i);
}
a.forEach(print)
// a
// b
Array.apply(null, a).forEach(print)
// a
// undefined
// b
(3)转换类似数组的对象
另外,利用数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组。
Array.prototype.slice.apply({0:1,length:1})
// [1]
Array.prototype.slice.apply({0:1})
// []
Array.prototype.slice.apply({0:1,length:2})
// [1, undefined]
Array.prototype.slice.apply({length:1})
// [undefined]
上面代码的apply方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length属性,以及相对应的数字键。
(4)绑定回调函数的对象
上一节按钮点击事件的例子,可以改写成
var o = new Object();
o.f = function () {
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
$('#button').on('click', f);
点击按钮以后,控制台将会显示true。由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的bind方法。
3.function.prototype.bind()
bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。
var d = new Date();
d.getTime() // 1481869925657
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
上面代码中,我们将d.getTime方法赋给变量print,然后调用print就报错了。这是因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。
bind方法可以解决这个问题,让log方法绑定console对象。
var print = d.getTime.bind(d);
print() // 1481869925657
上面代码中,bind方法将getTime方法内部的this绑定到d对象,这时就可以安全地将这个方法赋值给其他变量了。
下面是一个更清晰的例子。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
counter.count // 0
counter.inc()
counter.count // 1
上面代码中,counter.inc内部的this,默认指向counter对象。如果将这个方法赋值给另一个变量,就会出错。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc;
func();
counter.count // 0
count // NaN
上面代码中,函数func是在全局环境中运行的,这时inc内部的this指向顶层对象window,所以counter.count是不会变的,反而创建了一个全局变量count。因为window.count原来等于undefined,进行递增运算后undefined++就等于NaN。
为了解决这个问题,可以使用bind方法,将inc内部的this绑定到counter对象。
var func = counter.inc.bind(counter);
func();
counter.count // 1
上面代码中,bind方法将inc方法绑定到counter以后,再运行func就会得到正确结果。
this绑定到其他对象也是可以的。
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
上面代码中,bind方法将inc方法内部的this,绑定到obj对象。结果调用func函数以后,递增的就是obj内部的count属性。
bind比call方法和apply方法更进一步的是,除了绑定this以外,还可以绑定原函数的参数。
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5)
// 20
上面代码中,bind方法除了绑定this对象,还将add函数的第一个参数x绑定成5,然后返回一个新函数newAdd,这个函数只要再接受一个参数y就能运行了。
如果bind方法的第一个参数是null或undefined,等于将this绑定到全局对象,函数运行时this指向顶层对象(在浏览器中为window)。
function add(x, y) {
return x + y;
}
var plus5 = add.bind(null, 5);
plus5(10) // 15
上面代码中,函数add内部并没有this,使用bind方法的主要目的是绑定参数x,以后每次运行新函数plus5,就只需要提供另一个参数y就够了。而且因为add内部没有this,所以bind的第一个参数是null,不过这里如果是其他对象,也没有影响。
对于那些不支持bind方法的老式浏览器,可以自行定义bind方法。
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
var fn = this;
var context = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
return function(){
return fn.apply(context, args);
}
}
}
bind方法有一些使用注意点。
(1)每一次返回一个新函数
bind方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样。
element.addEventListener('click', o.m.bind(o));
上面代码中,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的。
element.removeEventListener('click', o.m.bind(o));
正确的方法是写成下面这样:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
。。。