【js进阶】-this解读

一、前序

在之前的博文中我们讲到函数在调用的时候会创建执行上下文,而执行上下文中主要有三个方面组成(创建作用域链、生成变变量对象以及确定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

下面是我对于第一个输出答案的验证👇🏻
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ronychen’s blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值