手写call、apply、bind及new函数

566 阅读5分钟

一、概述

我们都知道call、apply、bind的作用是改变某个函数运行时的上下文(context),也就是改变函数内部this指向。new操作符通过把this固化在实例对象上改变this的指向。本文从this的指向出发重点研究call、apply、bind以及new的使用方法,以及它们内部的实现原理。

二、this指向

使用 JavaScript 开发的时候,很多开发者多多少少会被 this 的指向搞蒙圈。没有弄清楚this的指向到底是什么,所以在某些情况下,this得到的不是我们想要的值。本节内容就来分析一下函数中this指向的问题,在此分享出来也方便自己今后巩固学习。函数分为普通函数和箭头函数,它们的this指向可以总结为以下情况。

1、普通函数中this

(1) 默认情况下,没有直接调用者,this指向window。

this指向window。所以this.a === window.a。因此输出1。

(2)总是代表着它的直接调用者,如obj.foo,this就是指向obj。

函数foo的调用者是obj。因此this指向obj。this.a 就等于obj.a。因此输出2。

(3)严格模式下(设置了'use strict'),this为undefined。

(4)当使用call,apply,bind绑定的,this指向绑定对象。

call、apply、bind都将this绑定到了对象o上。因此this.color 相当于o.color。因此都打印出blue。

(5)对于new 的方式来说,this永远被绑定在实例化对象上。不会被任何方式改变。

2、箭头函数中this

(1)箭头函数没有自己的this, 它的this是继承而来; 默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象, 定义它的时候,可能环境是window。

因为foo函数定义在全局中,因此所处的上下文就是window。即this指向window。即使通过obj来调用foo函数也无法该变this的指向。

setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在Person函数生成时。因此this指向Person。如果是普通函数,情况就不一样了。

setTimeout里匿名函数没有直接调用者,执行时this应该指向全局对象window。因此指向this.s2++实际是window.s2++,所以会输出4。而timer.s2没有改变,输出为0。

(2)即使是call,apply,bind等方法也不能改变箭头函数this的指向.

通过call函数也没有改变this的指向。

以上就是this指向的规则,可通过下图来说明this的指向。

但是可能会有多个规则同时出现的情况,这个时候不同的规则之间会根据优先级最高的来决定this的最终指向。首先new方式优先级最高,接下来是bind、call、apply这些函数,然后是obj.foo()这种调用方式。最后是foo()调用方式。同时箭头函数的this一旦被绑定就不会再被任何方式所改变。

上图说明call的优先级高于obj.foo()的优先级。

上图说明new的优先级高于bind。bar 被硬绑定到 obj1 上, 但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为 3。 相反,new 修改了硬绑定( 到 obj1 的) 调用 bar(..) 中的 this。 因为使用了 new 绑定, 我们得到了一个名字为 baz 的新对象, 并且 baz.a 的值是 3

三、call、apply、bind的用法和区别

1、call方法

用法: A.call(B,x,y)。

(1)改变函数A的this指向,使之指向B。

(2)把A函数放到B中运行,x和y是A函数的参数。

通过Person.call()在Chinese函数中执行了Person函数。当this上面挂载sayName函数。通过实例化创建chinese对象,此时this指向chinese。所以chinese上可以找到sayName()函数并且执行。call函数的参数是一个一个传递的。

2、apply方法

用法: A.apply(B,[x,y])。

(1)改变函数A的this指向,使之指向B.

(2)把A函数放到B中运行,A函数的参数是数组。这是跟call方法不同的地方。

3、bind方法

用法: A.bind(B,x,y)。

(1)改变函数A的this指向,使之指向B。

(2)把A函数放到B中运行,x和y是A函数的参数。

(3)bind 是返回新的函数。

bind返回一个新函数,执行此函数后实例chinese上才会有sayName函数。否则调用sayName会报错。

四、手写call、apply、bind、以及new函数

1、call函数的实现

首先context(上下文)为可选参数,不传的话默认上下文为window。接下来给context创建一个fn属性,并将值设置为需要调用的函数(text)。通过[...arguments].slice(1)获取myCall传入的参数。并在调用的时候传入。最后是调用函数并且删除对象上的函数。

2、apply函数的实现

apply的实现思路跟call的实现思路类似。区别在于对参数的处理。

3、bind函数的实现

4、new函数的实现

在调用new的过程中会发生以下4件事情:

(1) 新生成一个对象。

(2) 绑定this到新生成的对象上。

(3) 执行构造函数中的代码(即为这个新对象添加属性)。

(4) 返回新对象。

根据以上几个过程,实现一个new函数如下

以下是对实现的分析:

1、创建一个空对象

2、获取构造函数

3、设置空对象的原型

4、绑定this并执行构造函数

5、确保返回值为对象

五、总结

本文分析this在不同场景的指向问题,通过自己写call、apply、bind以及new函数加深对其原理的理解。