JavaScript - 手写call、apply和bind函数

简介: JavaScript - 手写call、apply和bind函数

前言

无论在面试时还是使用中,难免会遇到改变this指向的问题,这时我们便会想到call、apply、bind,可对于他们的底层是如何实现,大多数人不太清楚,如果你对他们还不了解,先看看mdn的callapplybind

本文尽量用简洁的语言讲解他们的用法,底层实现思路,模拟实现 call、apply、bind

模拟call

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

function.call(this, arg1, arg2, arg3, ...)

根据定义得知,call()方法有两个作用,一是改变 this 指向,另外一个传递参数,如下:

const obj = {
  value: '我是obj.value',
}
function fn(arg) {
  console.log(this.value, arg); // '我是obj.value', 2
}
fn.call(obj, 2);

上面的例子,使用 call()方法使函数fnthis 指向了obj,所以 this.value 的值为 我是obj.value

那么如果不使用 call()方法,该如何实现呢?

call 实现思路

不考虑使用 call、apply、bind 方法,上面例子 fn 函数如何能拿到 obj 里面的 value 值呢?

改造一下上面的例子

const obj = {
  value: '我是obj.value',
  fn: function() {
    console.log(this.value); // '我是obj.value'
  }
}
obj.fn();

这样一改,this 就指向了 obj,根据这个思路,封装一个将传入的 this转换的方法,那么当前 this 的指向就是我们想要的结果。

需要注意fn函数不能写成箭头函数,因为箭头函数没有this。

所以模拟的步骤为:

  1. 将函数设置为传入对象的属性;
  2. 执行该函数;
  3. 删除该属性; 上面的例子就可以改写为:
// 给obj添加属性func
obj.func = fn;
// 执行函数
obj.func();
// 删除添加的属性
delete obj.func;

开始模拟

根据上面的思路,来模拟实现一版 call()方法。

Function.prototype.myCall = function (obj) {
  obj.func = this;
  obj.func();
  delete obj.func;
}

看下这个极简版 call 方法,能否能正确改变 this。

const obj = {
  value: '我是obj.value',
}
function fn(arg) {
  console.log(this.value, arg); // '我是obj.value', undefined
}
fn.myCall(obj, 2);

实现了!!! 根据上面例子,已经能正确改变 this 的指向!!!

但是传入的值却没有拿到,考虑的传入的值是不确定的,只能借助Arguments 对象(通过它可以拿到所有传入的参数)

现在还有两个问题:

  1. 传入的值却没有拿到,我们可以借助Arguments 对象,再加上es6的'...'操作符将事半功倍。
  2. 自带的call()是有返回值的,我们目前还没有,解决这个问题很好办,在封装的 call 方法里面,将执行的函数结果存下来,return 出来即可

模拟call最终版

使用到了es6的语法糖。

Function.prototype.myCall = function () {
  const [context, ...args] = [...arguments];
  // 在传入的对象上设置属性为待执行函数
  context.fn = this;
  // 执行函数 并获取其返回值
  const res = context.fn(...args);
  // 删除属性
  delete context.fn;
  // 返回执行结果
  return res;
}
const obj = {
  value: '我是obj.value',
}
function fn(canshu1,canshu2,canshu3) {
  console.log(this.value);  // 我是obj.value
  return [canshu1,canshu2,canshu3]
}
const xxx = fn.myCall(obj, 2,3,4);
console.log(xxx) // [2,3,4]

到此,模拟实现了 call 方法。

模拟apply

定义: 调用一个具有给定 this 值的函数,及以一个数组的形式提供的参数。

func.apply(thisArg, [argsArray]);
复制代码

applycall的区别在于传参方式

call 的参数是分开传递,而 apply 则用数组传,如下:

const obj = {
    value: '我是obj.value',
};
function fn() {
    console.log(this.value); // '我是obj.value'
    return [...arguments]
}
console.log(fn.apply(obj, [1, 2])); // [1, 2]

思路

apply 的实现思路和 call 一样,需要考虑的是 apply 只有两个参数,因此,根据 call 的思路实现如下:

Function.prototype.apply1 = function (context, args) {
    // 给传入的对象添加属性,值为当前函数
    context.fn = this;
    // 判断第二个参数是否存在,不存在直接执行,否则拼接参数执行,并存储函数执行结果
    let res = !args ? context.fn() : context.fn(...args)
    // 删除新增属性
    delete context.fn;
    // 返回函数执行结果
    return res;
}
const obj = {
    value: '我是obj.value',
};
function fn() {
    console.log(this.value); // '我是obj.value'
    return [...arguments]
}
console.log(fn.apply(obj, [1, 2])); // [1, 2]

模拟bind

bindcallapply的区别 ,在于callapply是直接执行,而bind 方法会创建一个新的函数,返回这个函数,并允许传入参数。

首先看一个例子。

const obj = {
  value: '我是obj.value',
  fn: function () {
    return this.value;
  },
};
const func = obj.fn;
console.log(func()); // undefined

为什么会输出 undefined 呢?这涉及到了 this 的问题,不清楚的可以看这里

简单来讲,函数的this取决于谁调用它,直接运行func(),相当于是window.func()

window.func()的this自然是windowfunc函数中想要输出window.value,而在window上没有定义value,那输出的值自然就是undefined

此时就发现,this的值总是乱变,那如何让函数中的锁定this为某对象呢?bind就可以。

const obj = {
  value: '我是obj.value',
  fn: function () {
    return this.value;
  },
};
const func = obj.fn;
const bindFunc = func.bind(obj);
console.log(bindFunc()); // 我是obj.value

开始写自己的bind

首先解决 bind 的第一个问题,返回一个函数。

其中改变 this 指向问题,可以使用 call 和 apply 方法,可参照上面的实现方式。

Function.prototype.myBind = function (context) {
  // 将当前函数的this存放起来
  const _self = this;
  return function () {
    // 改变this
    return _self.apply(context);
  };
};

this 改变了后,需要考虑传参问题,参数是不定的,所以我们使用arguments。

// 关于对此行代码的解释,我写在了本文的最下方
// 此刻可以理解为 arguments.toArray().slice(1)
const args = Array.prototype.slice.call(arguments, 1);

参数取到后,将参数传入即可,最终代码如下:

Function.prototype.myBind = function (context) {
    // 将当前函数的this存放起来
    const _self = this;
    // 绑定bind传入的参数,从第二个开始
    const args = Array.prototype.slice.call(arguments, 1);
    return function () {
      // 绑定bind返回新的函数,执行所带的参数
      const bindArgs = Array.prototype.slice.apply(arguments);
      // 改变this
      return _self.apply(context, [...args, ...bindArgs]);
    };
};
const obj = {
    value: "我是obj.value",
    fn: function (value1, value2, value3, value4) {
      console.log("value1:", value1); // 我是传给fn方法的第一个参数
      console.log("value2:", value2); // 俺是第二个
      console.log("value3:", value3); // 呼呼呼,我是第三个
      console.log("value4:", value4); // 也可以传第四个
      return this.value;
    },
};
const func = obj.fn;
const fn = func.myBind(obj, "我是传给fn方法的第一个参数", "俺是第二个");
console.log(fn("呼呼呼,我是第三个", "也可以传第四个"));

这里模拟的 bind 函数不是最终版,在 CDN 上有bind 实现;

对Array.prototype.slice.call(arguments,1) 的简要理解

首先对其进行简要拆分:

Array.prototype 属性表示 Array 构造函数的原型,并允许您向所有Array对象添加新的属性和方法。 slice(start,end) 是Array原型上的方法,可以对一个数组进行截取(从start开始,不包含end;如果只有 start 则截取从 start 到数组结束的所有元素。)并返回一个新的数组(浅拷贝)。

Array.prototype.slice.call(arguments,1) 可以理解为:改变数组slice方法的作用域,使 this 指向arguments对象,call() 方法的第二个参数表示传递给slice的参数即截取数组的起始位置。这样 arguments 类数组就可以使用数组的方法 slice() 了,否则由于 arguments 是类数组并不是真正的数组,他是不可以使用 Array 的相关方法的。

简单理解为: Array.prototype.slice.call(arguments,1) 能够将具有length属性(这一点需要注意,必须包含length属性)的对象转换为数组,简化记忆为:arguments.toArray().slice(1); 但有一个例外,IE下的节点集合它不能转换(因为IE下的dom对象是以com对象的形式实现,js对象和com对象不能进行转换)。



目录
相关文章
|
12天前
|
JavaScript 前端开发 Java
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
本文介绍了JavaScript中常用的函数和方法,包括通用函数、Global对象函数以及数组相关函数。详细列出了每个函数的参数、返回值及使用说明,并提供了示例代码。文章强调了函数的学习应结合源码和实践,适合JavaScript初学者和进阶开发者参考。
24 2
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
|
11天前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
25天前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
25天前
|
JavaScript 前端开发
JS高级—call(),apply(),bind()
【10月更文挑战第17天】call()`、`apply()`和`bind()`是 JavaScript 中非常重要的工具,它们为我们提供了灵活控制函数执行和`this`指向的能力。通过合理运用这些方法,可以实现更复杂的编程逻辑和功能,提升代码的质量和可维护性。你在实际开发中可以根据具体需求,选择合适的方法来满足业务需求,并不断探索它们的更多应用场景。
9 1
|
27天前
|
存储 JavaScript 前端开发
JS函数提升 变量提升
【10月更文挑战第6天】函数提升和变量提升是 JavaScript 语言的重要特性,但它们也可能带来一些困惑和潜在的问题。通过深入理解和掌握它们的原理和表现,开发者可以更好地编写和维护 JavaScript 代码,避免因不了解这些机制而导致的错误和不一致。同时,不断提高对执行上下文等相关概念的认识,将有助于提升对 JavaScript 语言的整体理解和运用能力。
|
1月前
|
JavaScript 前端开发
js教程——函数
js教程——函数
32 4
|
1月前
|
存储 JavaScript 前端开发
js中函数、方法、对象的区别
js中函数、方法、对象的区别
16 2
|
30天前
|
存储 JavaScript 前端开发
JavaScript数据类型全解:编写通用函数,精准判断各种数据类型
JavaScript数据类型全解:编写通用函数,精准判断各种数据类型
18 0
|
3月前
|
前端开发 JavaScript 开发者
揭秘JavaScript魔法三剑客:call、apply、bind,解锁函数新世界,你的前端之路因它们而精彩!
【8月更文挑战第23天】在 JavaScript 的世界里,`call`、`apply` 和 `bind` 这三个方法常常让新手感到困惑。它们都能改变函数执行时的上下文(即 `this` 的指向),但各有特点:`call` 接受一系列参数并直接调用函数;`apply` 则接收一个参数数组,在处理不确定数量的参数时特别有用;而 `bind` 不会立即执行函数,而是创建一个新版本的函数,其 `this` 上下文已被永久绑定。理解这三个方法能帮助开发者更好地运用函数式编程技巧,提升代码灵活性和可维护性。
37 0
|
1月前
|
JavaScript 前端开发
js 中call()和apply()
js 中call()和apply()
27 1