手写题是前端面试中经常遇到的,可以考察面试者的基础能力。
call
,apply
和 bind
是 Function.prototype
上的三个方法。这三个方法的基本用法介绍,我们在上一篇“聊聊 call、apply 和 bind”中已经进行了介绍。
如果在前端面试中,如果面试官要求手写这三个方法的模拟实现,小伙伴们是否能顺利写出来呢?今天我们就来看看这几个方法如何模拟实现。
call()
方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
在开始实现call
函数之前,先要思考如何修改this
的指向,其实很简单,直接把目标函数挂载到指定的上下文中。
Function.prototype.call = function (context) {
context = context ? Object(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;
}
有同学可能会对 'context.fn(' + args +')'
这段感到奇怪,其实里面有个js的隐式转换的知识点。
比如'(' + [1,2,3] + ')'
会输出什么?答案是 (1,2,3)
。
上述代码中,为了执行函数,使用了eval
。eval
会将传入的字符串当做 JavaScript
代码进行执行。
console.log(eval('2 + 2'));
// expected output: 4
console.log(eval(new String('2 + 2')));
// expected output: 2 + 2
console.log(eval('2 + 2') === eval('4'));
// expected output: true
eval()
是一个危险的函数,不建议大家使用,这儿也是给大家举个例子。
当然es6
的扩展运算符也可以实现相同的功能。
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
let args = [...arguments].slice(1);
let result = context.fn(...args);
delete context.fn
return result;
}
还是 ES6
版本的模拟实现看上去更加清爽。
apply()
方法调用一个具有给定 this
值的函数,以及作为一个数组(或类似数组对象)提供的参数。
apply
和 call
的作用是一样的,区别是 apply
是将参数作为一个数组传入,而 call
是将参数一个一个的传入。
所以就直接上代码:
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
var result;
// 判断是否存在第二个参数
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 + ')');
}
delete context.fn
return result;
}
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn
return result;
}
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
实现 bind
主要有三个要点:
返回函数和预设参数我们可以用 apply 来实现,大致的效果如下。
Function.prototype._bind = function (thisArg) {
let self = this;
let args = Array.prototype.slice.call(arguments, 1);
let fBound = function () {
let bindArgs = Array.prototype.slice.call(arguments);
return self.apply(thisArg, args.concat(bindArgs));
}
return fBound;
}
function sum(c, d) {
return this.a + this.b + c + d;
}
let obj = {a: 1, b: 2}
let t = sum._bind(obj, 3);
console.log(t(4)) //10
下面就是要实现 new
调用。
当使用 new
调用绑定函数,this
将指向绑定函数的原型,我们要的效果是原型指向的是原函数的 prototype
,那么最直接的想法就是将绑定函数的 prototype
指向原函数的 prototype
即可。
但是这样做有一个问题就是当我们后面改变绑定函数的 prototype
,原函数的 prototype
也会被修改,他们指向的是同一个对象。基于这样的原因我们需要在中间加一层。最终的实现如下:
Function.prototype._bind = function (thisArg) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
let self = this;
let args = Array.prototype.slice.call(arguments, 1);
let fNOP = function () {};
let fBound = function () {
let bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : thisArg, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
function sum(c, d) {
console.log(this.a, this.b) //undefined undefined
this.a = c;
this.b = d;
}
let obj = {a: 1, b: 2}
let t = sum._bind(obj, 3);
let m = new t(4, 5)
console.log(m) //{3, 4}
我们可以看到最后结果使用的参数是 bind
的时添加的一个参数和 new
添加的第一个参数,new
的多余参数被忽略。这也是 bind
的另一个功能,可以预设参数。
而我们也发现 bind
绑定的 obj
没有生效,这部分我们是用 instanceof
判断调用绑定函数时的 this
来判断的,如果是 new
调用,那么这个 this
是 fNOP
的实例(如果是直接调用,那么这个 this
会是全局对象,浏览器环境就是 window
对象)。
已经到了6月中旬,大厂的秋招提前批,还有半个月就要开始了。祝应届的同学们都能在秋招中,收获让自己满意的offer。
这儿也打个广告,《前端面试题宝典》经过近一年的迭代,现已推出 小程序
和 电脑版刷题网站 (https://fe.ecool.fun/
),欢迎大家使用~
同时,我们还推出了面试辅导的增值服务,可以为大家提供 “简历指导” 和 “模拟面试” 服务,感兴趣的同学可以联系小助手(微信号:interview-fe)进行报名。
最后,给大家留一个思考题:不使用 call
和 apply
,怎么模拟实现 bind
?