先看几个有趣的题:
function fn1(){
console.log(this, 1);
}
function fn2(){
console.log(this, 2);
}
fn1.call(); //输出 window 1
fn1.call(fn2); //输出 fn2 1
fn1.call.call(fn2); //输出 window 2
amazing why?
要回答这个问题先要弄清楚call的原理,下面就自己实现一个call:
实现之前要明白call要实现的效果:
- 调用call的函数执行;
- 若有参数,则改变函数的this指向(第一个参数);
- 暂时理解就这么两点;
Function.prototype.call = function (context) {
var content = Object(context) || window;
content.fn = this;
var args = [];
// arguments是类数组对象,遍历之前需要保存长度,过滤出第一个传参
for (var i = 1, len = arguments.length ; i < len; i++) {
// 避免object之类传入
args.push('arguments[' + i + ']');
}
var result = eval('content.fn('+args+')');
delete content.fn;
return result;
}
然后我们一点点解读:
1:把传入的第一个参数作为 call 函数内部的一个临时对象 context;
2:给 context 对象一个属性 fn , 我称呼其为实际执行函数 context.fn ;让 this 关键字(仅仅是关键字,而不是this对象)指向这个属性 ,即 context.fn = this ; 注意 : 在这里的 this 对象指向的是调用call()函数的函数对象。如 fn1.call(fn2);在执行 call 函数时,call 函数内部的this指向的是fn1;然而 fn1.call.call(fn2);在执行 call() 函数时(注意这里必须是打了小括号“()”才算执行函数,fn1.call访问的是一个对象),call函数内部的 this 指向的是 fn1.call 。
3:将传入call函数的其他参数,放入临时数组arr[];
4:利用 eval (笔者采用es3的方法实现,也可以利用其他方式实现)。执行 context.fn( [args] ) ; 实际就是执行 this( [args] );结合第2点。
5:执行完成后再把 context.fn 删除。返回执行 this( [args] ) 的结果
知道原理后我们就解读实例:
fn1.call(); //输出 window 1
fn1 调用call 没有传参,函数内部:
context = undefined || window; --> context = window
context.fn = this; --> window.fn = fn1;
context.fn(); --> window.fn(); --> fn1()
fn1.call(fn2); //输出 fn2 1
fn1 调用call 传入fn2,函数内部:
context = fn2 || window; --> context = fn2
context.fn = this; --> fn2.fn = fn1
context.fn(); --> fn2.fn(); --> f1()
fn1.call.call(fn2); //输出 window 2
fn1.call 调用call 传入fn2,函数内部:
fn1.call.call(fn2) --> Function.prototype.call.call(fn2)
context = fn2 || window; --> context = fn2
context.fn = this; --> fn2.fn = Function.prototype.call
context.fn(); --> fn2.fn(); --> fn2.Function.prototype.call() --> fn2.call() --> 到这里参照第一种情况
fn1.call.call(fn2) ;先将 fn2 作为 临时的 context 对象 。然后 将 fn1.call这个函数对象作为 实际执行函数属性 : context.fn = fn1.call;注意:fn1.call会通过原型链找到最终的对象。其本质为 Function.prototype.call; 然后执行context.fn();可简化为fn2.call() 传入的参数为空,故而 新的 call()函数对象 的this关键字 被替换为window; 而执行 this()时,就是执行 fn2();不涉及 this操作。故最终输出2。
上面的研究结束 然后我们自己可以参照着实现apply bind:
(由于es6已经出来下面写法都使用es5,6实现)
Function.prototype.call = function(context, ...args){
context = context ? Object(context) : global;
context.fn = this;
let result = context.fn(...args);
delete context.fn;
return result;
}
Function.prototype.apply = function(context, ...args){
context = context ? Object(context) : global;
context.fn = this;
let result = context.fn(args);
delete context.fn;
return result;
}
Function.prototype.bind = function(context){
let that = this;
return function(...args){
that.call(context, ...args);
}
}
外加一个有趣的题:
function fc(){
console.log(this);
}
let bindFn = fc.bind(1);
console.log(bindFn);
let bindFn2 = bindFn.bind(2);
bindFn2();
自己试试输出什么。