jsliang 求职系列 - 10 - 手写 call/apply/bind

3,822 阅读8分钟

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 最终实现
3.1 手写 call
3.2 手写 apply
3.3 手写 bind
四 Arguments 对象
五 call
5.1 原生 call
5.2 手写 call
六 apply
6.1 原生 apply
6.2 手写 apply
七 bind
7.1 原生 bind
7.2 手写 bind
八 题目
8.1 this 指向问题 1
8.2 this 指向问题 2
九 参考文献

二 前言

返回目录

面试官:手写一个 call/apply/bind

工欲善其事,必先利其器,我们先了解一下这 3 者有什么区别:

call 的使用

function Product (name, price) {
  this.name = name;
  this.price = price;
}

function Food (name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

const food = new Food('cheese', 5);
console.log(food.name); // 'cheese'
  • call:可以改变函数指向,第一个参数是要改变指向的对象,之后的参数形式是 arg1, arg2... 的形式
  • apply:基本同 call,不同点在于第二个参数是一个数组 [arg1, arg2...]
  • bind:改变 this 作用域会返回一个新的函数,这个函数不会马上执行

三 最终实现

返回目录

下面我们列一下今天的实现目标:

  1. 手写 call
  2. 手写 apply
  3. 手写 bind

3.1 手写 call

返回目录

Function.prototype.myCall = function(context = globalThis) {
  // 设置 fn 为调用 myCall 的方法
  context.fn = this;

  // 获取剩余参数
  const otherArg = Array.from(arguments).slice(1);

  // 调用这个方法,将剩余参数传递进去
  context.fn(otherArg);

  // 将这个方法的执行结果传给 result
  let result = context.fn();

  // 删除这个变量
  delete context.fn;

  // 返回 result 结果
  return result;
};

this.a = 1;

const fn = function() {
  this.a = 2;
  console.log(this.a);
}

fn.myCall(fn);

3.2 手写 apply

返回目录

Function.prototype.myApply = function(context = globalThis, arr) {
  // 设置 fn 为调用 myCall 的方法
  context.fn = this;

  let result;

  // 如果存在参数,则传递进去
  // 将结果返回给 result
  if (arr) {
    result = context.fn(arr);
  } else { // 否则不传
    result = context.fn();
  }

  // 删除这个变量
  delete context.fn;

  // 返回 result 结果
  return result;
};

this.a = 1;

const fn = function() {
  this.a = 2;
  console.log(this.a);
}

fn.myApply(fn);

3.3 手写 bind

返回目录

Function.prototype.myBind = function(context = globalThis) {
  // 设置 fn 为调用 myCall 的方法
  const fn = this;

  // 获取该方法剩余参数
  const otherArg = [...arguments].slice(1);

  // 设置返回的一个新方法
  const result = function() {

    // 获取返回方法体的参数
    const resultArg = [...arguments];

    // 如果是通过 new 调用的,绑定 this 为实例对象
    if (this instanceof result) {
      fn.apply(this, otherArg.concat(resultArg));
    } else { // 否则普通函数形式绑定 context
      fn.apply(context, otherArg.concat(resultArg));
    }
  }

  // 绑定原型链
  result.prototype = Object.create(fn.prototype);

  // 返回结果
  return result;
};

this.a = 1;

const fn = function() {
  this.a = 2;
  console.log(this.a);
}

fn.myBind(fn);
fn();

OK,懂了么,我们发车继续深造~

四 Arguments 对象

返回目录

arguments 是一个对应于传递给函数的参数的类数组对象。

function fun(a, b, c) {
  console.log(arguments[0]); // 1
  console.log(arguments[1]); // 2
  console.log(arguments[2]); // 3
}

arguments 对象不是一个 Array

它类似于 Array,但除了 length 属性和索引元素之外没有任何 Array 属性。

arguments 转为数组:

// ES5
var arg1 = Array.prototype.slice.call(arguments);
var arg2 = [].slice.call(arguments);

// ES6
var arg3 = Array.from(arguments);
var arg4 = [...arguments];

在手写 call/bind/apply 过程中,会用到 arguments 来获取方法体的传参,就好比手写 call 过程中,通常我们通过 Array.from(arguments).slice(1) 来获取第二个及后面的参数。

五 call

返回目录

5.1 原生 call

返回目录

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。

  • 语法:function.call(thisArg, arg1, arg2, ...)
    • thisArg:可选的。在 function 函数运行时使用的 this 值。
    • arg1, arg2, ...:指定的参数列表
function Product (name, price) {
  this.name = name;
  this.price = price;
}

function Food (name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

const food = new Food('cheese', 5);
console.log(food.name); // 'cheese'

5.2 手写 call

返回目录

首先我们得搞明白 call 的特性:

  1. 如果 obj.call(null),那么 this 应该指向 window
  2. 如果 obj1.call(obj2),那么谁调用它,this 指向谁(这里就是 obj2 了)
  3. call 可以传入多个参数,所以可以利用 arguments 这个字段来获取所有参数。将 arguments 转换数组后,获取除第一个参数外的其他参数
  4. 设置一个变量,用完可以删掉它

综上:

手写 call 的 JS 代码:

Function.prototype.myCall = function(context = globalThis) {
  // 设置 fn 为调用 myCall 的方法
  context.fn = this;

  // 获取剩余参数
  const otherArg = Array.from(arguments).slice(1);

  // 调用这个方法,将剩余参数传递进去
  context.fn(otherArg);

  // 将这个方法的执行结果传给 result
  let result = context.fn();

  // 删除这个变量
  delete context.fn;

  // 返回 result 结果
  return result;
};

this.a = 1;

const fn = function() {
  this.a = 2;
  console.log(this.a);
}

fn.myCall(fn);

小伙伴稍稍理解下,搞清楚它内部的流程,然后咱们实践一下:

防抖函数绑定手写 call

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>手写 call</title>
</head>
<body>
  <button class="btn">123</button>

  <script>
    (function() {
      Function.prototype.myCall = function(context) {
        const newContext = context || window;
        newContext.fn = this;
        const otherArg = Array.from(arguments).slice(1);
        newContext.fn(otherArg);
        const result = newContext.fn(otherArg);
        delete newContext;
        return result;
      };

      const debounce = function(fn) {
        let timer = null;
        return function() {
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.myCall(this, arguments);
          }, 1000);
        }
      }

      let time = 0;
      const getNumber = function() {
        console.log(++time);
      }

      const btn = document.querySelector('.btn');
      btn.addEventListener('click', debounce(getNumber));
    })()
  </script>
</body>
</html>

这样我们就摸清了手写 call

六 apply

返回目录

6.1 原生 apply

返回目录

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

  • 语法:function.apply(thisArg, [argsArray])
    • thisArg:必选的。在 function 函数运行时使用的 this
    • [argsArray]:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。
const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);
console.log(max); // 7

const min = Math.min.apply(null, numbers);
console.log(min); // 2

6.2 手写 apply

返回目录

下面我们开始尝试手写 apply,记住这个方法和 call 类似,理解起来也不难:

Function.prototype.myApply = function(context = globalThis, arr) {
  // 设置 fn 为调用 myCall 的方法
  context.fn = this;

  let result;

  // 如果存在参数,则传递进去
  // 将结果返回给 result
  if (arr) {
    result = context.fn(arr);
  } else { // 否则不传
    result = context.fn();
  }

  // 删除这个变量
  delete fcontext.fnn;

  // 返回 result 结果
  return result;
};

this.a = 1;

const fn = function() {
  this.a = 2;
  console.log(this.a);
}

fn.myApply(fn);

用自定义 apply + 防抖实践一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>DOM 操作</title>
</head>
<body>
  <button class="btn">123</button>

  <script>
    (function() {
      Function.prototype.myApply = function(context, arr) {
        const newContext = context || window;
        newContext.fn = this;
        console.log(newContext.fn)
        if (!arr) {
          result = newContext.fn();
        } else {
          result = newContext.fn(arr);
        }

        delete newContext;
        return result;
      };

      const debounce = function(fn, number) {
        let timer = null;
        return function() {
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.myApply(this, number);
          }, 1000);
        }
      }

      const getNumber = function(time) {
        console.log(time);
      }

      let number = [1, 2, 3, 4, 5];
      const btn = document.querySelector('.btn');
      btn.addEventListener('click', debounce(getNumber, number));
    })()
  </script>
</body>
</html>

这样我们就摸清了手写 apply

七 bind

返回目录

7.1 原生 bind

返回目录

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

  • 语法:function.bind(thisArg, arg1, arg2, ...)
    • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。
    • arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
  • 返回值:一个原函数的拷贝,并拥有指定的 this 值和初始参数
const module = {
  x: 42,
  getX: function() {
    return this.x;
  },
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined
// 谁调用指向谁,这里 unboundGetX = module.getX
// 让 getX 里面的 this 指向了 window
// 而 window 里面并没有 x 方法
// 当然,在前面加上 window.x = 43 就有了

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42
// 通过 bind,将 this 指向 module

7.2 手写 bind

返回目录

手写 bind 稍微有点小复杂,但是小伙伴们别慌,多读几遍可以摸清套路:

Function.prototype.myBind = function(context = globalThis) {
  // 设置 fn 为调用 myCall 的方法
  const fn = this;

  // 获取该方法剩余参数
  const otherArg = [...arguments].slice(1);

  // 设置返回的一个新方法
  const result = function() {

    // 获取返回方法体的参数
    const resultArg = [...arguments];

    // 如果是通过 new 调用的,绑定 this 为实例对象
    if (this instanceof result) {
      fn.apply(this, otherArg.concat(resultArg));
    } else { // 否则普通函数形式绑定 context
      fn.apply(context, otherArg.concat(resultArg));
    }
  }

  // 绑定原型链
  result.prototype = Object.create(fn.prototype);

  // 返回结果
  return result;
};

this.a = 1;

const fn = function() {
  this.a = 2;
  console.log(this.a);
}

fn.myBind(fn);
fn();

八 题目

返回目录

通过上面的实践,小伙伴们应该对手写 callbindapply 有自己的理解了,下面看看这些题,试试挑战下。

8.1 this 指向问题 1

返回目录

var color = 'green';

var test4399 = {
  color: 'blue',
  getColor: function() {
    var color = 'red';
    console.log(this.color);
  },
};

var getColor = test4399.getColor;
getColor(); // 输出什么?
test4399.getColor(); // 输出什么?

答案:greenblue

8.2 this 指向问题 2

返回目录

var myObject = {
  foo: 'bar',
  func: function() {
    var self = this;
    console.log(this.foo);
    console.log(self.foo);
    (function() {
      console.log(this.foo);
      console.log(self.foo);
    })()
  }
}
myObject.func();

程序输出什么?

  • A:bar bar bar bar
  • B:bar bar bar undefined
  • C:bar bar undefined bar
  • D:undefined bar undefined bar

答案:C

  1. 第一个 this.foo 输出 bar,因为当前 this 指向对象 myObject
  2. 第二个 self.foo 输出 bar,因为 selfthis 的副本,同指向 myObject 对象。
  3. 第三个 this.foo 输出 undefined,因为这个 IIFE(立即执行函数表达式)中的 this 指向 window。 4.第四个 self.foo 输出 bar,因为这个匿名函数所处的上下文中没有 self,所以通过作用域链向上查找,从包含它的父函数中找到了指向 myObject 对象的 self

九 参考文献

返回目录


jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 github.com/LiangJunron… 上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。