在实际工作中,这三个函数在我们代码中是很活跃的,比如经常使用Object.prototype.toString.call()来精确判断数据类型;今天我们就尝试去探究如果让我们自己动手去实现。
call的模拟实现
在模拟实现之前我们首先需要知道call做了什么?
- 改变了函数的this指向
- 第一个参数是指定的this,后面的参数是函数运行时的参数
- 执行函数
那么接下来我们一步步的进行实现
第一步改变函数的this
看下面这个例子:
function boo() {
console.log(this.name)
}
var foo = {
name: 'foo',
boo: boo
}
foo.boo() // 输出结果为foo
从这个例子中我们得出改变this的思路:将函数设置成第一个参数的属性;然后在call内部执行。话不多说按照这个思路进行实现:
function call1(context) {
context.fn = this; // 在这里this就是call前面的函数
let result = context.fn(); // 通过context执行函数
delete context.fn; // 执行完成之后为了不改变原有变量的属性,将添加的函数删除
return result;
}
// 将call1挂载到Function的原型上
Function.prototype.call1 = call1
第二步可以接收参数
call函数从第二个参数开始都是函数运行时的实参,那接下来在第一步的基础上进行改进
function call2(context) {
context = context || window; // 第一个参数有可能为null
context.fn = this;
var arg = []; // 用来保存参数
var result = ''; // 用来保存函数运行的返回值
// ES6的写法
for (var i = 1; i < arguments; i++) {
arg.push(arguments[i]);
}
result = context.fn(...arg)
// ES6之前的写法
for (var i = 1; i < arguments; i++) {
arg.push('arguments[' + i + ']');
}
result = eval('context.fn(' + arg + ')');
delete context.fn;
return result;
}
Function.prototype.call2 = call2;
至此call的模拟已经完成
apply模拟实现
apply和call的作用是一样的,它们之间的区别是传参数方式不一样,apply只有两个参数,第一个参数是指定的this,第二个参数是数组,包含了函数执行所需的参数。下面我们来进行模拟:
function apply1(context, arr) {
context = context || window;
var result = '';
context.fn = this;
if (arr) {
result = context.fn(...arr);
} else {
result = context.fn();
}
delete context.fn;
return result;
}
bind模拟实现
按照顺序首先介绍一下bind做了哪些事:
- 返回一个函数
- 该函数运行时的this就是bind方法的第一个参数
- 可以给函数传参
第一步:返回函数和this的绑定
Function.prototype.bind1 = function(context) {
var _self = this;
return function () {
return _self.apply(context)
}
}
第二步:实现参数的传递
Function.prototype.bind2 = function(context) {
var _self = this;
var arg = Array.prototype.slice.call(arguments, 1); // 获取bind函数的后续参数
return function () {
return _self.apply(context, arg);
}
}
上面代码我们已经实现了bind绑定时的参数传递,但是bind方法返回的函数也可以传参数,下面我们就对这个进行一下优化:
Function.prototype.bind2 = function(context) {
var _self = this;
var arg = Array.prototype.slice.call(arguments, 1);// 获取bind函数的后续参数
return function () {
arg = arg.concat([...arguments]);
return _self.apply(context, arg);
}
}
到此基本功能就完成,但是你以为就此结束了吗?那只能说对不起了,bind函数返回的方法还可以作为构造函数,当作为构造函数通过new生成实例时,这时的this就不是bind函数的第一个参数了,那么下面我们开始进行优化:
Function.prototype.bind3 = function (context) {
var _self = this;
var arg = Array.prototype.slice.call(arguments, 1);
function fn () {
arg = arg.concat([...arguments]);
return _self.apply(this instanceof fn ? this : context, arg);
}
fn.prototype = _self.prototype;
return fn;
}
上面代码实现作为构造函数时的情况,但是这种实现有个缺陷,当我们改变bind方法返回函数的原型时会同时把原来函数的原型给改了,下面我们进行优化一下:
Function.prototype.bind3 = function (context) {
var _self = this;
var arg = Array.prototype.slice.call(arguments, 1);
function fn() {
arg = arg.concat([...arguments]);
return _self.apply(this instanceof fn ? this : context, arg);
}
function Trans(){};
Trans.prototype = _self.prototype;
fn.prototype = new Trans();
fn.prototype.constructor = fn;
return fn;
}
现在bind的模拟实现已经完成了。
参考:冴羽的博客