深入理解apply,call,bind及源码实现,再也不担心分不清了!!!

236 阅读3分钟

apply,call,bind都可以改变this指向,都可以传参数,但是在具体用法上面仍然有差异。

call

参数1:设置this指向; 参数2:设置传入参数,参数以逗号分隔

注:函数立即执行

举例如下:

<script>
    var name = '李四'
    var obj = {
        name: '张三'
    };

    function func1(n) {
        console.log(this.name + n + '岁了')
    }
    func1.call(obj, '18'); //'张三18岁了'
    func1.call(null, '19'); //'李四19岁了'
</script>

源码实现:

Function.prototype.call = function(obj) {
    // 当call的第一个参数没有或者是null的时候,在浏览器环境this的指向是window,
    var obj = obj || window;
    // this是call前面的方法
    obj.fn = this;
    // 用于存储call后面的参数
    var args = [];
    var len = arguments.length;
    // 这里是将传入的参数整理起来,后面要处理
    for (var i = 1; i < len; i++) {
        args.push('arguments[' + i + ']');
    };
    // 在eval的使用下,args数组会变成一个一个参数字符串(默认是会调用Array.toString(),变成以逗号隔开的形式)
    /*eval()函数计算或执行参数。如果参数是表达式,则eval()计算表达式。如果参数是一个或多个 JavaScript 语句,则 eval()执行这些语句*/
    var result = eval('obj.fn(' + args + ')');
    // 删除obj里面的fn方法,只是为了使用,而不应该改变obj
    delete obj.fn;
    // 因为函数可能有返回值,所以把结果也返回
    return result;
};
apply

参数1:设置this指向; 参数2:设置传入参数,参数是数组形式

注:函数立即执行

举例如下:

<script>
    var obj = {
        desc: "我是张三"
    };

    function func2(name, age) {
        console.log(this); //{desc: '我是张三'}
        console.log(`${name}的年龄是${age}岁`) //张三的年龄是18岁
    }
    func2.apply(obj, ['张三', '18']);
</script>

源码实现:

Function.prototype.apply = function(obj, arr) {
    // 当call的第一个参数没有或者是null的时候,在浏览器环境this的指向是window,
    var obj = obj || window;
    // this是call前面的方法
    obj.fn = this;
    //判断有没有传值
    if (!arr) {
        result = obj.fn();
    } else {
        //判断传入的是不是数组,不是的话抛出异常
        if (!Array.isArray(arr)) {
            throw new Error('传的必须是数组');
        };
        var args = [];
        // 这里是将传入的参数整理起来,后面要处理
        for (var i = 0; i < arr.length; i++) {
            args.push('arr[' + i + ']');
        };
        var result = eval('obj.fn(' + args + ')');
    }
    delete obj.fn;
    return result;
}
bind

参数1:设置this指向; 参数2:设置传入参数,参数以逗号分隔

注:返回值是一个函数。

举例如下:

<script>
    var obj = {
        name: "李四"
    };

    function func3(age) {
        console.log(this.name + age + '岁了')
    }
    var func5 = func3.bind(obj, 19);
    func5();//李四19岁了
</script>

源码实现:

Function.prototype.bind = function(obj) {
    var self = this;
    if (typeof this !== 'function') {
        throw new Error('只有函数才可以调用bind');
    };
    //第一个参数为它运行时的this,应该取第二个之后的参数
    var args = Array.prototype.slice.call(arguments, 1);
    //返回一个新函数
    var newFn = function() {
        //这里的三相表达式是为了在new或者直接调用的时候,能正确改变this指向
        self.apply(this instanceof f ? this : obj, args.concat(Array.prototype.slice.call(arguments)));
    };
    //过渡函数是为了防止改变了新创建出来的函数的原型同样也修改了原函数的原型,使用过渡函数可以避免。
    var f = function() {};
    f.prototype = this.prototype;
    newFn.prototype = new f();
    return newFn;
};
扩展
Array.prototype.slice

文章当中用到了Array.prototype.slice.call(arguments)这种写法,目的是把参数类数组arguments转换成数组。下面的是slice方法的实现:

Array.prototype.slice = function(start, end) {
    var result = new Array();
    start = start || 0;
    // this指向调用的对象,当用了call后,能够改变this的指向,也就是指向传进来的对象。
    end = end || this.length;
    //使用如下for循环的方法将this的每一个字符push到一个数组之后,return出去这个数组
    for (var i = start; i < end; i++) {
        result.push(this[i]);
    }
    return result;
}
总结:将函数参数转成数组

方法一:Array.prototype.slice.call(arguments);

方法二:[].slice.call(arguments);

方法三:Array.from(arguments);

方法四:[...arguments];

方法五:封装代码块如下:

let toArray = function() {
    try {
        return Array.prototype.slice.call(arguments);
    } catch {
        let resArray = [];
        for (let i = 0, len = arguments.length; i < len; i++) {
            resArray[i] = arguments[i]
        }
        return resArray;
    }
}
console.log(toArray("zhangsan", "20")) //[ 'zhangsan', '20' ];