CALL源码解析

411 阅读4分钟
let obj = {
    name: "obj"
};
 
function func(x, y) {
    console.log(this);
    return x + y;
}
 
console.log(func.call(obj, 10, 20));

call的原理是让func立即执行,并且让this指向obj,把参数传递给func

那怎么样才能让this指向obj呢?

obj.?xxx = func;
obj.?xxx(10,20);

只要按照成员访问这种方式执行,就可以让FUNC中的THIS变为OBJ【前提OBJ中需要有FUNC这个属性】,当然属性名不一定是FUNC,只要属性值是这个函数即可。

对象可以用成员访问的方式,那基本数据类型呢?

创建一个值的两种方法:对于引用数据类型来讲,两种方式没啥区别,但是对于值类型,字面量方式创建的是基本类型值,但是构造函数方式创造的是对象类型值;再但是,不管基本类型还是对象类型都是所属类的实例,都可以调用原型上的方法;(基本值无法给其设置属性,但是引用值是可以设置属性的)

1.字面量创建
let num1 = 10;
let obj1 = {};
 
2.构造函数创建
let num2 = new Number(10);
let obj2 = new Object();

对于字面量创建值,我发设置属性,也就不能用成员访问的方式,我们可以用constructor来创建对象类型值

new num1.constructor(num1);

创建一个值的两种方法:对于引用数据类型来讲,两种方式没啥区别,但是对于值类型,字面量方式创建的是基本类型值,但是构造函数方式创造的是对象类型值;再但是,不管基本类型还是对象类型都是所属类的实例,都可以调用原型上的方法;(基本值无法给其设置属性,但是引用值是可以设置属性的)

那此时我们就开始实现call的功能:

核心原理:给CONTEXT设置一个属性(属性名尽可能保持唯一,避免我们自己设置的属性修改默认对象中的结构,例如可以基于Symbol实现,也可以创建一个时间戳名字),属性值一定是我们要执行的函数(也就是THIS,CALL中的THIS就是我们要操作的这个函数);接下来基于CONTEXT.XXX()成员访问执行方法,就可以把函数执行,并且改变里面的THIS(还可以把PARAMS中的信息传递给这个函数即可);都处理完了,别忘记把给CONTEXT设置的这个属性删除掉(人家之前没有你自己加,加完了我们需要把它删了)

如果CONTEXT是基本类型值,默认是不能设置属性的,此时我们需要把这个基本类型值修改为它对应的引用类型值(也就是构造函数的结果)

Function.prototype.call = function call(context, ...params) {
    // CONTEXT不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型
    if (!/^(object|function)$/.test(typeof context)) {
        if (/^(symbol|bigint)$/.test(typeof context)) {
            context = Object(context);
        } else {
            context = new context.constructor(context);
        }
    }
    let key = Symbol('KEY'),
        result;
    //此时this就是func,把this赋给context[key](也就是obj的成员访问)
    context[key] = this;
    //把参数传给context[key]并且执行
    result = context[key](...params);
    //最后删除context[key]
    delete context[key];
    return result;
};

我们用这个原理来巩固一下:

function fn1(){console.log(1)};
function fn2(){console.log(2)};

fn1.call(fun2);
fn1.call.call(fn2);
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);

首先:fn1.call(fun2);
this => fn1, context => fn2
fn2[xx] = fn1;
fn2[xx]();
执行的是fn1,让fn1中的this变成了fn2,
所以fn1函数中输出的是 "1"

第二问:fn1.call.call(fn2);
this => 第一个call函数,context => fn2
fn2[xx] = 第一个call函数,//此时call已经变成了fn2[xx]
fn2[xx]();
让fn2[xx]执行,就是让第一个call执行,call函数中的this指向fn2,

那call已经变成了fn2[xx],所以这时this就指向了fn2
如果不明白fn2[xx],那就用fn2.xx,这是一样的
fn2.xx执行,那this就是fn2,没有传参,context => window/undefined(严格模式下不传就是undefined)

相当于是window[xx] => fn2
window[xx]()
让fn2执行,this指向window
所以fn2输出的是 "2"

第三问:Function.prototype.call(fn1);
this => Function.prototype, context => fn1
fn1[xx] = Function.prototype;
fn1[xx]();
执行的是Function.prototype,让Function.prototype中的this变成了fn1,
Function.prototype是匿名空函数,所以什么也不输出

第四问:Function.prototype.call.call(fn1);
this => 第一个call函数,context => fn1
fn1[xx] = 第一个call函数,//此时call已经变成了fn1[xx]
fn1[xx]();
让fn1[xx]执行,就是让第一个call执行,call函数中的this指向fn1,

那call已经变成了fn1[xx],所以这时this就指向了fn1
如果不明白fn1[xx],那就用fn1.xx,这是一样的
fn1.xx执行,那this就是fn1,没有传参,context => window/undefined(严格模式下不传就是undefined)

相当于是window[xx] => fn1
window[xx]()
让fn1执行,this指向window
所以fn1输出的是 "1"