手写call、apply和bind函数
手写call、apply和bind函数
前言
提到改变 this 的指向,首先想到的方式就是 call、apply 和 bind。对于每种方式底层是如何实现,大多数人不太清楚,如果你还不清楚他们的用法,请移步call、apply、bind。本文会简单讲解他们的用法,底层实现思路,及模拟实现 call、apply、bind。
call
1、定义: 使用一个指定的 this 值和单独给出一个或多个参数来调用一个函数。
function.call(this, arg1, arg2, arg3, ...)
复制代码
根据定义我们知道,call()方法有两个作用,一个是改变 this 指向,另外一个传递参数,如下:
const obj = {
value: 1,
}
function fn(arg) {
console.log(this.value, arg); // 1, 2
}
fn.call(obj, 2);
复制代码
上面的例子,使用 call()方法使函数fn
的 this 指向了obj
,所以 this.value 的值为 1。那么如果不使用 call()方法,该如何实现呢?
2、call 实现思路
不考虑使用 call、apply、bind 方法,上面例子 fn 函数如何能拿到 obj 里面的 value 值呢?改造一下上面的例子
const obj = {
value: 1,
fn: function() {
console.log(this.value); // 1
}
}
obj.fn();
复制代码
这样一改,this 就指向了 obj,根据这个思路,可以封装一个方法,将传入的 this,转换成这样的方式,那么当前 this 的指向就是我们想要的结果。需要注意fn函数不能写成箭头函数,因为箭头函数没有this
。所以模拟的步骤为:
- 将函数设置为传入对象的属性;
- 执行该函数;
- 删除该属性; 上面的例子就可以改写为:
// 给obj添加属性func
obj.func = fn;
// 执行函数
obj.func();
// 删除添加的属性
delete obj.func;
复制代码
3、模拟 call 方法
根据上面的思路,来模拟实现一版 call()方法。
Function.prototype.call1 = function (context) {
context.func = this;
context.func();
delete context.func;
}
复制代码
实现了简化版 call 方法,来试验下是否能正确改变 this。
const obj = {
value: 1,
}
function fn(arg) {
console.log(this.value, arg); // 1, undefined
}
fn.call1(obj, 2);
复制代码
根据上面例子,已经能正确改变 this 的指向,但是传入的值却没有拿到,该怎么办呢?考虑的传入的值是不确定的,只能借助Arguments 对象。通过它可以拿到所有传入的参数。
4、模拟 call 方法第二版
上面提到可以通过 arguments 解决传入参数不定长问题,如下:
const res = [];
// 因为第一个参数是传入的this,故这里从i = 1开始遍历
for (let i = 1; i < arguments.length; i++) {
res.push(arguments[i]);
}
// 或者
const args = [...arguments].splice(0, 1);
复制代码
这样就能拿到所有的参数,接下来我们是不是将拿到的参数放到函数里面执行就可以了吗?先试一下:
Function.prototype.call1 = function(context) {
// 初始化存储函数参数
const res = [];
// 改变当前函数的this指向
context.fn = this;
for (let i = 1; i < arguments.length; i++) {
res.push(arguments[i]);
}
context.fn(res.join(','));
delete context.fn;
}
const obj = {
value: 1,
}
function fn(val1, val2) {
console.log(this.value, ...arguments); // 1 "1,2,200,[object Object]"
}
fn.call1(obj, [1,2], 200, {a: 1});
复制代码
结果和我们期望的不一致,函数的参数被转换成了一个字符串,那么怎么出来才能达到想要的结果呢?这儿可以考虑两种方式处理
第一种方式 es6 的“...”操作符,如下:
Function.prototype.call1 = function(context) {
// 初始化存储函数参数
const res = [];
// 改变当前函数的this指向
context.fn = this;
for (let i = 1; i < arguments.length; i++) {
res.push(arguments[i]);
}
context.fn(...res);
delete context.fn;
}
const obj = {
value: 1,
}
function fn(val1, val2) {
console.log(this.value, ...arguments); // 1 [1, 2] 200 {a: 1}
}
fn.call1(obj, [1,2], 200, {a: 1});
复制代码
第二种方式,借助eval方法,eval 会将传入的字符串当做 JavaScript 代码执行。那么我们可以考虑将要执行的函数,拼装成字符串,然后通过 eval 执行即可。思路如下:
Function.prototype.call1 = function(context) {
// 初始化,用于存放参数
const args = [];
// 在传入的对象上设置属性为待执行函数
context.fn = this;
for (let i = 1; i < arguments.length; i++) {
// 将参数从第二个开始,拼装成待执行的字符串参数列表
args.push(`arguments[${i}]`);
}
eval(`context.fn(${args})`);
delete context.fn;
}
const obj = {
value: 1,
};
function fn(val1, val2) {
console.log(this.value, ...arguments); // 1 [1, 2] 200 {a: 1}
}
fn.call1(obj, [1, 2], 200, { a: 1 });
复制代码
到这里,通过模拟实现 call 方法,已经能实现改变 this,传入参数,是不是就完了呢?可能会有这样一种情况,如果函数本身会有返回值,还是用吗?如下:
const obj = {
value: 1,
};
function fn() {
const args = [...arguments];
console.log(this.value, ...args); // 1 [1, 2] 200 {a: 1}
return {
c: args,
};
}
console.log(fn.call1(obj, [1, 2], 200, { a: 1 })); // undefined
复制代码
此时,执行函数的返回值为undefined
,解决这个问题很好办,在封装的 call 方法里面,将执行的函数结果存下来,return 出来即可,如下:
Function.prototype.call1 = function (context) {
// 初始化,用于存放参数
const args = [];
// 在传入的对象上设置属性为待执行函数
context.fn = this;
for (let i = 1; i < arguments.length; i++) {
// 将参数从第二个开始,拼装成待执行的字符串参数列表
args.push(`arguments[${i}]`);
}
eval(`context.fn(${args})`);
delete context.fn;
};
const obj = {
value: 1,
};
function fn() {
const args = [...arguments];
console.log(this.value, ...args); // 1 [1, 2] 200 {a: 1}
return {
c: args[0],
};
}
console.log(fn.call1(obj, [1, 2], 200, { a: 1 })); // {c: [1, 2]}
复制代码
模拟 call 终版
上面的方式都是使用 eval 来实现 call 方法,es6 提供了很多语法糖,个人比较喜欢 es6 的实现方式,比较简洁,故终版使用的 es6 的实现方式。
Function.prototype.call1 = function () {
// 初始化,获取传入的this对象和后续所有参数
const [context, ...args] = [...arguments];
// 在传入的对象上设置属性为待执行函数
context.fn = this;
// 执行函数
const res = context.fn(args);
// 删除属性
delete context.fn;
// 返回执行结果
return res;
};
复制代码
到此,模拟实现了 call 方法。
模拟 apply 方法
定义: 调用一个具有给定 this 值的函数,及以一个数组的形式提供的参数。
func.apply(thisArg, [argsArray]);
复制代码
从定义上知道,apply 相比于 call 方法,区别在与 this 后面的参数,call 后面的有一个或多个参数,而 apply 只有两个参数,第二个参数是一个数组,如下:
const obj = {
value: 1,
};
function fn() {
console.log(this.value); // 1
return [...arguments]
}
console.log(fn.apply(obj, [1, 2])); // [1, 2]
复制代码
思路
apply 的实现思路和 call 一样,需要考虑的是 apply 只有两个参数,因此,根据 call 的思路实现如下:
Function.prototype.apply1 = function (context, args) {
// 给传入的对象添加属性,值为当前函数
context.fn = this;
// 判断第二个参数是否存在,不存在直接执行,否则拼接参数执行,并存储函数执行结果
let res = !args ? context.fn() : context.fn(...args)
// 删除新增属性
delete context.fn;
// 返回函数执行结果
return res;
}
const obj = {
value: 1,
};
function fn() {
console.log(this.value); // 1
return [...arguments]
}
console.log(fn.apply(obj, [1, 2])); // [1, 2]
复制代码
模拟 bind
定义:创建一个新的函数,在 bind 被调用时,这个新函数的 this 被指定为 bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
function.bind(thisArg[, arg1[, arg2[, ...]]])
复制代码
bind 相比于 call、apply 有较大的区别,bind 方法会创建一个新的函数,返回一个函数,并允许传入参数。首先看一个例子。
const obj = {
value: 1,
fn: function () {
return this.value;
},
};
const func = obj.fn;
console.log(func()); // undefined
复制代码
为什么值是 undefined 呢?这会涉及到 this 的问题,不清楚的可以看这里,简单来讲,函数的调用决定 this 的值,即运行时绑定。这里声明了 func 用于存放 obj.fn,再执行 func()方法时,当前的 this 指向的是 window 是,故值为 undefined。改如何处理才能达到预期的值呢?这时 bind 即将登场。
const obj = {
value: 1,
fn: function () {
return this.value;
},
};
const func = obj.fn;
const bindFunc = func.bind(obj);
console.log(bindFunc()); // 1
复制代码
模拟 bind 第一版
首先解决 bind 的第一个问题,返回一个函数,可通过闭包的模式实现,改变 this 指向问题,可以使用 call 和 apply 方法,可参照上面的实现方式。
Function.prototype.bind1 = function (context) {
// 将当前函数的this存放起来
const _self = this;
return function () {
// 改变this
return _self.apply(context);
};
};
复制代码
this 改变了后,需要考虑第二个问题,传参,bind 的参数,从第二个到第 n 个函数,存在参数不定的情况,结合上面 call 的实现方式,解决这个问题如下:
// 方法1, 从第二个参数开始
const args = Array.prototype.slice.call(arguments, 1);
// 方法2
const [context, ...args] = [...arguments];
复制代码
参数取到后,将参数传入即可,如下:
Function.prototype.bind1 = function (context) {
// 将当前函数的this存放起来
const _self = this;
// 绑定bind传入的参数,从第二个开始
const args = Array.prototype.slice.call(arguments, 1);
return function () {
// 绑定bind返回新的函数,执行所带的参数
const bindArgs = Array.prototype.slice.apply(arguments);
// 改变this
return _self.apply(context, [...args, ...bindArgs]);
};
};
const obj = {
value: 1,
fn: function () {
return {
value: this.value,
args: [...arguments],
};
},
};
const func = obj.fn;
const bindFunc = func.bind1(obj, 1, 2);
console.log(bindFunc(3)); // { value: 1, args: [1, 2, 3]}
复制代码
到这里,bind 的模拟已经完成一半,为什么说完成一半呢?功能已经实现,考虑到有这样一种情况,将绑定的 bind 返回的新函数作为构造函数使用,使用new
操作符去创建一个由目标函数创建的新实例。当绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。什么意思呢?先看下面例子:
var value = 1;
var obj = {
value: 100,
};
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this.value); // undefined
console.log(name); // jack
console.log(age); // 35
}
var bindPerson = Person.bind1(obj, "jack");
var bp = new bindPerson(35);
复制代码
从上面的结果可以看出,尽管已经在全局和 obj 上定义了 value 值,但是构造函数 Person 中拿到的 this.value 仍然是 undefined 值,说明 this 的绑定失效了,为什么会出现这样的情况呢? 出现这样的情况是因为关键字new
造成的,当程序遇到 new 会进行如下的操作:
- 创建一个空的简单的 JavaScript 对象(如{});
- 设置该对象的 constructor 到另外一个对象;
- 将步骤 1 创建的对象作为 this 的上下文;
- 如果函数没有返回对象则返回 this; 这样就明白为什么 this.value 的值为 undefined 了,当前的 this 指向的是 bp,bp 上并没有 value 属性,所以为 undefined。
模拟 bind 终版
Function.prototype.bind1 = function (context) {
// 将当前函数的this存放起来
var _self = this;
// 绑定bind传入的参数,从第二个开始
var args = Array.prototype.slice.call(arguments, 1);
// 声明一个空的构造函数
function fNOP() {}
var fBound = function () {
// 绑定bind返回新的函数,执行所带的参数
const bindArgs = Array.prototype.slice.apply(arguments);
// 合并数组
args.push.apply(args, bindArgs);
// 作为普通函数,this指向Window
// 作为构造函数,this指向实例
return _self.apply(this instanceof fNOP ? this : context, args);
};
if (this.prototype) {
// 修改返回函数的prototype为绑定函数的prototype,实例就可以继承绑定函数的原型中的值
fNOP.prototype = this.prototype;
}
// FNOP继承fBound
fBound.prototype = new fNOP();
return fBound;
};
复制代码
在这个方法里面用到了原型与原型链、继承等知识,不清楚的可以转到到《JavaScript 之深入原型与原型链》、《JavaScript 之深入多种继承方式及优缺点》。 到这里,还需要思考调用 bind 不是函数怎么办?报个错就好了
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
复制代码
这里模拟的 bind 函数不是最终版,在 CDN 上有bind 实现;
参考文献
[0]《Function.prototype.call()》
[1]《Function.prototype.bind()》