call
call的作用有两个:
- 改变this指向
- 可以让函数执行
下面我们就通过自己实现一个call来看看它的原理
function fn() {
console.log(this);
}
Function.prototype.call = function(thisValue, ...args) {
if (thisValue == null) {// 包含了undefined和null两种情况
return fn(...args);
}
if (typeof thisValue !== 'object') {
thisValue = new Object(thisValue);
}
let context = this;// context就是fn,原函数
thisValue.f = context;// 这样f作用域中的this就是thisValue,也就是我们传进去的那个新的上下文
thisValue.f(...args);
delete thisValue.f;
}
fn.call(null);
fn.call(1,3,4,5);
我们简单总结下其中的过程:
- 当然是类型判断
- 保存原函数
- 绑定新的上下文
- 执行原函数
就是这么so easy。
so easy?那我们做一个面试题试试看,下面代码的结果是什么?
function fn() {
console.log(this);
}
function fn2() {
console.log('fn2');
}
fn.call.call.call(fn2);
是不是突然就蒙了?最后结果是fn2
一步步分析:
首先fn.call,很明显fn上面没有call方法那么就去fn的__proto__上找,找到了Function.prototype.call,这个call方法就是所有函数都有的call方法
继续,fn.call.call也就是要找到Function.prototype.call上的call方法,别忘了call本身也是一个函数,他自然有call方法,当然还是Function.prototype.call,所以到了这一步其实就是拿到了原型上的call方法
最终的结果就是call方法.call(this), 而this就是fn2。
apply
apply基本上和call一样,区别主要是在参数上,apply的第二个参数是数组 实现:
Function.prototype.apply = function(thisValue, args) {
if (thisValue == null) {
return fn(args);
}
if (typeof thisValue !== 'object') {
thisValue = new Object(thisValue);
}
let context = this;
thisValue.f = context;
thisValue.f(args);
delete thisValue.f;
}
这里我们想一下,如果一个函数有自己的apply方法,那么如果想使用原型上的apply方法怎么实现呢?
function fn() {
console.log(this, arguments);
}
fn.apply = function() {
console.log('inner apply');
}
很明确
fn.apply();
只会调用自己的apply方法, 那么下面的写法呢?
Function.prototype.apply(fn, [2, 3, 4]);
乍一看是那么回事,其实是错误的,我们结合上面apply实现的原理,此时apply内部的this是Function.prototype这明显不对了。正确的写法是
Function.prototype.apply.call(fn, 1, [2,3,4]);
这种写法其实就等效于
let f = Function.prototype.apply;
f.call(fn, 1, [2, 3, 4]);
call的内部会执行
fn.f(1, [2,3,4]);
这样call方法把Function.prototype.apply方法的this变为了fn, 后面的 1, [2,3,4]作为Function.prototype.apply的参数传入,那么1就相当于Function.prototype.apply方法thisValue, [2, 3, 4]就是参数。如果用Reflect的话就会更简单
Reflect.apply(fn, 1, [2, 3, 4]);
bind
bind的作用:返回一个新的函数,并且可以改变this指向,注意它的返回值是一个函数。
实现也很简单:
Function.prototype.bind = function(thisValue, ...args) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
if (typeof thisValue !== 'object') {
thisValue = new Object(thisValue);
}
let context = this;
thisValue.f = context; // 这个f可以通过Object.defineProperty变为不可枚举
console.log(thisValue);
return function(...values) {
thisValue.f(...args, ...values);
}
}
测试:
function fn(...args) {
console.log(this, args);
}
let bindFunc = fn.bind(1, 2);
bindFunc = bindFunc.bind(5, 5);
bindFunc = bindFunc.bind(100, 100);
bindFunc(3);
结果
[Number: 1] { f: [Function: fn] } [ 2, 5, 100, 3 ]
我们测试了比较复杂的场景,但是多次绑定之后的结果依然是1。
因为第一次bind之后返回了一个函数f,之后的bind都是把这个函数f挂载到了一个新的对象上,比如f挂载到5返回f1, 然后f1挂载到100返回f2, 最终的执行顺序类似下面
function f2 () {// 先执行f2
console.log(this);// this就是100 然后执行100.f1
function f1 () {
console.log(this);// this就是5 然后执行5.f
function f () {
console.log(this) // this是1
fn(); // 最终执行的就是1.fn(), 所以结果是1
}
}
}
所以说多次绑定无意义,但是传参是有意义的。