# 👉 call、apply、bind 的实现
# 三者的联系与区别
三者都会改变函数执行的上下文,即改变函数运行时的 this 指向。
三者的第一个参数都是改变上下文所指向的对象。当传 null/undefined 会自动指向全局对象 window,传原始值(数字,字符等)会指向它们的自动包装对象。
call(Fun, arg1, arg2..)
和apply(Fun, [..args])
的区别在于:
call 方法接受的是若干个参数的列表,而 apply 方法接受的是一个包含多个参数的数组。bind(Fun, param1, param2, ...)
方法与 call/apply 最大的不同是:
前者返回的是一个绑定上下文的函数,需要手动调用,而后两者是直接执行了函数。
# call/apply 的模拟实现
call、apply 和 bind 是挂在 Function 对象上的三个方法,只有函数才有这些方法。
call/apply 需要做的事情:改变调用函数的 this 指向,执行函数,让指定的对象接受并拥有参数。分步骤来:
- 判断指定对象是否为
null/undefined
,是指向window
; - 获取若干个参数列表(类数组);
- 将调用函数作为指定对象属性 fn 的值,通过此方法改变调用函数重的 this 值;执行 fn 为指定对象增加属性方法,并删除该属性。
(利用原理:函数作为一个对象的属性,被这个对象自身调用时,this 指向当前对象。)
这里也主要参考了大神冴羽 (opens new window)的文章:
- call 的模拟实现
Function.prototype._call = function(ctx, ...arguments) {
// ctx为null/undefined时,this将指向全局对象window
ctx = ctx || window;
ctx.fn = this;
const result = ctx.fn(...arguments);
// 若入参只有一个ctx, 则需分离开 指定this对象 和 参数列表
// 获取传入的额外参数 var args = [arguments[1], arguments[2], ...]
// var args = [];
// for (var i = 1; i < ctx.length; i++) {
// args.push("arguments[" + i + "]");
// }
// var result = eval("ctx.fn(" + args + ")");
delete ctx.fn;
return result;
};
function bar(name, age) {
const value = this.value;
return { value, name, age };
}
var value = "window";
const obj = { value: "obj" };
console.log("test1: ", bar._call(null));
//test1: {value: "window", name: undefined, age, undefined}
console.log("test2: ", bar._call(obj, "obj", 1));
//test2: {value: "obj", name: "obj", age: 1}
- apply 的模拟实现
apply 和 call 的模拟实现方式差不多,稍有区别的地方是传参的方式为一个包含了多个参数数组。
Function.prototype._apply = function(ctx, argArr) {
ctx = ctx || window;
ctx.fn = this;
let result;
if (!argArr || !argArr.length) {
result = context.fn();
} else {
result = context.fn(...argArr);
}
delete ctx.fn;
return result;
};
function bar(name, age) {
const value = this.value;
return { value, name, age };
}
var value = "window";
const obj = { value: "obj" };
console.log("test1: ", bar._apply(null));
//test1: {value: "window", name: undefined, age, undefined}
console.log("test2: ", bar._apply(obj, ["obj", 1]));
//test2: {value: "obj", name: "obj", age: 1}
# bind 的模拟实现
bind 的模拟实现会较前两者复杂,bind 的特点:
- 支持若干个参数,也支持分开传参数;
- 会返回一个新的函数(形成闭包)。
先来看看原生 bind 的用法:
// 直接调用
function bar(name, age) {
return {
value: this.value,
name: name,
age: age,
};
}
var value = "window";
const foo = {
value: "foo",
};
const barFoo = bar.bind(foo, "jack");
console.log(barFoo(30));
// { value: "foo", name: "jack", age: 30 }
对于以上的特点,来个 bind 的简易版实现:
Function.prototype._bind = function(ctx, ...bindArgs) {
ctx = ctx || window;
const fn = this;
return function(...args) {
return fn.apply(ctx, bindArgs.concat(args));
};
};
以为这就结束了?还没还没......
需特别注意的是:
对于 bind 返回的函数有两种调用方式,直接调用和通过 new 的方式。
来自于 MDN 关于 bind 的原话
绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。
原话直白翻译就是我们可以把 bind 返回的函数当做构造函数去用 new 操作符创建实例,但这个时候 bind 传入的 this 指向会失效(因为按照 new 原理来说 this 会被指向新的实例对象),但是传入的参数还是有效的。
// new方式
function Student(name, age) {
this.habit = "running";
this.name = name;
this.age = age;
console.log({ grade: this.grade, name, age });
}
Student.prototype.job = "study";
var grade = "window";
var grade1 = { grade: "grade1" };
var grade1Student = Student.bind(grade1);
var student1 = grade1Student("jack", 11);
// {grade: "grade1", name: "jack", age: 11}
console.log(student1.job); //Cannot read property 'job' of undefined
var newStudent = new grade1Student("chichi", 10);
// {grade: undefined, name: "chichi", age: 10}
console.log(newStudent);
// Student {habit: "running", name: "chichi", age: 10}
// 对bind返回的函数,直接调用:student1可以获取到 grade 值。
// 对bind返回的函数,new式调用:返回了 undefind。说明绑定的this会因为new时重新被指向新实例而失效。
console.log(
newStudent.name,
newStudent.habit,
newStudent.job,
newStudent.grade
);
//chichi running study undefined
接着,继续完善,bind 的第二版模拟实现:
Function.prototype._bind = function(ctx, ...bindArgs) {
// 获取指定的上下文对象,即新的this指向
ctx = ctx || window;
// 获取调用函数上下文内容
const fn = this;
// 重新绑定指定上下文对象的函数
const fBind = function(...args) {
// 1. 当_bind返回的函数,通过new调用时(fBind作为构造器),this 会被指向实例对象
// 存在关系:实例 instanceof fBind,此时实例对象还能获取到绑定至self的值和原型链上的值
// 2. 当_bind返回的函数,被直接调用时,this指向ctx/window
if (this instanceof fBind) {
return fn.apply(this, bindArgs.concat(args));
} else {
return fn.apply(ctx, bindArgs.concat(args));
}
};
// 重新绑定指定上下文对象的函数,需继承调用函数的原型链
fBind.prototype = Object.create(fn.prototype);
// 不能直接 fBind.prototype = this.prototype,否则当修改fBind.prototype属性值时,也会影响fn.prototype属性
return fBind;
};
# 三者的应用场景
- 数组最大/小值 Math.max.apply(Math, array)
- 数组合并 [].push.apply(arr1, arr2)
- 将类数组转成数组 Array.prototype.slice.call(arguments)