前言:
在js引擎中,内置函数apply、call、bind是由c++实现的。
这里我们通过js来模拟实现,
主要练习函数、this、调用关系,不过度考虑一些边界情况。(edge case
前置知识:了解this的绑定规则(new绑定、显式绑定、隐式绑定、默认绑定)
摸索思路:
1、通过在Function.prototype(所有函数对象的原型对象)上定义新方法的方式让所有函数实例可以访问和调用新方法。
javascript
Function.prototype.hycall = function () {
console.log("调用了");
};
function foo() {
console.log("foo", this);
}
function sum(n1, n2) {
console.log("sum", n1,n2,this);
return n1 + n2;
}
foo.call(); // foo window
console.log(foo.call()); // undefined
foo.call("abc"); // foo String {abc}
console.log(foo.call("abc")); // undefined
foo.call(123); // foo Number {123}
foo.call({ name: "zhangsan" }); // foo {name: "zhangsan"}
foo.call(null); // foo window
foo.call(undefined); // foo window
foo.call(true); // foo Boolean {true}
sum.call(); // sum undefined undefined window
sum.call(123, 1, 2); // sum 1 2 window
console.log(sum.call(123, 1, 2)); // 3
foo.hycall(); // 调用了
sum.hycall(); // 调用了
2、分析内置的call方法
通过代码我们发现call主要做了以下事情
1)执行调用它的函数(示例中的foo和sum函数)
2)可以改变this的指向,并且this最终是指向对象(第一个传入的参数会被绑定this
3)可以传入多个参数
4)最后有一个返回结果,回调函数的返回值或undefined
代码实现:
1、执行调用它的函数
对应第一点的实现,首先我们要拿到这个函数,通过之前对this绑定规则的学习我们可以知道,形如foo.hycall这样的调用方式符合this的隐式绑定规则,this指向foo,因此在hycall中通过this可以拿到我们的函数 (示例中的foo和sum函数,并且进行调用。对我们自己的hycall进行如下改造
javascript
Function.prototype.hycall = function () {
// 拿到函数
var fn = this
// 调用函数
// 独立函数调用,符合this的默认绑定规则,this指向window
fn()
};
foo.hycall(); // foo window
sum.hycall(); // sum window
可以发现,foo.hycall()现在的执行结果和foo.call()一样,都是window
2、改变this的指向,并且this最终是指向对象(第一个传入的参数会被绑定this
对于要改变this的指向,
首先,同样的我们可以利用this的隐式绑定规则,例如xx.fn()这样的调用方式,改变this的指向为xx
javascript
Function.prototype.hycall = function (thisArg) { // 参数任意取名,这里取为thisArg
var fn = this
thisArg.fn = fn
// 隐式调用,this指向thisArg
thisArg.fn()
};
foo.call({name:"aa"}) // foo {name: 'aa'}
foo.hycall({name:"aa"}) // foo {name: 'aa', fn: ƒ}
此时执行foo.hycall({name:"aa"}),我们发现,打印的结果比call多了一个fn属性,因为我们通过thisArg.fn = fn使得thisArg多了一个fn属性。可以使用delete进行完善(浏览器打印可能看起来还有fn属性,显示问题,展开就没了)
javascript
Function.prototype.hycall = function (thisArg) {
var fn = this;
thisArg.fn = fn;
thisArg.fn();
delete thisArg.fn; // 使用call打印的this没有fn这个属性,所以这里删除
};
至此,我们完成了做到了改变this的指向。
还有一点,我们知道,执行foo.call(参数),不论参数传的是null、undefined、字符串、数字、对象、Boolean甚至什么都不出传,最终this打印都是一个对象。this打印出来最终是指向的是一个对象 。
显然目前我们的hycall做不到这一点。
可以使用JavaScript内置的Object,通过Object(thisArg)的方式,将参数转化为对象
javascript
Function.prototype.hycall = function (thisArg) {
var fn = this;
// 对thiArg进行类型转换(防止传入的是非对象类型,如数字、字符串),没传入则设置为window
// thisArg = thisArg ? Object(thisArg) : window; // 这句代码在thisArg传入数字0会有问题
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
thisArg.fn = fn;
thisArg.fn();
delete thisArg.fn;
};
3、可以传入多个参数
例如sum.call("abc", 2, 3),call是可以拿到后面两个参数并且传入sum函数的
现在我们hycall也要做到这一点,涉及到一个es6的知识点,剩余参数
javascript
function test(...args) {
console.log(args);
}
test() // []
test(1, 2, 3); // [1, 2, 3]
test(1, 2, 3, 4, 5, 6, 7, 8, 9); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
...args这样的参数形式,称为剩余参数,函数里可以拿到所有传入的参数组成的一个数组。
那么我们的hycall可以完善为:
javascript
Function.prototype.hycall = function (thisArg, ...args) {
var fn = this;
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
thisArg.fn = fn;
thisArg.fn(...args); // 这里...args的含义是把args里面的参数展开,...是展开运算符
delete thisArg.fn;
};
sum.hycall("abc",3,8) // sum 3 8 String{"abc"}
console.log(sum.hycall("abc",3,8)) // undefined
4、返回结果
我们知道console.log(sum.hycall("abc",3,8))此时打印的结果是undefined,
而console.log(sum.call("abc", 3, 8))的结果是11
因此,我们只要把thisArg.fn(...args)的执行结果返回就行了。
javascript
Function.prototype.hycall = function (thisArg, ...agrs) {
var fn = this;
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
thisArg.fn = fn;
var result = thisArg.fn(...agrs);
return result;
}
完整注释版
javascript
Function.prototype.hycall = function (thisArg, ...agrs) {
// ...agrs是剩余参数
// 1、拿到函数
var fn = this;
// 2、对thiArg进行类型转换(防止传入的是非对象类型,如数字、字符串),没传入则设置为window
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
// 3、将函数作为属性增加到thisArg上,同时通过隐式调用改变this指向
thisArg.fn = fn;
var result = thisArg.fn(...agrs); // 这里...args的含义是把agrs里面的参数展开,...是展开运算符
delete thisArg.fn;
// 4、返回结果
return result;
};
拓展
这里抛出一个边界问题:
如果传入的thisArg本身有一个属性fn,要求不能被覆盖掉,该怎么修改代码?