一、前序
在之前的博文中我们讲到函数在调用的时候会创建执行上下文,而执行上下文中主要有三个方面组成(创建作用域链、生成变变量对象以及确定this指向)
从上面这张执行上下文的生命周期图可以看到,执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。其中变量对象与作用域链我们都已经明白了(如果有不太清楚的同学,可以去看之前的博文(js基础-执行上下文/执行上下文栈)),本文的关键,就是确定this指向。
从上面可以得出一条非常重要的结论,对于我们来说一定要记住:
结论:this指向是在函数被调用时确定的,也就是执行上下文被创建时确定的
我们要知道一个函数中的this指向,可以非常灵活。比如下面的例子中,同一个函数由于调用方式的不同,this指向了不一样的对象
var b = 1
var obj = {
b: 2
}
function fn() {
console.log(this.b);
}
fn(); // 1
fn.call(obj); // 2
fn.apply(obj); // 2
这个例子是不是很easy,我们对上面的代码做下改动:在fn函数体中,我们试图去修改fn函数中的this指向,看看会不会生效
结论:在函数执行过程中,this一旦被确定,就不可更改了,否则会报错
好,接下来我们就详细地来看看各种地方的this的指向问题:
二、全局对象中的this
全局环境中的this,指向它本身,也就是window对象
// 通过this绑定到全局对象
this.a1 = 111;
// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a2= 222;
// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 333;
// 输出结果会全部符合预期
console.log(window.a1); //111
console.log(window.a2); //222
console.log(window.a3); //333
三、普通函数中的this
在聊函数中的this指向问题前,这里我先把判断的方法(结论)给出:
结论:
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
好了,我们知道了这个判定方法后,我们再来看几个栗子:
var name = "哈哈";
function fn() {
function sayName() {
console.log(this.name);
}
sayName();
}
fn(); // 哈哈
代码解析:上面code的执行顺序就是全局变量name和函数fn定义,然后执行fn函数,当执行到fn()时,会进一步执行fn内部的代码,内部又定义了一个sayName函数,然后执行sayName函数,打印输出this.name,所以上述代码中的sayName函数就是调用函数,它在调用时时独立调用的,所以该sayName函数内部的this就指向undefined,由于在非严格模式中,当this指向undefined时,它会被自动指向全局对象,所以最终打印输出的是全局中的name值“哈哈”
是不是挺简单的,我们继续趁热打铁,再看下面例子👇🏻
var a = 111;
var obj = {
a: 222,
c: this.a + 100,
fn: function () {
return this.a;
}
}
console.log(obj.c); //211
console.log(obj.fn()); //222
代码解析:
打印输出obj.c的结果取决于这个的this指向谁,由于obj对象的{}不会形成新的作用域,所以此时还是属于全局作用域下,所以这里的this还是指向全局对象window,所以c属性的值为111+100 =211,当输出打印obj.fn()时,此时fn函数为调用函数,它在执行调用时属于obj对象中的一个属性,所以该函数中this指向为obj对象,所以最终结果为222
再来看个容易搞混的例子:
function fn() {
console.log(this.a)
}
function bigFn(arg) {
arg(); // 真实调用者,为独立调用
}
var a = 100;
var obj = {
a: 10,
myFn: fn
}
bigFn(obj.myFn); //100
代码解析:
当执行bigFn(obj.myFn)时,()里面传的相当于是fn函数的一个指针引用,然后在bigFn函数中用arg形参接收这个在外面全局定义的fn函数指针引用,然后在bigFn函数中执行arg()相当于直接执行了fn(),所以此时fn为真正的调用者函数,它属于独立调用,所以它内部的this指向undefined,非严格模式下指向window对象,所以最终结果为100
四、使用call,apply、bind改变this指向
JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有这两个方法。它们除了参数略有不同之外,其功能完全一样。它们的第一个参数都为this将要指向的对象。
如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了
function fn() {
console.log(this.a);
}
var obj = {
a: 100
}
fn.call(obj); // 100
call与applay除第一个参数以外后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同。
function fn(n1, n2) {
console.log(this.a + n1 + n2);
}
var obj = {
a: 100
}
fn.call(obj, 10, 20); // 130
fn.apply(obj, [10, 30]); // 140
bind方法和前面的call、apply有所不同,bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值,下面例子中func函数中的this通过bind方法就指向了a对象,所以打印输出Hello
var a = {
b : function(){
var func = function(){
console.log(this.c);
}.bind(this);
func();
},
c : 'Hello!'
}
a.b(); //Hello!
结论:
call、apply方法调用时会自动执行方法前的调用函数,即执行fn.call(obj)后就会自动执行fn函数体内部代码,改变fn函数内部this指向为obj,但bind方法它只会创建一个新函数,并不会自动执行该函数内部代码,即 fn.bind(obj)() 的效果和fn.call(obj)是一样的
五、构造函数与原型方法上的this
废话不多说,直接上代码就是干
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
// 这里的this又指向了谁?
return this.name;
}
var p1 = new Person('哈哈', 18);
p1.getName(); // 哈哈
我们已经知道,this是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。
通过new操作符调用构造函数,会经历以下4个阶段。
- 创建一个新的对象;
- 将构造函数的this指向这个新对象;
- 指向构造函数的代码,为这个对象添加属性,方法等;
- 返回新对象
因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象:p1。
而原型方法上的this就好理解多了,根据上边对函数中this的定义,p1.getName()中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。
六、箭头函数中的this
箭头函数中的this不适合上面说的几种指向规则,由于箭头函数不绑定this, 它会捕获其箭头函数所在(即定义的位置)上下文的this值, 作为自己的this值
代码解析:
setTimeout中的第一个参数是一个箭头函数,由于该函数内部不绑定this,所以它的this取得是定义该箭头函数时的那个上下文中的this,也就是Person构造函数中的this,也就是在new过程中新创建的那个对象,所以它和new创建出来的实例p打印的结果是一样的,它们指向的都是同一个对象
七、常见面试题
Demo1:
function a(xx) {
this.x = xx;
return this;
}
var x = a(5);
var y = a(6);
console.log(x.x); // undefined
console.log(y.x);
代码解析:
a(5)运行时由于是独立调用,所以a函数中的this指向window,this.x相当于声明了一个全局变量x,值为5,然后返回a函数返回window,又赋值给了x,即var x = window,这里的x用var声明,也就是相当于在全局环境中又声明了一个x,刚才全局中已经存在一个x = 5 了,此时就会覆盖刚才的那个5,所以此时在全局环境中x 的值为window,然后执行a(6),再次调用a函数,传值6进去,此时给this.x这个全局变量x赋值为了6,即此时window.x = x = 6,然后同样返回window,赋值给y,所以最终打印x.x就相当于6.x,所以是undefined。而y.x = window.x = 6,
Demo2:
foo = function(){
this.myName = "Foo function.";
}
foo.prototype.sayHello = function(){
alert(this.myName); // 延迟1s 输出undefined
}
foo.prototype.bar = function(){
setTimeout(this.sayHello, 1000);
}
var f = new foo;
f.bar();
代码解析:
f为通过foo构造函数创建出来的一个实例对象,前面给foo原型对象中增加sayHello和bar函数,是为了能让其所有实例对象也能访问到sayHello和bar方法,当执行f.bar()时,bar作为了调用者函数,它被f对象所拥有,不属于独立调用,所以bar函数中的this指的是f实例对象,所以setTimeOut中的回调函数相当于是一个sayHello函数,当延迟1s过后执行该回调函数时,此时sayHello回调函数属于独立调用,所以此时该函数内部this指向undefined,由于是非严格模式,就指向了window对象,所以最终打印输出的是window.myName,很显然,在全局环境中没有该属性,所以最终延迟一秒输出undefined
Demo3
let length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
}
obj.method(fn, 1);
代码解析:
执行obj.method方法时,传入两个参数,一个fn函数和1,所以此时method函数会立刻执行,于是就到了fn()这一步,这里的fn可以理解为就是通过method函数实参穿过来的fn函数,也就是外面全局定义的fn函数,所以这里fn函数是独立调用的,所以fn函数此时的this指向的是window,所以fn()这句话打印的其实是window.length,但window.length其实代表的是当前页面中有多少个iframe,所以这个输出答案在不同的页面中输出是不同的,这个你们可以自行试一下,然后继续执行下一行代码arguments0,这里的arguments对象其实接受了两个实参(fn和1),arguments对象是一个类数组,它的第0位下标是实参列表的第1个参数,也就是fn函数。
当这个fn函数调用的时候,它的this被绑定到arguments对象上。
因为obj.method传入了两个参数,所以arguments对象的length属性为2
下面是我对于第一个输出答案的验证👇🏻