3

相信每一个前端的朋友都会遇到过this.xxx is undefined或者this.xxx is not a function的错误,明明我们定义了这个xxx,但是还是要报错?令人百思不得其解,其实就是因为this指针的引用对象中,没有找到这个定义xxx导致的,因此今天来总结一下this指针的几种常见的指向问题。

由于this的定义中提到了上下文,因此我们在这里先简单的梳理一下Js中的上下文。

一、执行上下文

上下文分为:

  • 全局上下文:全局执行上下文是在代码执行时就创建了,函数执行上下文是在函数调用的时候创建的。
  • 函数上下文:同一个函数,在不同时刻调用,会创建不同的执行上下文。

    变量和函数的上下文决定了它们可以访问哪些数据以及他们的行为。
    每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出函数的上下文。---《JavaScript 高级程序设计》

无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。然而,在严格模式下,如果进入执行环境时没有设置 this 的值,this 会保持为 undefined。如下代码:

  function f() {
    "use strict"; // 这里是严格模式
    return this;
  }
  console.log(f() === undefined); // true

在浏览器中全局上下文也是window对象,node.js中的全局对象是global。

  console.log(this === window); // true
  a = 1;
  console.log(window.a); // 1
  this.b = "小白";
  console.log(window.b)  // "小白"
  console.log(b)         // "小白"

二、this指针的引用问题

this是函数中一个特殊的对象,它在标准函数和箭头函数中的行为并不相同。

1.this在标准函数中的指向:

在标准函数的调用中,基本上可以分为以下三类:
①作为对象调用时,指针指向所属对象。
②作为函数调用时,指针指向window。
③作为构造函数调用时,指针指向实例对象。

  var obj = {
    a: 1,
    b: function () {
      console.log(this);
    },
    c: function (a) {
      this.a = a;
      this.func = function () {
        console.log(this.a)//2
        console.log(this)//c {a: 2, func: ƒ}
      }
    }
  }
  //①作为对象调用时,指针指向所属对象。
  obj.b();//{a: 1, b: ƒ,c: ƒ}
  //②作为函数调用时,指针指向window。
  var fun = obj.b;
  fun();//Window
  //③作为构造函数调用时,指针指向实例对象。
  var obj1 = new obj.c(2);
  obj1.func();

解释①:在①例子中,将b作为方法调用的对象是obj,因此this指针引用的是obj对象。
解释②:在②例子中,相当于将obj.b的函数赋值给fun,因此fun是被作为函数调用,因此this指针引用的是window对象。
解释③:在例子③中,使用new构造了一个obj.c的实例obj1,调用func()方法的对象是obj1,因此打印的是2和对象obj1。
扩展:

  function Person(name,age){
    this.name = name;
    this.age = age;
  }
  var person1 = new Person("张三",18);
  console.log(person1);//Person{name: '张三', age: 18}
  console.log(person1.name,person1.age);//张三 18
  var person2 = Person("李四",12);
  console.log(person2);//undefined
  console.log(window.name,window.age);//李四 12

  在这里补充一下new关键字的作用:
    a.在构造函数代码开始执行前,创建一个空的对象
    b.修改this的指向:把this指向刚刚创建出来的空对象
    c.执行函数的代码
    d.在函数完成之后,返回给this引用的对象,即创建出来的对象
  1.此时Person相当于构造函数,使用new关键字后,创建一个空对象person1,并将this指向这个对象person1,所以打印时person1是一个对象,里面有name和age属性
  2.为什么person2是undefined呢?此时的Person(“李四”,12) 相当于函数执行,由于函数没有返回值,所以person2为undefined。
  3.为什么window中有name和age属性可以打印?因为此时Person(“李四”,12)是在window对象中调用的,因此相当于window.Person(“李四”,12),所以Person函数执行时,将name和age属性添加到window对象中去了。

2.this在箭头函数中的指向:

在箭头函数中,this引用的是定义箭头函数的上下文。

  var obj = {
    d: function () {
      let a = () => {
        console.log(this)
      }
      a();
    }
  }
  //箭头函数,指针始终指向定义箭头函数的上下文。
  obj.d();//{d: ƒ}
  var funTest = obj.d;
  funTest()//Window{window: Window, self: Window, document: document, name: '', location: Location,…}

解释:第一次obj.b()是由对象调用b中定义的方法,因此this指针在匿名函数中指向对象obj,定义箭头函数a的时候指针指向obj,因此打印的是{d: ƒ},而第二次是作为函数调用funTest,因此,匿名函数中this指向window,因此打印的是window对象。

  var color = 'red';
  let sayColor = () => {
    console.log(this.color);
  }
  let obj = {
    color: 'yellow',
    sayColor: sayColor,
    objSayColor: function () {
      console.log(this.color)
    },
    objSayColor2: function () {
      return () => {
        console.log(this.color);
      }
    }
  }
  obj.sayColor();//red
  obj.objSayColor();//yellow
  obj.objSayColor2()();//yellow

3.this在闭包中的指向:

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。---《JavaScript高级程序设计》

具体闭包从上下文、作用域链角度是如何实现的,我后续会写一篇文章记录详解,在此就不多赘述了。
现在我们知道了this的引用,标准函数①当作为对象调用时,this指向调用的对象;②作为普通函数调用时,this指向window;③作为构造函数调用时,this指向生成的实例对象。箭头函数中,this始终指向定义箭头函数的上下文。

  var identity = 'The Window';
  let object = {
    identity: 'The Object',
    getIdentity() {
      return function () {
        console.log(this.identity);
      }
    }
  }
  object.getIdentity()();//The Window

大家看一下,这个结果和大家想的是否一样呢?
在这个例子中,虽然getIdentity()函数是在对象中调用,所以,getIdentity()方法中的this指针指向object,但是返回的匿名函数,后面加上()相当于执行返回的匿名函数,如②作为普通函数调用时,this指针指向window;
可是,这样就真的没有办法访问object中的identity对象了吗?其实不然,要知道每个函数在调用时都会自动创建两个特殊变量:this和arguments内部函数虽然永远不可能直接访问外部函数的这两个变量,但是,如果把this保存到闭包可以访问的其他变量中,则是行得通的,代码如下:

  var identity = 'The Window';
  let object = {
    identity: 'The Object',
    getIdentity() {
      let that = this;
      return function () {
        console.log(that.identity);
      }
    }
  }
  object.getIdentity()();//The Object

4.this在事件调用函数中的指向:

  • 当函数作为dom事件的处理函数时:this 指向触发事件的元素。

    <button onclick="console.log(this)">
    Show this
    </button>
    //<button onclick="console.log(this)">Show this</button>
  • 当函数作为一个内联事件处理函数:它的this指向监听器所在的DOM元素:

    <button onclick="alert(this.tagName.toLowerCase());">
    Show this
    </button>//button

    但需注意下面这种情况,当函数调用时,它是属于被独立调用的函数,所以函数里面的this指向的是window。

    <button onclick="alert((function(){return this})());">
    Show inner this
    </button>//[object Window]

    三、改变this指针指向的方法(apply、bind、call)

  • call方法:

  var a = 'The Window';
  var obj={
    a:'The Obj',
  };
  function sayA(){
    console.log(this.a);
  }
  sayA()//The Window
  sayA.call(obj)//The Obj
function func (a,b,c) {}

func.call(obj, 1,2,3)
// func 接收到的参数实际上是 1,2,3

func.call(obj, [1,2,3])
// func 接收到的参数实际上是 [1,2,3],undefined,undefined

需要注意以下几点:
调用 call 的对象,必须是个函数 Function。
1)call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。 如果不传,则默认为全局对象 window。
2)第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。

  • apply方法:和call的作用是一样的,需要注意的是:

1)它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
2)第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。

知识点总结:

apply、call的共同点:都能够改变函数执行时的上下文(this指针指向),将一个对象的方法交给另一个对象来执行,并且是立即执行的。
apply、call的区别:传入参数的方式,call接收单个参数,例如:func.call(obj, 1,2,3);而apply接受参数数组或者类数组,例如:func.apply(obj, [1,2,3])。

apply的巧用:

  let arr1 = [1, 2, 3];
  let arr2 = [4, 5, 6];
  Array.prototype.push.apply(arr1, arr2);//实现两个数组合并。
  console.log(arr1); // [1, 2, 3, 4, 5, 6]
  let max = Math.max.apply(null, arr1);//结合Math.max返回数组最大值
  console.log(max)//6
  let min = Math.min.apply(null, arr1);//结合Math.max返回数组最小值
  console.log(min)//1
  • bind方法:

    bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 ---MDN

其实,bind 方法 与 apply 和 call 方法功能类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行,而 apply 和 call 则是立即调用。

  var a = 'The Window';
  var obj={
    a:'The Obj',
  };
  function sayA(){
    console.log(this);
  }
  let CopyFunction=sayA.bind(obj)//不打印,此处相当于返回方法。
  CopyFunction();//{a: 'The Obj'}
  sayA.bind(obj)()//{a: 'The Obj'}
  sayA.bind()()//Window{window: Window, self: Window, document: document, name: '', location: Location,…}

参考资料:


很白的小白
145 声望125 粉丝

引用和评论

0 条评论