JavaScript 的 this的指向

JavaScript 的 this 指向

一、问题的由来

学懂 JavaScript 语言,一个标志就是理解下面两种写法,可能有不一样的结果。


var obj = {
  foo: function () {}
};

var foo = obj.foo;

// 写法一
obj.foo()

// 写法二
foo()

上面代码中,虽然obj.foofoo指向同一个函数,但是执行结果可能不一样。请看下面的例子。


var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1
foo() // 2

这种差异的原因,就在于函数体内部使用了this关键字。很多教科书会告诉你,this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

这种解释没错,但是教科书往往不告诉你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么obj.foo()就是在obj环境执行,而一旦var foo = obj.foofoo()就变成在全局环境执行?

本文就来解释 JavaScript 这样处理的原理。理解了这一点,你就会彻底理解this的作用。

二、内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。


var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。


{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

三、函数

这样的结构是很清晰的,问题在于属性的值可能是一个函数。


var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。


{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。


var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

四、环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。


var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。


var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的x


var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x

obj环境执行,this.x指向obj.x

回到本文开头提出的问题,obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

 

this的指向

1, 普通函数指向函数的调用者:有个简便的方法就是看函数前面有没有点,如果有点,那么就指向点前面的那个值;

2, 箭头函数指向函数所在的所用域: 注意理解作用域,只有函数的{}构成作用域,对象的{}以及 if(){}都不构成作用域;

 

 

ES6中箭头函数与普通函数this的区别

普通函数中的this:

1. this总是代表它的直接调用者, 例如 obj.func ,那么func中的this就是obj

2.在默认情况(非严格模式下,未使用 'use strict'),没找到直接调用者,则this指的是 window

3.在严格模式下,没有直接调用者的函数中的this是 undefined

4.使用call,apply,bind(ES5新增)绑定的,this指的是 绑定的对象

箭头函数中的this

默认指向在定义它时,它所处的对象,而不是执行时的对象, 定义它的时候,可能环境是window(即继承父级的this);

下面通过一些例子来研究一下 this的一些使用场景

示例1

复制代码
<script>
  var obj = {
    say: function () {
      setTimeout(function () {   //匿名函数
        console.log(this)
      });
    }
  }
  obj.say();
</script>
复制代码

结果是:window

匿名函数,定时器中的函数,由于没有默认的宿主对象,所以默认this指向window

问题: 如果想要在setTimeout中使用这个对象的引用呢?

用一个 变量提前把正确的 this引用保存 起来, 我们通常使用that = this, 或者 _this = this来保存我们需要的this指针!

复制代码
<script>
  var obj = {
    func: function() {},
    say: function () {
      var that = this;   //此时的this就是obj对象
      setTimeout(function () {
        console.log(this)
        that.func()
      });
    }
  }
  obj.say();
</script>
复制代码

示例2

复制代码
window.val = 1;
var obj = {
  val: 2,
  dbl: function () {
    this.val *= 2;
    val *= 2;
    console.log(val);
    console.log(this.val);
  }
};
// 说出下面的输出结果
obj.dbl();
var func = obj.dbl;
func();
复制代码

结果是:2  4  8  8

<1> 12行代码调用

val变量在没有指定对象前缀,默认从函数中找,找不到则从window中找全局变量

即 val *=2 就是 window.val *= 2

this.val默认指的是 obj.val ;因为 dbl()第一次被obj直接调用

<2>14行代码调用

func() 没有任何前缀,类似于全局函数,即  window.func调用,所以

第二次调用的时候, this指的是window, val指的是window.val

第二次的结果受第一次的影响

示例3.在严格模式下的this

复制代码
<script>
  function test() {
    'use strict';
    console.log(this);
  }
  test();
</script>
复制代码

结果是:undefined

 

箭头函数中的this

箭头函数中没有自己的this,它的this都是继承父作用域的this。

箭头函数中this是定义的时候就指定了它,也不会随着调用的方法改变而改变

 

示例4.箭头函数中的this

复制代码
<script>
  var obj = {
    say: function () {
      setTimeout(() => {
        console.log(this)
      });
    }
  }
  obj.say(); // obj
</script>
复制代码

此时的this是定义它的对象,即继承父级的this,父级中的this指的是obj,而非window

示例5

复制代码
<script>
  var obj = {
    say: function () {
      var f1 = () => {
        console.log(this); // obj
        setTimeout(() => {
          console.log(this); // obj
        })
      }
      f1();
    }
  }
  obj.say()
</script>
复制代码

结果:都是obj

f1继承父级this指代的obj,不管f1有多层箭头函数嵌套,都是obj.

示例6

复制代码
<script>
  var obj = {
    say: function () {
      var f1 = function () {
        console.log(this);    // window, f1调用时,没有宿主对象,默认是window
        setTimeout(() => {
          console.log(this); // window
        })
      };
      f1();
    }
  }
  obj.say()
</script>
复制代码

结果:window,window

第一个this:f1调用时没有宿主对象,默认是window

第二个this:继承父级的this,父级的this指代的是window.

 

不适合用箭头函数的几种情况

1.在构造函数中,需要使用this来绑定对象

    var Person=(name,age)=>{
      this.name=name
      this.age=age
      this.say=function(){
        console.log('他的名字'+this.name)
      }
    }
    var person=new Person('zjx',22)

原因:使用new生成实例的步骤

1.生成一个新的对象

2.把构造函数中this指向这个对象

3.绑定原型对象

4.返回这个新生成的对象

在这里我们使用箭头函数无法把this绑定到新生成的对象中

应该修改为

    var Person=function(name,age){
      this.name=name
      this.age=age
      this.say=function(){
        console.log('他的名字'+this.name)
      }
    }
    var person=new Person('zjx',22)

补充:当我们对原型中使用箭头函数

    var Person=function(name,age){
      this.name=name
      this.age=age
    }
    Person.prototype.say=()=>{
      console.log(this)
    }
    var person=new Person('zjx',22)
    person.say()

打印的this是window,因为()=>{console.log(this)},在全局作用域内,所以继承父级的this,但是父级的this是全局作用域,所以this为window

修改为

    var Person=function(name,age){
      this.name=name
      this.age=age
    }
    Person.prototype.say=function(){
      console.log(this)
    }
    var person=new Person('zjx',22)
    person.say()

 

2.当你需要this的时候 比如点击事件绑定的this,点击后添加样式,两秒后移除样式

    const button=document.querySelector('button')
    button.addEventListener('click',()=>{
      console.log(this)
      this.classList.add('active')
    })

这里()={console.log(this);this.classList.add('active)} 箭头函数,this也是在全局作用域内,this指向的是window

修改为

    const button=document.querySelector('button')
    button.addEventListener('click',function(){
      console.log(this)
      this.classList.add('active')
    })

补充:

    const button=document.querySelector('button')
    button.addEventListener('click',function(){
      console.log(this)
      this.classList.add('active')
      setTimeout(function(){
     console.log(this) this.classList.remove('active') }) })

因为setTimeout里面的 this是绑定到调用者,现在setTimeout里面没有函数的的调用者,所以this指向的是window

修改为使用箭头函数后,this继承于父级作用域的this

    const button=document.querySelector('button')
    button.addEventListener('click',function(){
      console.log(this)
      this.classList.add('active')
      setTimeout(()=>{
        console.log(this)
        this.classList.remove('active')
      })
    })

箭头函数没有this,所以this继承父级作用域的this,所以这个this指向的是button这个元素

 

 

 

this的指向

一、前言

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。对于那些没有投入时间学习this机制的JavaScript开发者来说,this的绑定一直是一件非常令人困惑的事。this到底是谁,看似挺简单的一个问题,却困扰了许久的一个问题,自以为已经掌握,却屡次在笔试中铩羽而归。

二、了解this

学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域,你也许被这样的解释误导过,但其实它们都是错误的。随着函数使用场合的不同,this的值会发生变化。但总有一条原则就是JS中的this代表的是当前行为执行的主体,在JS中主要研究的都是函数中的this,但并不是说只有在函数里才有this,this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。如何的区分this呢?

三、this到底是谁

这要分情况讨论,常见有五种情况:

1、函数执行时首先看函数名前面是否有".",有的话,"."前面是谁,this就是谁;没有的话this就是window

function fn(){
  console.log(this);
}
var obj={fn:fn};
fn();//this->window
obj.fn();//this->obj
function sum(){
     fn();//this->window
}
sum();
var oo={
 sum:function(){
 console.log(this);//this->oo
       fn();//this->window
  }
};
oo.sum();

2、自执行函数中的this永远是window

  (function(){ //this->window })();
  ~function(){ //this->window }();

3、给元素的某一个事件绑定方法,当事件触发的时候,执行对应的方法,方法中的this是当前的元素,除了IE6~8下使用attachEvent(IE一个著名的bug)

  • DOM零级事件绑定
  oDiv.onclick=function(){
     //this->oDiv
  };
  • DOM二级事件绑定
  oDiv.addEventListener("click",function(){
     //this->oDiv
  },false);
  • 在IE6~8下使用attachEvent,默认的this就是指的window对象
  oDiv.attachEvent("click",function(){
       //this->window
  });

我们大多数时候,遇到事件绑定,如下面例子这种,对于IE6~8下使用attachEvent不必太较真

function fn(){
  console.log(this);
}
document.getElementById("div1").onclick=fn;//fn中的this就是#divl
document.getElementById("div1").onclick=function(){
 console.log(this);//this->#div1
 fn();//this->window
};

4、在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例

function CreateJsPerson(name,age){
//浏览器默认创建的对象就是我们的实例p1->this
this.name=name;//->p1.name=name
this.age=age;
this.writeJs=function(){
console.log("my name is"+this.name +",i can write Js");
   };
//浏览器再把创建的实例默认的进行返回
}
var p1=new CreateJsPerson("尹华芝",48);

必须要注意一点:类中某一个属性值(方法),方法中的this需要看方法执行的时候,前面是否有".",才能知道this是谁。大家不妨看下接下来的这个例子,就可明白是啥意思。

function Fn(){
this.x=100;//this->f1
this.getX=function(){
console.log(this.x);//this->需要看getX执行的时候才知道
   }
}
var f1=new Fn;
f1.getX();//->方法中的this是f1,所以f1.x=100
var ss=f1.getX;
ss();//->方法中的this是window ->undefined

5.call、apply和bind

我们先来看一个问题,想在下面的例子中this绑定obj,怎么实现?

var obj={name:"浪里行舟"};
function fn(){
 console.log(this);//this=>window
}
fn();
obj.fn();//->Uncaught TypeError:obj.fn is not a function

如果直接绑定obj.fn(),程序就会报错。这里我们应该用fn.call(obj)就可以实现this绑定obj,接下来我们详细介绍下call方法:

  • call方法的作用:

①首先我们让原型上的call方法执行,在执行call方法的时候,我们让fn方法中的this变为第一个参数值obj;然后再把fn这个函数执行。

②call还可以传值,在严格模式下和非严格模式下,得到值不一样。

//在非严格模式下
var obj={name:"浪里行舟 "};
function fn(num1,num2){
console.log(num1+num2);
console.log(this);
}
fn.call(100,200);//this->100 num1=200 num2=undefined
fn.call(obj,100,200);//this->obj num1=100 num2=200
fn.call();//this->window
fn.call(null);//this->window
fn.call(undefined);//this->window
//严格模式下 
fn.call();//在严格模式下this->undefined
fn.call(null);// 在严格模式 下this->null
fn.call(undefined);//在严格模式下this->undefined
  • apply和call方法的作用是一模一样的,都是用来改变方法的this关键字并且把方法
    执行,而且在严格模式下和非严格模式下对于第一个参数是null/undefined这种情况的规
    律也是一样的。

两者唯一的区别:call在给fn传递参数的时候,是一个个的传递值的,而apply不是一个个传,而是把要给fn传递的参数值统一的放在一个数组中进行操作。但是也相当子一个个的给fn的形参赋值。总结一句话:call第二个参数开始接受一个参数列表,apply第二个参数开始接受一个参数数组

fn.call(obj,100,200);
fn.apply(obj,[100,200]);
  • bind:这个方法在IE6~8下不兼容,和call/apply类似都是用来改变this关键字的,但是和这两者有明显区别:
fn.call(obj,1,2);//->改变this和执行fn函数是一起都完成了
fn.bind(obj,1,2);//->只是改变了fn中的this为obj,并且给fn传递了两个参数值1、2,
                     但是此时并没有把fn这个函数执行
var tempFn=fn.bind(obj,1,2);
tempFn(); //这样才把fn这个函数执行

bind体现了预处理思想:事先把fn的this改变为我们想要的结果,并且把对应的参数值也准备好,以后要用到了,直接的执行即可。

call和apply直接执行函数,而bind需要再一次调用。

  var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }
  var b = a.fn;
  b.bind(a,1,2)

上述代码没有执行,bind返回改变了上下文的一个函数,我们必须要手动去调用:

 b.bind(a,1,2)() //3

必须要声明一点:遇到第五种情况(call apply和bind),前面四种全部让步。

四、箭头函数this指向

箭头函数正如名称所示那样使用一个“箭头”(=>)来定义函数的新语法,但它优于传统的函数,主要体现两点:更简短的函数并且不绑定this。

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};

现在,箭头函数完全修复了this的指向,箭头函数没有自己的this,箭头函数的this不是调用的时候决定的,而是在定义的时候处在的对象就是它的this。

换句话说,箭头函数的this看外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。

    <button id="btn1">测试箭头函数this_1</button>
    <button id="btn2">测试箭头函数this_2</button>
    <script type="text/javascript">   
        let btn1 = document.getElementById('btn1');
        let obj = {
            name: 'kobe',
            age: 39,
            getName: function () {
                btn1.onclick = () => {
                    console.log(this);//obj
                };
            }
        };
        obj.getName();
    </script>

上例中,由于箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。其实可以简化为如下代码:

   let btn1 = document.getElementById('btn1');
        let obj = {
            name: 'kobe',
            age: 39,
            getName: function () {
                console.log(this)
            }
        };
   obj.getName();

那假如上一层并不存在函数,this指向又是谁?

    <button id="btn1">测试箭头函数this_1</button>
    <button id="btn2">测试箭头函数this_2</button>
    <script type="text/javascript">   
        let btn2 = document.getElementById('btn2');
        let obj = {
            name: 'kobe',
            age: 39,
            getName: () => {
                btn2.onclick = () => {
                    console.log(this);//window
                };
            }
        };
        obj.getName();
    </script>

上例中,虽然存在两个箭头函数,其实this取决于最外层的箭头函数,由于obj是个对象而非函数,所以this指向为Window对象

由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:

var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2018); // 28

 

this 是运行时到某个对象的绑定

1.隐式绑定

一般地,被直接对象所包含的函数调用时,也称为方法调用,this隐式绑定到该直接对象。

        function printA(){
          console.log(this.a)
        }
        var obj={
          a:616,
          print:printA
        }
        obj.print();   //616

使用 obj.print()来调用 obj 中的 print方法。这时候  printA() 处于被 obj 调用的上下文,故此时 this 被绑定到了 obj。这种根据被调用情况将 this绑定到对应对象上的情况我们称之为 隐式绑定。而称之为隐式绑定的原因则是,我们是在一个对象(上例中为 obj)内包含一个指向函数的属性(上例中的 obj.print),并通过这个属性间接(隐式)引用函数。

      function printA() {
          console.log(this.a);
      }
      var obj = {
          a: 616, print: printA
      };

      var alias = obj.print;
      alias(); // 输出为 undefined

      function callFunc(fn) {
          fn();
      }
      callFunc(obj.print); // 输出为 undefined 

这个例子中我们把 obj.print 赋值给了 alias ,本意为给 obj.print 起的别名,却发现结果得到了 undefined 。实际上,obj.print 持有的只是对 printA() 的引用而已,而 alias 也自然实际是指向 printA() 了。此时 alias() 显然不是由 obj 调用的,故并不能遵循隐式绑定的规则。至于 callFunc() ,我们传递进来的 obj.print 实际也是 printA() ,故它也不是被 obj 调用的,所以结果也是 undefined 了。

 

2.显式绑定

callapplybindFunction对象自带的三个方法,都是为了改变函数体内部 this 的指向。

apply 、 call 、bind 三者第一个参数都是 this 要指向的对象,也就是想指定的上下文;apply 、 call 、bind 三者都可以利用后续参数传参;

 

将上面例子改成:

      function printA() {
          console.log(this.a);
      }
      var obj = {
          a: 616, print: printA
      };
      // var alias = obj.print;
      // alias(); // 输出为 undefined
      var alias=printA.bind(obj);
      alias()               //输出616

 

bind 是返回对应 函数,便于稍后调用;apply 、call 则是立即调用 。

var obj = {
  x: 81,
};
  
var foo = {
  getX: function() {
    return this.x;
  }
}
console.log(foo.getX.bind(obj)());  //81   bind返回的是函数 需要后面加()来调用
console.log(foo.getX.call(obj));    //81   call 立即调用
console.log(foo.getX.apply(obj));   //81  apply

call方法的原理:首先寻找call方法,最后通过原型链在Function的原型中找到call方法,然后让call方法执行,在执行call方法的时候,让fn方法中的this变为第一个参数值obj,最后再把fn这个函数执行。

function fn1() {
    console.log(1);
}
function fn2() {
    console.log(2);
}
fn1.call(fn2); // 1

首先fn1通过原型链查找机制找到Function.prototype上的call方法,并且让call方法执行,此时call这个方法中的this就是要操作的fn1。在call方法代码执行的过程过程中,首先让fn1中的“this关键字”变为fn2,然后再让fn1这个方法执行。

注意:在执行call方法的时候,fn1中的this的确会变为fn2,但是在fn1的方法体中输出的内容中并没有涉及到任何和this相关的内容,所以还是输出1.

对于 apply、call 二者而言,作用完全一样,只是接受 参数 的方式不太一样。call 是把参数按顺序传递进去,而 apply 则是把参数放在数组 里。

      var a={
        user:'zhujianxiong',
        fn:function(e,ee){
          console.log(this.user);
          console.log(e+ee);
        }
      }
      var b=a.fn;                  //打印结果都是
      b.call(a,1,2);               //zhujianxiong  
      b.apply(a,[1,2]);            //3

注意如果call和apply的第一个参数写的是null,那么this指向的是window对象。

3.new绑定

使用 new 来调用构造函数(或者说,发生构造调用时),所执行的操作实际是这些:

  1. 创建一个全新的对象(这是为什么我们称之为 “构造函数” 或 “构造调用”)
  2. 对这个新对象执行 [[Prototype]] 链接(不再本次讨论范围内)
  3. 将新对象绑定到 this 
  4. 将这个新对象作为 new 表达式的返回值(如果函数没有返回其它对象的话)

 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。

function fn(){
    this.a = 2;
}
var test = new fn();
console.log(test);//{a:2}

 

4.默认绑定

全局环境中,this默认绑定到window

函数独立调用时,this默认绑定到window

被嵌套的函数独立调用时,this默认绑定到window

//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
var a = 0;
var obj = {
    a : 2,
    foo:function(){
            function test(){
                console.log(this.a);
            }
            test();
    }
}
obj.foo();//0

闭包,类似地,test()函数是独立调用,而不是方法调用,所以this默认绑定到window严格模式

var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//0

由于闭包的this默认绑定到window对象,但又常常需要访问嵌套函数的this,所以常常在嵌套函数中使用var that = this,然后在闭包中使用that替代this,使用作用域查找的方法来找到嵌套函数的this值 

var a = 0;
function foo(){
    var that = this;
    function test(){
        console.log(that.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//2

 

5.严格模式

【1】严格模式下,独立调用的函数的this指向undefined

      function fn(){
          'use strict'; //严格模式
          console.log(this); 
      }
      fn();    //undefined
      function fn(){
          console.log(this);
      }
      fn();    //window

【2】在非严格模式下,使用函数的call()或apply()方法时,null或undefined值会被转换为全局对象。而在严格模式下,函数的this值始终是指定的值

var color = 'red';
function displayColor(){
    console.log(this.color);
}
displayColor.call(null);//red

//严格模式
var color = 'red';
function displayColor(){
    'use strict';
    console.log(this.color);
}
displayColor.call(null);//TypeError: Cannot read property 'color' of null

 

最后

this的四种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定,分别对应函数的四种调用方式:独立调用、方法调用、间接调用和构造函数调用。

 

 

 

 

 

posted @ 2020-01-14 16:36  零度从容  阅读(348)  评论(0编辑  收藏  举报