hzzzzzzzq / Blog

这是我记录博客的地方,希望能多写一些技术博客,JS、ES、React、Vue等
14 stars 0 forks source link

深入理解JavaScript系列(8):call 和 apply 函数 #49

Open hzzzzzzzq opened 2 years ago

hzzzzzzzq commented 2 years ago

call bind apply 的区别

首先,我们先来了解一下 call,apply,bind 的区别,方便我们去手写实现自己的 call、apply、bind

区别 bind call apply
是否立即调用
参数一 指定的对象(该函数的执行上下文 指定的对象(该函数的执行上下文 指定的对象(该函数的执行上下文
其他参数 后面的参数都是传入函数的值 后面的参数都是传入函数的值 只有两个参数,第二个参数是数组

call 实现

我们可以分为七个步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 处理传入的参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果

第一版

call 在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个参数或方法。

let value = 1;
const foo = {
  value: 2,
};
function fn() {
  console.log(this.value);
}
fn.call(foo); // 2

我们来看看步骤:

Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    console.log('只有函数可以调用 myCall');
    return;
  }
  context.fn = this;
  context.fn();
  delete context.fn;
};

这时候我们会发现打印结果对上了,也是 2

第二版

接下来就是处理参数了,从区别可以知道,call 是可以有无数参数的,我们在第二步之前添加一个步骤。

先来看看真实 call 的测试。

let value = 1;
const foo = {
  value: 2,
};
function fn(name, age) {
  console.log(this.value);
  console.log(name);
  console.log(age);
}
fn.call(foo, 'hzzzzzzzq', 18);
// 2
// hzzzzzzzq
// 18
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    console.log('只有函数可以调用 myCall');
    return;
  }
  let args = [...arguments].slice(1); // 截取参数
  context.fn = this;
  context.fn(...args); // 给函数传入参数
  delete context.fn;
};

使用我们的 myCall 执行,发现结果是相同的。

最后版

在这里我们需要注意两个问题

注意,我将外层的定义 let 改成了 var,因为 var 设置了全局变量,如果设置 let 则会打印 undefined,并没有在 window 对象中定义。

var value = 1;
const foo = {
  value: 2,
};
function fn(name, age) {
  console.log(this.value);
}
fn.call(null); // 1

执行代码,可以知道,传入 null 时,指向了我们的 window 对象。

var value = 1;
const foo = {
  value: 2,
};
function fn(name, age) {
  return {
    value: this.value,
    name,
    age,
  };
}
const result = fn.call(foo, 'hzzzzzzzq', 18);
console.log(result);
// {
//   value: 2,
//   name: 'hzzzzzzzq',
//   age: 18
// }

首先处理传入 null 的时候,判断传入上下文对象是否存在,如果不存在,则设置为 window。 处理第二个问题则是将方法执行结果进行保留,返回结果。

Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    // 判断,在手写过程中 可以不需要
    console.log('只有函数可以调用 myCall');
    return;
  }
  context = context || window; // 判断上下文对象是否存在
  context.fn = this; // 将函数作为上下文的一个属性
  let args = [...arguments].slice(1); // 获取传入的参数,从第二个参数起的所有参数
  const result = context.fn(...args); // 执行该函数,并使用上面的参数
  delete context.fn; // 删除新增对象
  return result; // 返回结果
};

测试一下我们自己写的 call 代码

var value = 2;
let obj = {
  value: 1,
};
function test(name, age) {
  console.log(this.value);
  return {
    value: this.value,
    name: name,
    age: age,
  };
}
console.log(test.myCall(obj, 'hzq', 18));
// 1
// {
//   "value": 1,
//   "name": "hzq",
//   "age": 18
// }

apply 实现

我们首先了解了三者的区别,我们发现 applycall 的区别,就在于传入的参数,针对这一区别,我们开始实现 apply,就只需要对 call 的代码进行部分修改即可。

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
Function.prototype.myApply = function (context, arr) {
  if (typeof this !== 'function') {
    console.log('只有函数可以调用 myApply');
    return;
  }
  context = context || window;
  context.fn = this;
  let result = null;
  if (!arr) {
    // 数组没有值时,直接调用
    result = context.fn();
  } else {
    // 数组有值,传入参数
    result = context.fn(...arr);
  }
  delete context.fn;
  return result;
};

测试与 call 类似,只是修改了传入参数为数组, 打印结果相同。

console.log(test.myApply(obj, ['hzq', 18]));