> For the complete documentation index, see [llms.txt](https://coffe1891.gitbook.io/frontend-hard-mode-interview/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://coffe1891.gitbook.io/frontend-hard-mode-interview/id-1/2.-he-xin-gai-nian/1.2.4.md).

# 壹.2.4 深入理解 call、apply、bind

上一篇我们知道，call、apply、bind都是和`this`指向有关的，这三个方法是JavaScript内置对象`Function`的原型的方法。相当一部分前端工程师对它们的理解仍旧比较浅显，所谓具备JavaScript基础扎实，是绕不开这些基础又常用的知识点的，这次让我们来深入理解并掌握它们。

## 壹.2.4.1 基本介绍

### 1. 语法

```javascript
func.call(thisArg, param1, param2, ...)//func是个函数

func.apply(thisArg, [param1,param2,...])

func.bind(thisArg, param1, param2, ...)
```

#### 返回值：

call / apply：返回`func` 执行的结果 ；\
bind：返回`func`的拷贝，并拥有指定的`this`值和初始参数。

#### 参数：

`thisArg`(可选):

1. **`func`的`this`指向`thisArg`对象`；`**
2. 非严格模式下：若`thisArg`指定为null，undefined，则`func`的`this`指向window对象；
3. 严格模式下：`func`的`this`为`undefined`；
4. 值为原始值(数字，字符串，布尔值)的this会指向该原始值的自动包装对象，如 String、Number、Boolean。

`param1`，`param2`(可选): 传给`func`的参数。

1. 如果param不传或为 null/undefined，则表示不需要传入任何参数.
2. apply第二个参数为类数组对象，数组内各项的值为传给`func`的参数。

### 2. 必须是函数才能调用call/apply/bind

call、apply 和 bind 是挂在`Function`对象上的三个方法，只有函数才有这些方法。只要是函数就可以调用它们，比如: `Object.prototype.toString`就是个函数，我们经常看到这样的用法：`Object.prototype.toString.call(data)`。

### 3. 作用

改变函数执行时的`this`指向，目前所有关于它们的运用，都是基于这一点来进行的。

### 4. 如何不弄混call和apply

弄混这两个方法的不在少数，不要小看这个问题，记住下面的这个方法就好了。双a记忆法：`apply`是以a开头，它传给`func`的参数是类Array对象（类数组对象），也是以a开头的。

**call与apply的唯一区别**

传给`func`的参数写法不同：

* apply是第2个参数，这个参数是一个类数组对象：传给`func`参数都写在数组中。
* call从第2\~n的参数都是传给`func`的。

**call/apply与bind的区别**

**执行：**

* call/apply改变了函数的`this`的指向并马上**执行该函数**；
* bind则是返回改变了`this`指向后的函数，**不执行该函数**。

**返回值：**

* call/apply 返回`func`的执行结果；
* bind返回`func`的拷贝，并指定了`func`的`this`指向，保存了`func`的参数。

返回值这段在下方bind应用中有详细的示例解析。

## 壹.2.4.2 什么是类数组？

先说数组，这我们都熟悉。它的特征有：可以通过索引(index)调用，如 array\[0]；具有长度属性length；可以通过 for 循环或forEach方法，进行遍历。

那么，类数组是什么呢？顾名思义，就是**具备与数组特征类似的对象**。比如，下面的这个对象，就是一个类数组。

```javascript
let arrayLike = {
    0: 1,
    1: 2,
    2: 3,
    length: 3
};
```

类数组 arrayLike 可以通过下标进行调用，具有length属性，同时也可以通过 for 循环进行遍历。

类数组，还是比较常用的，只是我们平时可能没注意到。比如，我们获取 DOM 节点的方法，返回的就是一个类数组；再比如，在一个函数体中使用 arguments 获取到的所有参数，也是一个类数组。

但是需要注意的是：**类数组无法使用 forEach、splice、push 等数组原型链上的方法**，毕竟它不是真正的数组。那么类数组想使用数组原型链上的方法，该怎么办呢？请继续往下看。

## 壹.2.4.3 call/apply/bind的核心理念：借用方法

什么是借用方法，让我们打个比方。

#### 生活中：

平时没时间做饭的我，周末想给家人炖个牛肉火锅尝尝。但是没有适合的锅，而我又不想出去买，所以就向邻居借了一个锅来用，这样既达到了目的，又节省了开支，一举两得。

#### 程序中：

A对象有个方法，B对象因为某种原因也需要用到同样的方法，那么这时候我们是单独为 B 对象扩展一个方法呢，还是借用一下 A 对象的方法呢？

当然是借用 A 对象的方法更便捷，既达到了目的，又节省了内存。

**这就是call/apply/bind的核心理念：借用方法**。**借助已实现的方法，改变方法中数据的this指向，减少重复代码，节省内存。**

还记得刚才的类数组么？如果它想借用 Array 原型链上的slice方法，可以这样：

```javascript
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
```

以此类推，domNodes 就可以应用 Array 下的其他方法了。

## 壹.2.4.4 应用场景

这些应用场景，多加体会就可以发现它们的理念都是：借用方法。

### 1. 判断数据类型

`Object.prototype.toString`用来判断类型再合适不过，借用它我们几乎可以判断所有类型的数据：

```javascript
function isType(data, type) {
  const typeObj = {
    "[object String]": "string",
    "[object Number]": "number",
    "[object Boolean]": "boolean",
    "[object Null]": "null",
    "[object Undefined]": "undefined",
    "[object Object]": "object",
    "[object Array]": "array",
    "[object Function]": "function",
    "[object Date]": "date", // Object.prototype.toString.call(new Date())
    "[object RegExp]": "regExp",
    "[object Map]": "map",
    "[object Set]": "set",
    "[object HTMLDivElement]": "dom", // document.querySelector('#app')
    "[object WeakMap]": "weakMap",
    "[object Window]": "window", // Object.prototype.toString.call(window)
    "[object Error]": "error", // new Error('1')
    "[object Arguments]": "arguments"
  };

  let name = Object.prototype.toString.call(data); // 借用Object.prototype.toString()获取数据类型
  let typeName = typeObj[name] || "未知类型"; // 匹配数据类型
  return typeName === type; // 判断该数据类型是否为传入的类型
}

console.log(
  isType({}, "object"), //>> true
  isType([], "array"), //>> true
  isType(new Date(), "object"), //>> false
  isType(new Date(), "date") //>> true
);
```

### 2. 类数组对象借用数组的方法

因为类数组对象不是真正的数组，所以没有数组类型上自带的一些方法，所以我们需要去借用数组的方法。

比如借用数组的push方法：

```javascript
//类数组对象
var arrayLike = {
  0: "OB",
  1: "Koro1",
  length: 2
};

Array.prototype.push.call(arrayLike, "添加数组项1", "添加数组项2");

console.log(arrayLike);
//>> {"0":"OB","1":"Koro1","2":"添加数组项1","3":"添加数组项2","length":4}
```

### 3. apply获取数组最大值最小值

apply直接传递数组当作要调用方法的参数，也省去了展开数组这一步，比如使用Math.max、Math.min来获取数组的最大值/最小值。

```javascript
const arr = [15, 6, 12, 13, 16];

const max = Math.max.apply(Math, arr); // 16

const min = Math.min.apply(Math, arr); // 6
```

### 4. 继承

ES5的继承也都是通过借用父类的构造方法来实现父类方法/属性的继承：

```javascript
// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型

}

supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};

// 子类
function sub(name, age) {
    // 借用父类的方法：修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}

// 重写子类的prototype，修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}

inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};

// 实例化子类，可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')

console.log(instance1);
//>> {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2);
//>> {"name":"小明","colors":["red","blue","green"],"age":18}
```

类似的应用场景还有很多，就不再一一列举了。关键在于它们借用方法的理念，若不理解的话可以多看几遍。

### 5. call、apply应该用哪个？

call,apply的效果完全一样，它们的区别也在于

* **参数数量/顺序确定就用call，参数数量/顺序不确定的话就用apply**。
* 考虑可读性：参数数量不多就用call，参数数量比较多的话，把参数整合成数组，使用apply。
* 参数集合已经是一个数组的情况，用apply，比如上文的获取数组最大值/最小值。

参数数量/顺序不确定的话就用apply，比如以下示例：

```javascript
const obj = {
    age: 24,
    name: 'OBKoro1',
}

const obj2 = {
    age: 777
}

callObj(obj, handle);
callObj(obj2, handle);

// 根据某些条件来决定要传递参数的数量、以及顺序
function callObj(thisAge, fn) {
    let params = [];

    if (thisAge.name) {
        params.push(thisAge.name);
    }

    if (thisAge.age) {
        params.push(thisAge.age);
    }

    fn.apply(thisAge, params); // 数量和顺序不确定 不能使用call
}

function handle(...params) {
    console.log('params', params); // do some thing
}
```

### 6. bind的应用场景

#### 保存函数参数

首先来看下一道经典的面试题：

```javascript
for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i) //>> 6 6 6 6 6
    }, i * 1000);
}
```

造成这个现象的原因是等到setTimeout异步执行时,i已经变成6了。那么如何使他输出: 1,2,3,4,5呢？可以通过bind来巧妙实现。

```javascript
for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i) //>> 1 2 3 4 5
    }.bind(null, i), i * 1000);
}
```

实际上这里也用了闭包，我们知道bind会返回一个函数，这个函数也是闭包（下一篇会深度介绍“闭包”的相关知识）。

它保存了函数的this指向、初始参数，每次i的变更都会被bind的闭包存起来，所以输出1-5。具体细节，下面有个手写bind方法，详细阅读一下就能搞明白。

#### 回调函数this丢失问题

这是一个常见的问题，下面是我在开发VSCode插件处理webview通信时，遇到的真实问题，一开始以为VSCode的API哪里出问题，调试了一番才发现是this指向丢失的问题。

```javascript
class Page {
    constructor(callBack) {
        this.className = 'Page';
        this.MessageCallBack = callBack; //回调函数
        this.MessageCallBack('发给注册页面的信息'); // 执行PageA的回调函数
    }
}

class PageA {
    constructor() {
        this.className = 'PageA';
        //问题在下面这句
        this.pageClass = new Page(this.handleMessage);//注册页面 传递回调函数 
    }

    // 与页面通信回调
    handleMessage(msg) {
        console.log('处理通信', this.className, msg); // 'Page' this指向错误
    }
}

new PageA();
```

回调函数this为何会丢失？显然声明的时候不会出现问题，执行回调函数的时候也不可能出现问题。问题出在传递回调函数的时候：

```javascript
this.pageClass = new Page(this.handleMessage);
```

因为传递过去的`this.handleMessage`是函数内存地址，没有附带上下文对象，也就是说该函数`this.handleMessage`没有绑定它的this指向。

既然知道问题了，那我们只要绑定回调函数的this指向为PageA就解决问题了。

#### 回调函数this丢失的解决方案

**（1）bind绑定回调函数的this指向：**

这是典型bind的应用场景, 绑定this指向，用做回调函数。

```javascript
this.pageClass = new Page(this.handleMessage.bind(this));
//绑定回调函数的this指向
```

**（2）用箭头函数绑定this指向：**

箭头函数的this指向定义的时候外层第一个普通函数的this，在这里指的是class类：PageA

```javascript
this.pageClass = new Page(() => this.handleMessage());
//箭头函数绑定this指向
```

## 壹.2.4.5 面试题：用原生JavaScript实现call/apply、bind

在一线互联网公司的面试中，手写实现call、apply、bind(特别是bind)一直是比较高频的面试题，在这里我们也一起来实现一下这几个函数。

### 1. 实现call

**实现思路**

1. 参考call的语法规则，需要设置一个参数`thisArg`，也就是this的指向；
2. 将`thisArg`封装为一个Object；
3. 通过为`thisArg`创建一个临时方法，这样`thisArg`就是调用该临时方法的对象了，会将该临时方法的this隐式指向到`thisArg`上（参考上一篇文章[《壹.2.3 彻底搞懂this》](/frontend-hard-mode-interview/id-1/2.-he-xin-gai-nian/1.2.3.md)）；
4. 执行`thisArg`的临时方法，并传递参数；
5. 删除临时方法，返回方法的执行结果。

```javascript
/**
 * 用原生JavaScript实现call
 */
Function.prototype.myCall = function(thisArg, ...arr) {

  //1.判断参数合法性/////////////////////////
  if (thisArg === null || thisArg === undefined) {
    //指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    thisArg = window;
  } else {
    thisArg = Object(thisArg);//创建一个可包含数字/字符串/布尔值的对象，
                              //thisArg 会指向一个包含该原始值的对象。
  }

  //2.搞定this的指向/////////////////////////
  const specialMethod = Symbol("anything"); //创建一个不重复的常量
  //如果调用myCall的函数名是func，也即以func.myCall()形式调用；
  //根据上篇文章介绍，则myCall函数体内的this指向func
  thisArg[specialMethod] = this; //给thisArg对象建一个临时属性来储存this（也即func函数）
  //进一步地，根据上篇文章介绍，func作为thisArg对象的一个方法被调用，那么func中的this便
  //指向thisArg对象。由此，巧妙地完成将this隐式地指向到thisArg！
  let result = thisArg[specialMethod](...arr);

  //3.收尾
  delete thisArg[specialMethod]; //删除临时方法
  return result; //返回临时方法的执行结果
};

let obj = {
  name: "coffe1891"
};

function func() {
  console.log(this.name);
}

func.myCall(obj);//>> coffe1891
```

认真读一下注释，基本就能理解了，主要还是用到判断`this`指向的规则。如果读完还是不明白，可以加我的微信（见本书封面）交流。附带更正一下，其中第11行：

> 当以非构造函数形式被调用时，`Object` 等同于 `new Object()`。\
> <https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object>

**多讲一下如何正确地判断thisArg：**

上面代码判断`thisArg`的写法是经过调整后的严谨写法，因为之前笔者发现很多前端工程师判断参数thisArg，只是简单的以是否为false来判断，比如：

```javascript
// 不够严谨
thisArg = thisArg ? Object(thisArg) : window;
thisArg = thisArg || window;
```

经过测试，以下三种情况，thisArg都会意外地绑定到window上：

```javascript
// 参数明明传了值，本意不是要让thisArg指向window，然后结果却意外地指向了window
func.myCall(''); // window
func.myCall(0); // window
func.myCall(false); // window
```

所以应该严谨一点儿判断，如下：

```javascript
// 严谨的判断
if (thisArg === null || thisArg === undefined) {
  // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
  thisArg = window;
} else {
  thisArg = Object(thisArg); 
  //创建一个包含原始值（数字，字符串，布尔值）的对象，
  //thisArg 会指向一个包含该原始值的对象。
}
```

### 2. 实现Apply

#### 实现思路

1. 传递给函数的参数处理，不太一样，其他部分跟call一样；
2. apply接受第二个参数为类数组对象, 这里用了《JavaScript权威指南》一书中判断是否为类数组对象的方法。

```javascript
/**
 * 用原生JavaScript实现apply
 */
Function.prototype.myApply = function(thisArg) {
  if (thisArg === null || thisArg === undefined) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }

  //判断是否为【类数组对象】
  function isArrayLike(o) {
    if (
      o && // o不是null、undefined等
      typeof o === "object" && // o是对象
      isFinite(o.length) && // o.length是有限数值
      o.length >= 0 && // o.length为非负值
      o.length === Math.floor(o.length) && // o.length是整数
      o.length < 4294967296
    )
      // o.length < 2^32
      return true;
    else return false;
  }

  const specialMethod = Symbol("anything");
  thisArg[specialMethod] = this;

  let args = arguments[1]; // 获取参数数组
  let result;

  // 处理传进来的第二个参数
  if (args) {
    // 是否传递第二个参数
    if (!Array.isArray(args) && !isArrayLike(args)) {
      throw new TypeError(
        "第二个参数既不为数组，也不为类数组对象。抛出错误"
      );
    } else {
      args = Array.from(args); // 转为数组
      result = thisArg[specialMethod](...args); // 执行函数并展开数组，传递函数参数
    }
  } else {
    result = thisArg[specialMethod]();
  }

  delete thisArg[specialMethod];
  return result; // 返回函数执行结果
};
```

### 3. 实现bind

手写实现bind是一线互联网企业中的一个高频的面试题，如果面试的中高级前端，只是能说出它们的区别、用法并不能让你脱颖而出，对bind的理解要有足够的深度，才能得到面试官的赞许。

#### 实现思路

（1）拷贝调用函数:

* 调用函数，也即调用`myBind`的函数，用一个变量临时储存它；
* 使用`Object.create`复制调用函数的prototype给`funcForBind`；

（2）返回拷贝的函数`funcForBind`；

（3）调用拷贝的函数`funcForBind`：

* `new`调用判断：通过`instanceof`判断函数是否通过`new`调用，来决定绑定的context；
* 通过`call`绑定this、传递参数；
* 返回调用函数的执行结果。

这里给出一种基于ES6的实现（基于老版ES的实现方式也不少，但没有ES6的[spread和rest操作符](/frontend-hard-mode-interview/id-1/2.-he-xin-gai-nian/1.2.12.md)实现简洁）：

```javascript
/**
 * 用原生JavaScript实现bind
 */
Function.prototype.myBind = function(objThis, ...params) {
  const thisFn = this;//存储调用函数，以及上方的params(函数参数)
  //对返回的函数 secondParams 二次传参
  let funcForBind = function(...secondParams) {
    //检查this是否是funcForBind的实例？也就是检查funcForBind是否通过new调用
    const isNew = this instanceof funcForBind;

    //new调用就绑定到this上,否则就绑定到传入的objThis上
    const thisArg = isNew ? this : Object(objThis);

    //用call执行调用函数，绑定this的指向，并传递参数。返回执行结果
    return thisFn.call(thisArg, ...params, ...secondParams);
  };

  //复制调用函数的prototype给funcForBind
  funcForBind.prototype = Object.create(thisFn.prototype);
  return funcForBind;//返回拷贝的函数
};
```

上面的代码第7行可能不好理解，二次传参(secondParams)是说什么？举个例子：

```javascript
let func = function(p,secondParams){//其实测试用的func其参数可以是任意多个
    console.log(p.name);
    console.log(this.name);
    console.log(secondParams);
}
let obj={
    name:"1891"
}
func.myBind(obj,{name:"coffe"})("二次传参");
//>> coffe
//>> 1891
//>> 二次传参
```

上面第8行调用myBind之后，会返回一个新函数，然后再给这个新函数传入参数，就是二次传参。
