一文搞懂JS系列(十二)之改变this指向的方式

879 阅读9分钟

大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络,整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞!

合集地址:一文搞懂JS系列专题

概览

温馨提示:在阅读本章节前,最好先阅读以上两篇文章,方便对于箭头函数的 this 指向的理解

方式目录

  • 使用 ES6 的箭头函数
  • 使用 apply、call、bind
  • new 实例化一个对象
  • let vm = this;

箭头函数

箭头函数是作为 ES6 中的一个特性。

它除了减少代码量,书写优雅等特点之外,还有改变 this 指向的作用。

在 this 的设计之初,它是用来指代当前的运行环境(执行上下文)的。而箭头函数可不是根据调用来决定 this 指向的,而是根据函数定义时的 this 指向。

造成这个现象的主要原因是箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,所以,箭头函数引用的是外层作用域的 this。

如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。

正是因为它没有 this ,所以也就不能用作构造函数

那么,我们来通过一个案例,来了解 箭头函数的 this 指向。

首先来看一个用普通函数写的例子,

例1:

var dog = {
  name: 'wangcai',
  hello: function (){
    console.log(this.name)
  }
}
var name='windowName';
// 调用语句
dog.hello();   // wangcai

因为 this 永远指向最后调用它的那个对象 ,最后调用对象是 dog ,所以,输出的值为 wangcai 。

那么,接下来,我们用箭头函数来改写一下这个例子。

例2:

var dog = {
  name: 'wangcai',
  // 声明语句
  hello: ()=>{
    console.log(this.name)
  }
}
var name='windowName';
dog.hello();     // windowName 

这种时候,我们可以看到,输出的结果为 windowName 。

咦,为什么输出的值是 windowName ,而不是 wangcai 。首先,箭头函数的 this 指向不根据调用语句,而是根据声明语句,这个我理解, dog.hello() 我直接无视。

可 hello 方法不是定义在对象 dog 中的么,可千万不能被它声明的声明位置所欺骗。

还记得上面我们说的,箭头函数根据函数定义时的 this 指向,由于它本身没有 this 绑定,必须通过查找作用域链来决定其值,所以,箭头函数引用的是外层作用域的 this。

但是对象不构成单独的作用域,所以,此处的 this 指向的是全局对象,也就是 window。

所以,它的输出值为 windowName。

由于箭头函数的 this 指向固定的特点,很利于封装回调函数。

同时,定义对象方法的时候,我们不适合使用箭头函数。

可以用 ES6 的方法简写来代替:

var dog = {
  name: 'wangcai',
  hello(){
      console.log(this.name);
  }
}

上面的代码等同于:

var dog = {
  name: 'wangcai',
  hello: function(){
      console.log(this.name);
  }
}

apply 、 call 、 bind

将这三个方法放在一起的主要原因,就是它们都可以改变 this 指向,而且都是函数的方法。

首先要明白一点,它们是函数的一个方法。

所以,应该写在方法的后面,它的主要作用是改变当前方法的 this 指向。

将之补全,其实就是:

  • Function.prototype.apply()
  • Function.prototype.call()
  • Function.prototype.bind()

当然,心急吃不了热豆腐,我们来一一揭开它们的面纱。

不过,为了方便大家的理解,我们先来一个案例:

例3:

var dog = {
  name: 'wangcai',
  hello(...rest){
    console.log(`${this.name} 喜欢吃 ${rest.join(',')}`);
  }
}
var name='cooldream'
dog.hello('骨头','肉条');    // wangcai 喜欢吃 骨头,肉条

ES6 引入 rest 参数(形式为 ...变量名 ),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 用于将剩余参数打包,以数组的形式返回。

接下来,我们就通过三种方式,通过改变 this 来让 hello() 指向 window 。

apply

Function.apply(thisArg,[argsArray])

apply() 相当于给定方法的 this 指向,并且执行它。

首先第一个参数就是 this 指向,第二个就是方法调用的传参。第二个传参的格式为数组。

那么,我们还是使用 dog.hello() 方法进行调用,但是,不同的一点是,我们要用 apply() 去改变 this 指向,让它指向 window ,输出 cooldream 喜欢吃 牛蛙煲,烤鸡,芝士焗饭

xdm , 上代码:

var dog = {
  name: 'wangcai',
  hello(...rest){
    console.log(`${this.name} 喜欢吃 ${rest.join(',')}`);
  }
}
var name='cooldream'
dog.hello.apply(window,['牛蛙煲','烤鸡','芝士焗饭'])  //cooldream 喜欢吃 牛蛙煲,烤鸡,芝士焗饭

可以看到,我们用 apply(window) ,通过第一个参数 thisArg ,将 this 指向了 window。

call

Function.call(thisArg, arg1, arg2, ...)

call() 方法和 apply 的作用,其实是一样的,这里就不再赘述了。

如果忘记了,也不用返回上面继续去看了,因为我觉得你看完这篇文章,一小时也就忘记了。(开个玩笑,不要问为什么加粗,怕你跑了),请自觉返回上层查看,温故而知新。

可以看到,第一个参数和 apply 一模一样,但是,两者的主要区别就在于第二个入参。

它是用 , 拼接的列表。

下面,我们用 call() 来改写一下。

var dog = {
  name: 'wangcai',
  hello(...rest){
    console.log(`${this.name} 喜欢吃 ${rest.join(',')}`);
  }
}
var name='cooldream'
dog.hello.call(window,'牛蛙煲','烤鸡','芝士焗饭')  //cooldream 喜欢吃 牛蛙煲,烤鸡,芝士焗饭

bind

bind() 相当于重新创建一个指向你的第一个 this 入参的函数,然后没了,懂吗,它不会执行啊。

那还能怎么办呢,记得自己调用。

别写了之后,咦,我的代码怎么不执行呢???

var dog = {
  name: 'wangcai',
  hello(...rest){
    console.log(`${this.name} 喜欢吃 ${rest.join(',')}`);
  }
}
var name='cooldream'
dog.hello.bind(window,'牛蛙煲','烤鸡','芝士焗饭')()  //cooldream 喜欢吃 牛蛙煲,烤鸡,芝士焗饭
var dog = {
  name: 'wangcai',
  hello(...rest){
    console.log(`${this.name} 喜欢吃 ${rest.join(',')}`);
  }
}
var name='cooldream'
dog.hello.bind(window,['牛蛙煲','烤鸡','芝士焗饭'])()  //cooldream 喜欢吃 牛蛙煲,烤鸡,芝士焗饭

不难发现,对于入参,列表和数组,都可以接受。

那么,总结一下:

  • 作用

    call apply bind 都可以改变 this 的指向。

  • 入参

    call 和 apply 的功能是一样的,但是入参的方式不一样。

    call 接受一串用 , 拼接的列表。

    apply 接受数组。

    bind 表示都行,数组和列表都可。

  • 执行机制

    call 和 apply 都是立即执行的。

    bind 是创建一个新的函数,所以本身并不会执行,需要自己进行调用。

new

构造函数中的 new 能改变 this 指向,其实本身和 new 本身做了什么有关。

理解 new 的实现方式,也就不难理解了。

这次上个来点难度的,自己实现 new,出息了,自己造轮子。

那么,我们就来说一说 new 操作符,到底做了哪些事情

  1. 创建一个新的对象
  2. 将空对象的原型地址 _proto_ 指向构造函数的原型对象 (这里涉及到的原型和原型链的概念,下面会有讲到)
  3. 利用 apply , call , 或 bind ,将原本指向window的绑定对象this指向了obj。(这样一来,当我们向函数中再传递实参时,对象的属性就会被挂载到obj上。)
  4. 返回这个对象

那么,接下来我们可以自己实现一个 new 方法

// const xxx = _new(Person,'cooldream',24)  ==> new Person('cooldream',24)
function _new(fn,...args){
    // 新建一个对象 用于函数变对象
    const newObj = {};
    // 将空对象的原型地址 `_proto_` 指向构造函数的原型对象
    newObj.__proto__ = fn.prototype;
    // this 指向新对象 
    fn.apply(newObj, args);
    // 返回这个新对象
    return newObj;
}

其实,关于自己实现 new 。

用 apply call 或者 bind 都可以,看心情。装完逼的感觉,舒服。

如有不懂,可以移步至这里,一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类 ,完整了解 构造函数 , new ,实例对象 ,原型 ,原型链 ,以及 ES6 当中的类,这些大家老生常谈的概念之间的关系。

所以,这就是为什么,new 同样可以做到改变 this 指向。

let vm = this;

说这个改变 this 指向,虽然有点牵强了。但是,也不失为一种方法。

相当于在函数的外层,定义一个变量 vm ,来保存当前环境中的 this 。

然后,内层函数如果想访问最外层的 this 指向,仅仅只需要用 vm 即可。

因为,函数是可以创建单独的作用域的。而作用域是可向外访问不可向内访问的。就是我们之前讲过的探出头去。

这样子,就属于无脑操作,我管它什么 this 指向规则,我只知道,vm 一定是最外层的 this 。无论在嵌套多深的函数内访问 vm ,vm 始终都是最外层的 this 。

以上就是我们所有的关于 this 指向的内容。

系列目录