call-js原理系列篇

231 阅读3分钟

作用:借用某个对象的方法同时调用执行。【修改this指向同时执行】

知识储备

call,apply,bind属于显示绑定,这三个方法都能直接修改this指向。

- 其中call与apply比较特殊,在修改this的同时还会执行方法,而bind只是返回一个修改完this的boundFunction并未执行

- call与apply唯一区别在于参数不同

fn.call(this, arg1, arg2); // 参数散列
fn.apply(this, [arg1, arg2]) // 参数使用数组包裹

实现步骤

  1. _call方法挂载到函数原型,让所有函数都可以直接访问_call

  2. obj 是我们方法fn 要借给的对象。在借用的对象身上临时挂载上这个方法,然后调用,调用完,从这个对象身上删除。

  3. fn._call属于this隐式绑定,所以在执行时_call时内部this指向fn,这里的obj.fn = this就是将方法fn赋予成了obj的一条属性,属性名字 也叫‘fn’。obj 是我们方法要接给的对象。

代码实现

第一步:改变this指向

//模拟call方法
Function.prototype._call = function (obj) {
    obj.fn = this; // 此时this就是函数fn
    obj.fn(); // 执行fn
    delete obj.fn; //删除fn
};
fn._call(obj); // obj的name

测试代码

var name = '我是Window 的name';
var obj = {
    name: '我是对象obj 的name'
};

function fn() {
    console.log(this.name);
};
fn(); //我是Window 的name
fn.call(obj); //我是对象obj 的name

第二步:接受参数

函数有一个arguments属性,代指函数接收的所有参数,它是一个类数组。

你可能想到Array.prototype.slice.call(arguments),转成数组再用。很遗憾,我们现在是在模拟call方法,也不行。那就用最基础的for循环。如下:

Function.prototype._call= function (obj) { 
    var args = [];
    // 注意i从1开始 
    for (var i = 1, len = arguments.length; i < len; i++) { 
        args.push(arguments[i]);  
    };  
        console.log(args);// [1, 2, 3]
    }; 
fn._call(obj, 1, 2, 3);

最后的代码实现

Function.prototype._call = function (obj) {
    //判断是否为null或者undefined,同时考虑传递参数不是对象情况
    obj = obj ? Object(obj) : window;

    var args = [];
    // i从1开始
    for (var i = 1, len = arguments.length; i < len; i++) {
        args.push("arguments[" + i + "]");
    };

    obj.fn = this; // 此时this就是函数fn
    let res = eval("obj.fn(" + args + ")"); // 执行fn并丢入参数
    delete obj.fn; //删除fn
    return res;
};

用ES6实现,更简洁。使用拓展运算符处理参数这里

Function.prototype._call = function (obj) {
    obj = obj ? Object(obj) : window;
    obj.fn = this;
    // 利用拓展运算符直接将arguments转为数组
    let args = [...arguments].slice(1);
    let result = obj.fn(...args);

    delete obj.fn
    return result;
};

测试代码

var name = 'window';
var obj = {
    name: 'Macrolam'
};

function fn() {
    console.log(this.name);

fn._call(obj, "我的", "名字", "是"); // 我的名字是Macrolam
fn._call(null, "我的", "名字", "是"); // 我的名字是window
fn._call(undefined, "我的", "名字", "是"); // 我的名字是window

注意:

  • 容错

参数容错处理,防止传入,string、number等简单类型时候报错。用Object包装下。

  • 参数的处理上:

在`args.push(arguments[i])`这一步我们提前将字符串进行了解析,这就导致`eval`在执行时,表达式变成了eval("obj.fn(我的,名字,是)");正常调用函数的形式应该是这样的obj.fn("我的","名字","是"),所以对于eval而言就像传递了三个没加引号的字符串,无法进行解析进而报错。

于是:

把 args.push(arguments[i])

改写:

args.push("arguments[" + i + "]");

args最终就是这个样子["arguments[1]","arguments[2]","arguments[3]"],当执行eval时,arguments[1]此时确实是作为一个变量存在,于是被eval解析成了一个真正的字符传递给了函数。