透彻理解 ES5和ES6 this 指向问题

首先对this的下个定义:this的指向在函数执行时才能确定,即:this是在执行上下文创建时确定的,一个在执行过程中不可更改的变量。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。如果使用箭头函数时再也不用担心this跳来跳去了。 下文会针对箭头函数进行说明

this是指向最近一次调用的对象(不过也有几种特殊情况,后面会做说明。

this只在函数调用阶段确定,也就是执行上下文创建的阶段进行赋值,保存在变量对象中。这个特性也导致了this的多变性:即当函数在不同的调用方式下都可能会导致this的值不同:

vara =1;
function fun(){  
   'use strict';
    var a =2;
    return this.a;
}
fun();    //报错 Cannot read property 'a' of undefined

严格模式下,this指向undefined;

vara =1;
function fun(){
  var a =2;
  return this.a;
}
fun();//1

即在不同模式下之所以有不同表现,就是因为this在严格模式,非严格模式下的不同。

结论:当函数独立调用的时候,在严格模式下它的this指向undefined,在非严格模式下,当this指向undefined的时候,自动指向全局对象(浏览器中就是window)

在全局环境下,this就是指向自己:

this.a =1;
var b =1;
c =1;
console.log(this===window)   //true

全局上下文的这三种都能得到想要的结果,对象中存在这三个变量

当this不在函数中用的时候,this还是指向了window:

var a =100;
var obj = {
  a:1,
  b:this.a +1
}
function fun(){
  var obj = { 
         a:1,
         c:this.a +2 //严格模式下这块报错 Cannot read property 'a' of undefined
   }
   return obj.c;
}
console.log(fun());//102
console.log(obj.b);//101

结论:当obj在全局声明的时候,obj内部属性中的this指向全局对象,当obj在一个函数中声明的时候,严格模式下this会指向undefined,非严格模式自动转为指向全局对象。 

现在知道了严格模式和非严格模式下this的区别,然而我们日常应用最多的还是在函数中用this,上面也说过了this在函数的不同调用方式还有区别,那么函数的调用方式都有哪些呢?四种:

  • 在全局环境或是普通函数中直接调用
  • 作为对象的方法
  • 使用apply和call
  • 作为构造函数
  • 隐式调用

下面分别就四种情况展开:

1. 直接调用

fun函数虽然在obj.b方法中定义,但它还是一个普通函数,直接调用在非严格模式下指向undefined,又自动指向了全局对象,正如预料,严格模式会报错undefined.a不成立,a未定义。

var a =1;
var obj = {
  a:2,
  b:function(){
    function fun(){
       return this.a
    }
    console.log(fun());
  }
} 
obj.b();  //1   this 指向windows  非严格模式

2. 作为对象的方法

b所引用的匿名函数作为obj的一个方法调用,这时候this指向调用它的对象。这里也就是obj:

var a =1;
var obj = {
  a:2,
  b:function(){
    return this.a;
  }
}
console.log(obj.b())  //2

那么如果b方法不作为对象方法调用:

var a =1;
var obj = {
  a:2,
  b:function(){
    return this.a;
  }
}
var t = obj.b;
console.log(t());  //1

//是因为把obj.b赋值给了全局变量t (并没有执行,所以this还不能确定)
//而t的执行者是window 所以this又指向了window 
//这也印证了本文开头的说法:this只有在 执行时 才确定,而this指向 最近一次 调用的对象

如上,t 函数执行结果竟然是全局变量1,为啥呢?这就涉及Javascript的内存空间了,就是说,obj对象的b属性存储的是对该匿名函数的一个引用,可以理解为一个指针。当赋值给t的时候,并没有单独开辟内存空间存储新的函数,而是让t存储了一个指针,该指针指向这个函数。相当于执行了这么一段伪代码:

var a =1;
function fun(){
   //此函数存储在堆中
   return this.a;
}
var obj = { 
a:2, 
b: fun //b指向fun函数
}
var t = fun;  //变量t指向fun函数
console.log(t());  //1

此时的t就是一个指向fun函数的指针,调用t,相当于直接调用fun,套用以上规则,打印出来1自然很好理解了。

3. 使用apply,call

这是个万能公式,实际上上面直接调用的代码,我们可以看成这样的:

function fun(){
   return this.a;
}
fun(); //1
//严格模式
fun.call(undefined)
//非严格模式
fun.call(window)

这时候我们就可以解释下,为啥说在非严格模式下,当函数this指向undefined的时候,会自动指向全局对象,如上,在非严格模式下,当调用fun.call(undefined)的时候打印出来的依旧是1,就是最好的证据。

为啥说是万能公式呢?再看函数作为对象的方法调用:

var a =1;
var obj = {
  a:2,
  b:function(){
    return this.a;  
  }
}
obj.b()
obj.b.call(obj)

如上,是不是很强大,可以理解为其它两种都是这个方法的语法糖罢了,那么apply和call是不是真的万能的呢?并不是,ES6的箭头函数就是特例,因为箭头函数的this不是在调用时候确定的这也就是为啥说箭头函数好用的原因之一,因为它的this固定不会变来变去的了。关于箭头函数的this我们稍后再说。

4. 作为构造函数

何为构造函数?所谓构造函数就是用来new对象的函数,像Function、Object、Array、Date等都是全局定义的构造函数。其实每一个函数都可以new对象,那些批量生产我们需要的对象的函数就叫它构造函数罢了。注意,构造函数首字母记得大写。

function Fun() {
    this.name = 'Lili';
     this.age = 21; 
     this.sex = 'woman'; 
     this.run = function () { 
         return this.name + '正在跑步'; 
        }
} 
Fun.prototype = { 
    contructor: Fun, 
    say: function () { 
        return this.name + '正在说话'; 
    } 
}
var f = newFun();
f.run();  //Lili正在跑步 
f.say();  //Lili正在说话

如上,如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。为啥呢?new做了啥呢?

伪代码如下:

function Fun(){
    //new做的事情
    varobj = {};
    obj.__proto__ = Fun.prototype;
    //Base为构造函数
    obj.name ='Lili';
    //... 一系列赋值以及更多的事
    return obj
}

也就是说new做了下面这些事:

创建一个临时对象

给临时对象绑定原型

给临时对象对应属性赋值

将临时对象return

也就是说new其实就是个语法糖,this之所以指向临时对象还是没逃脱上面说的几种情况。

当然如果直接调用Fun(),如下:

function Fun() { 
    this.name = 'Damonre'; 
    this.age = 21; 
    this.sex = 'man'; 
    this.run = function () { 
        return this.name + '正在跑步'; 
    } 
} 
Fun(); 
console.log(window)

其实就是直接调用一个函数,this在非严格模式下指向window,你可以在window对象找到所有的变量。

另外还有一点,prototype对象的方法的this指向实例对象,因为实例对象的proto已经指向了原型函数的prototype。这就涉及原型链的知识了,即方法会沿着对象的原型链进行查找。

箭头函数 (箭头函数中的this在函数定义时绑定,而不是在函数执行时绑定!)

var obj = {
    name: 'jack',
    fn: function(){
        console.log(this)
    }
}
obj.fn();   //obj  这里是ES5的写法  this指向obj

var obj = {
    name: 'jack',
    fn: ()=>{
        console.log(this)
    }
}
obj.fn();   //window

//为什么箭头函数中的this指向了window了呢? 
//原因就是this是继承自父执行上下文中的this
// 比如这里的箭头函数中的this,箭头函数本身所在的对象为obj,而obj的父执行上下文就是window

刚刚提到了箭头函数是一个不可以用call和apply改变this的典型

我们看下面这个例子:

var a = 1; 
var obj = { a: 2 }; 
var fun = () => console.log(this.a); 
fun();  //1
fun.call(obj)  //1

以上,两次调用都是1。

那么箭头函数的this是怎么确定的呢?箭头函数会捕获其所在上下文的 this 值,作为自己的 this 值,也就是说箭头函数的this在词法层面就完成了绑定。apply,call方法只是传入参数,却改不了this。

var a = 1; 
var obj = { a: 2 }; 
function fun(){ 
    var a = 3;
    let f = () => console.log(this.a); 
    f();
}
fun(); //1
fun.call(obj); //2

如上,fun直接调用,fun的上下文中的this值为window,注意,这个地方有点绕。fun的上下文就是此箭头函数所在的上下文,因此此时f的this为fun的this也就是window。当fun.call(obj)再次调用的时候,新的上下文创建,fun此时的this为obj,也就是箭头函数的this值。

再来一个例子:

function Fun(){ 
    this.name = 'Damonare'; 
} 
Fun.prototype.say = () => { 
    console.log(this); 
}
var f = newFun(); 
f.say(); //window

有的同学看到这个例子会很懵,感觉上this应该指向f这个实例对象啊。不是的,此时的箭头函数所在的上下文是proto所在的上下文也就是Object函数的上下文,而Object的this值就是全局对象。

那么再来一个例子:

function Fun() {
    this.name = 'Damonare';
    this.say = () => {
        console.log(this);
    }
}
var f = newFun();
f.say();//Fun的实例对象

如上,this.say所在的上下文,此时箭头函数所在的上下文就变成了Fun的上下文环境,而因为上面说过当函数作为构造函数调用的时候(也就是new的作用)上下文环境的this指向实例对象。

5. 隐式调用

function test(){
    var name = 'tom';
    console.log(this.name);  //undefined
    console.log(this);   //window
}
test();

这里乍一看好像并没有某个对象调用test方法,实际上这是一种隐式调用。真正的调用者是全局对象window, 相当于 window.test(); 所以这里的this指向window ,类似的场景还有setTimeout回调、匿名函数自调 

function test(){
    setTimeout(function(){
        console.log(this)   //window    
    },100)
}
//这次this出现在全局函数setTimeout()中的匿名函数里,并没有某个对象显示的调用这个匿名函数,所以this指向window对象
test();

(function(){
    console.log(this)   //window
})()  

还有一种特殊情况值得关注:当this遇上return

function test(){
    this.name = 'jack';
    return {};
}
var t = new test();
t.name  // undefined

function test(){
    this.name = 'jack';
    return;
}
var t = new test();
t.name  //  jack

这是怎么回事呢? 原因是当this遇上return时 如果return的是一个对象那么this指向这个return的对象,否则this仍然指向t函数的实例。有一个例外是:

function test() {
   this.name = 'jack';
   return null;
}

function test2() {
    this.name = 'jack';
}

var t = new test();  //{name: 'jack'}
var t2 = new test2(); //{name: 'jack'}
t.name;   //jack
t2.name;   //jack

null是一个对象但是this仍然指向函数的实例!!!

©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页