call、apply、bind 的实现

记录 call、apply、bind 的模拟实现

call#

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

note

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

Function.prototype.myCall = function (context) {
// 第一步:获取上下文及函数
var context = Object(context) || window;
context.fn = this; // 此处 this 是指调用 myCall 的 function
// 第二步:获取参数
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push('arguments[' + i + ']');
}
// 第三步:执行函数 fn
var result = eval('context.fn(' + args +')'); // eval() 函数会将传入的字符串当做 JavaScript 代码进行执行。
// 第四步:删除函数 fn,并返回执行结果
delete context.fn;
return result;
}

apply#

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

note

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

Function.prototype.myApply = function (context, arr) {
// 第一步:获取上下文及函数
var context = Object(context) || window;
context.fn = this;
var result;
// 第二步:获取参数 并 执行函数 fn
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')');
}
// 第四步:删除函数 fn,并返回执行结果
delete context.fn;
return result;
}

bind#

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

以下内容摘自 冴羽:JavaScript深入之bind的模拟实现

第一版#

// 第一版
Function.prototype.myBind = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}

第二版#

一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子:

var value = 2;
var foo = {
value: 1;
}
function bar() {
console.log(this.value);
}
var bindFoo = bar.bind(foo);
var obj = new bindFoo();
// undefined

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,结合 new 的原理可知此时 this 指向了 obj。

// 第二版
Function.prototype.myBind = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}

第三版#

避免修改 fBound.prototype 的时候,修改到绑定函数的 prototype,通过临时函数进行中转

// 第三版
Function.prototype.myBind = function() {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var context = [].shift.call(arguments); // 需要绑定的 this 上下文
var args = [].slice.call(arguments); // 剩下参数转为数组
var fNOP = function () {};
var fBound = function () {
var bindArgs = [].slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};

总结#

  • 当使用一个函数需要改变 this 指向的时候才会用到 call,apply,bind

  • 如果传递的参数不多,则可以使用 fn.call(thisObj, arg1, arg2 ...)

  • 如果传递的参数很多,则可以用数组将参数整理好调用 fn.apply(thisObj, [arg1, arg2 ...])

  • 如果需要生成一个新的函数长期绑定某个函数给某个对象使用,则可以使用

    const newFn = fn.bind(thisObj);
    newFn(arg1, arg2...)