JavaScript 自己实现 call、apply、bind

简介: JavaScript 自己实现 call、apply、bind

call

Try it

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

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

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

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

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

语法

function.call(thisArg, arg1, arg2, ...)

参数

  • thisArg

    可选,在 function 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

  • arg1, arg2, ...

    可选,指定要传递给函数的参数。

返回值

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

描述

call() 允许为不同的对象分配和调用属于一个对象的函数/方法。

call() 提供新的 this 值给当前调用的函数/方法。可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。

示例

使用 call 方法调用父构造函数

在一个子构造函数中,可以通过调用父构造函数的 call 方法来实现继承,类似于 Java 中的写法。

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

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

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

var cheese = new Food('cheese', 5);
var fun = new Toy('robot', 10);

使用 call 方法调用匿名函数

var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function (i) {
    this.print = function () {
      console.log('#' + i + ' ' + this.species + ': ' + this.name);
    };
    this.print();
  }).call(animals[i], i);
}

使用 call 方法调用函数并且指定上下文的 'this'

function greet () {
  var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}

var obj = {
  animal: 'cats',
  sleepDuration: '12 and 16 hours'
};
greet.call(obj); // cats typically sleep between 12 and 16 hours

使用 call 方法调用函数并且不指定第一个参数(argument

var sData = 'Wisen';

function display () {
  console.log('sData value is', this.sData);
}

display.call(sData); // sData value is Wisen

:::warning
在严格模式下,this 的值将会是 undefined
:::

'use strict';

var sData = 'Wisen';

function display() {
  console.log('sData value is %s ', this.sData);
}
display.call(); // TypeError: Cannot read properties of undefined (reading 'sData')

实现

思路

  1. 基础思路

    1. 将函数设置为对象的属性
    2. 执行该函数
    3. 删除该函数
Function.prototype.myCall = function (context) {
  context.fn = this;
  context.fn();
  delete context.fn;
}

var foo = {
  value: 1
};

function bar() {
  console.log(this.value);
}

bar.myCall(foo); // 1
  1. 处理参数
Function.prototype.myCall = function (context) {
  context.fn = this;
  var args = [];
  // 不使用 join ,因为 join 是 es6 语法,call 是 es3 语法
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
  }
  eval('context.fn(' + args + ')');
  delete context.fn;
} 

var foo = {
  value: 1
};

function bar (name, price) {
  console.log(this.value);
  console.log(name);
  console.log(price);
}

bar.myCall(foo, 'Wisen', 10); // 1 Wisen 10
  1. 其他边界情况
Function.prototype.myCall = function (context) {
  var context = context || window;
  context.fn = this;

  var args = [];
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
  }
  var result = eval('context.fn(' + args + ')');

  delete context.fn;

  return result;
}

var value = 2;

var obj = {
  value: 1
};

function bar (name, age) {
  console.log(this.value);
  return {
    name: name,
    age: age
  };
}

bar.myCall(null); // 2

console.log(bar.myCall(obj, 'Wisen', 10));
// 1
// { name: 'Wisen', age: 10 }

apply

Try it

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

::: tip
call() 方法的作用和 apply() 方法类似,区别就是 call() 方法接受的是参数列表,而 apply() 方法接受的是一个参数数组
:::

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

语法

func.apply(thisArg, [argsArray])

参数

thisArg
必选的。在 func 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

argsArray

可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数。从 ECMAScript 5 开始可以使用类数组对象。

返回值

调用有指定 this 值和参数的函数的结果。

描述

在调用一个存在的函数时,可以为其指定一个 this 对象。 this 指当前对象,也就是正在调用这个函数的对象。 使用 apply, 可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。

applycall() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或数组对象, 如 fun.apply(this, new Array('eat', 'bananas'))

也可以使用 arguments 对象作为 argsArray 参数。 arguments 是一个函数的局部变量。 它可以被用作被调用对象的所有未指定的参数。 这样,在使用 apply 函数的时候就不需要知道被调用对象的所有参数。可以使用 arguments 来把所有的参数传递给被调用对象。 被调用对象接下来就负责处理这些参数。

示例

apply 将数组各项添加到另一个数组

// concat 符合需求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组
// 由于 push 接受可变数量的参数,所以也可以一次追加多个元素。
// 如果 push 的参数是数组,它会将该数组作为单个元素添加,而不是将这个数组内的每个元素添加进去

var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements);
console.log(array); // ['a', 'b', 0, 1, 2]

使用 apply 和内置函数

对于一些需要写循环以遍历数组各项的需求,可以用 apply 完成以避免循环。

/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7];

/* 使用 Math.min/Math.max 以及 apply 函数时的代码 */
var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

::: warning
如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。一个方法传入过多参数(比如一万个)时的后果在不同 JavaScript 引擎中表现不同。
:::

如果参数数组非常大,可以使用混合策略:将数组切块处理

function minOfArray (arr) {
  var min = Infinity;
  var QUANTUM = 32768;

  for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
    var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
    min = Math.min(submin, min);
  }
  return min;
}

var min = minOfArray([5, 6, 2, 3, 7]); // 2

实现

Function.prototype.myApply = function (context, args) {
  var context = context || window;
  context.fn = this;
  var result;
  if (!args) {
    result = context.fn();
  } else {
    var args_arr = [];
    for (var i = 0, len = args.length; i < len; i++) {
      args_arr.push('arguments[' + i + ']');
    }
    result = eval('context.fn(' + args_arr + ')');
  }
  delete context.fn;
  return result;
}

bind

Try it

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

const obj = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = obj.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(obj);
console.log(boundGetX()); // The function gets invoked with the this bound to obj
// expected output: 42

语法

function.bind(thisArg[, arg1[, arg2[, ...]]])

参数

thisArg

调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用 new 运算符构造绑定函数,则忽略该值。当使用 bindsetTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者 thisArgnullundefined,执行作用域的 this 将被视为新函数的 thisArg

arg1, arg2, ...

当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值

返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

描述

bind() 函数会创建一个新的绑定函数bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数

绑定函数具有以下内部属性:

  • [[BoundTargetFunction]] - 包装的函数对象
  • [[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
  • [[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
  • [[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。

当调用绑定函数时,它调用 [[BoundTargetFunction]] 上的内部方法 [[Call]],就像这样 Call(boundThis, args)。其中,boundThis[[BoundThis]]args[[BoundArguments]] 加上通过函数调用传入的参数列表。

绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

示例

创建绑定函数

bind() 最简单的用法是创建一个函数,不论怎么调用,这个函数都有同样的 this 值。

this.x = 9;
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 81

var retrieveX = module.getX;
retrieveX(); // 9

// 创建一个新函数, 把 this 绑定到 module 对象上
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81

偏函数

使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。

function list() {
  return Array.prototype.slice.call(arguments);
}

function addArguments (arg1, arg2) {
  return arg1 + arg2;
}

var list_1 = list(1, 2, 3); // [1, 2, 3]

var result_1 = addArguments(1, 2); // 3

// 创建一个函数, 它拥有预设参数列表
var leadingThirtysevenList = list.bind(null, 37);

// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var list_2 = leadingThirtysevenList(); // [37]
var list_3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]

var result_2 = addArguments(5); // 37 + 5 = 42
var result_3 = addThirtySeven(5, 10); // 37 + 5 = 42 , 第二个参数会被忽略 

配合 setTimeout

function LaterBloomer () {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

LaterBloomer.prototype.bloom = function () {
  window.setTimeout(this.declare.bind(this), 1000);
};

LaterBloomer.prototype.declare = function () {
  console.log('I am a beautiful flower with ' +
              this.petalCount + ' petals!');
};

var flower = new LaterBloomer();
flower.bloom();

实现

  • 特点

    • 返回一个函数
    • 可以传入参数
  1. 返回函数模拟
Function.prototype.myBind = function (context) {
  var self = this;
  return function () {
    self.apply(context);
  }
}

var foo = {
  value: 1
};

function bar () {
  console.log(this.value);
}

var bindFoo = bar.myBind(foo);
bindFoo(); // 1
  1. 传入参数的模拟
Function.prototype.myBind = function (context) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);
  return function () {
    var bindArgs = Array.prototype.slice.call(arguments); // 返回函数的参数
    self.apply(context, args.concat(bindArgs)); // 旧的参数 + 新的参数
  };
}

var foo = {
  value: 1
};

function bar (name, age) {
  console.log(this.value, name, age);
}

var bindFoo = bar.myBind(foo, 'moe');
bindFoo(32); // 1, moe, 32
  1. 构造函数效果的模拟实现
// TODO
相关文章
|
25天前
|
JavaScript 前端开发
JS高级—call(),apply(),bind()
【10月更文挑战第17天】call()`、`apply()`和`bind()`是 JavaScript 中非常重要的工具,它们为我们提供了灵活控制函数执行和`this`指向的能力。通过合理运用这些方法,可以实现更复杂的编程逻辑和功能,提升代码的质量和可维护性。你在实际开发中可以根据具体需求,选择合适的方法来满足业务需求,并不断探索它们的更多应用场景。
9 1
|
1月前
|
JavaScript 前端开发
js 中call()和apply()
js 中call()和apply()
27 1
|
2月前
|
自然语言处理 JavaScript 前端开发
JS中this的应用场景,再了解下apply、call和bind!
该文章深入探讨了JavaScript中`this`关键字的多种应用场景,并详细解释了`apply`、`call`和`bind`这三个函数方法的使用技巧和差异。
|
2月前
|
JavaScript 前端开发
this指向的几种情况以及js简单实现call、apply、bind___六卿
本文讨论了JavaScript中`this`的指向规则,并提供了`call`、`apply`和`bind`方法的简单实现,用于改变函数的`this`指向。
17 0
this指向的几种情况以及js简单实现call、apply、bind___六卿
|
6月前
|
JavaScript 前端开发
javascript中的call和apply
javascript中的call和apply
|
6月前
|
JavaScript 前端开发
JavaScript中call()与apply()的作用与区别?
JavaScript中call()与apply()的作用与区别?
|
3月前
|
前端开发 JavaScript 开发者
揭秘JavaScript魔法三剑客:call、apply、bind,解锁函数新世界,你的前端之路因它们而精彩!
【8月更文挑战第23天】在 JavaScript 的世界里,`call`、`apply` 和 `bind` 这三个方法常常让新手感到困惑。它们都能改变函数执行时的上下文(即 `this` 的指向),但各有特点:`call` 接受一系列参数并直接调用函数;`apply` 则接收一个参数数组,在处理不确定数量的参数时特别有用;而 `bind` 不会立即执行函数,而是创建一个新版本的函数,其 `this` 上下文已被永久绑定。理解这三个方法能帮助开发者更好地运用函数式编程技巧,提升代码灵活性和可维护性。
37 0
|
2月前
|
JavaScript
js的this与call,apply,bind
js的this与call,apply,bind
|
3月前
|
JavaScript 前端开发
js 中call()和apply()
js 中call()和apply()
38 0
|
4月前
|
JavaScript
js【详解】call()、apply()、bind()方法
js【详解】call()、apply()、bind()方法
39 6