JS 手写: call、apply、bind

2023-05-17 15:07:22 浏览数 (2)

# call

# Try it

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

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

代码语言:javascript复制
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 可能不是该方法看到的实际值:如果这个函数处于非严格模式 (opens new window)下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。
  • arg1, arg2, ... 可选,指定要传递给函数的参数。
# 返回值

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

# 描述

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

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

# 示例

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

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

代码语言:javascript复制
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 方法调用匿名函数
代码语言:javascript复制
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'
代码语言:javascript复制
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
代码语言:javascript复制
var sData = 'Wisen';

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

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

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

代码语言:javascript复制
'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. 删除该函数
代码语言:javascript复制
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. 处理参数
代码语言:javascript复制
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. 其他边界情况
代码语言:javascript复制
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 值的函数,以及以一个数组(或类数组对象 (opens new window))的形式提供的参数。

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

代码语言:javascript复制
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 将数组各项添加到另一个数组
代码语言:javascript复制
// 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 完成以避免循环。

代码语言:javascript复制
/* 找出数组中最大/小的数字 */
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);

如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。一个方法传入过多参数(比如一万个)时的后果在不同 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

# 实现

代码语言:javascript复制
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() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

代码语言:javascript复制
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 值。

代码语言:javascript复制
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 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。

代码语言:javascript复制
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
代码语言:javascript复制
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. 返回函数模拟
代码语言:javascript复制
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. 传入参数的模拟
代码语言:javascript复制
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. 构造函数效果的模拟实现
代码语言:javascript复制
// TODO

0 人点赞