手撕JavaScript call apply bind 函数

Posted eyes++

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕JavaScript call apply bind 函数相关的知识,希望对你有一定的参考价值。

call ,apply ,bind ,是 js中非常重要的三个函数原型上的方法,其作用都是改变this的指向:

call函数

call函数语法:

Fun.call(Obj[, para...]);

即第一个参数是this的指向,之后的参数都会作为实参传给新函数。

示例代码如下:

function Fn(para1, para2) {
    console.log(this.clothes);
    this.clothes = "nihao";
    console.log(this.clothes);
    console.log("para1:", para1, " para2:", para2);
}

const o = {
    clothes: "shirt"
};

Fn();
Fn.call(o, 5, "hello");
console.log(o.clothes);


对于Fn.call(o),其处理实际上类似于下面这种方式,其底层的调用还是o.Fn()

const o = {
    clothes: "shirt",
    Fn() {
        ...
        ...
    }
}

然后我们就可以使用代码写出一个效果和call一样的方法:

function Fn(a, b, c) {
    console.log(this.clothes, a, b, c);
}

const o = {
    clothes: "shirt"
};

Function.prototype.myCall = function (o, ...args) {
    // 防止调用myCall不传第一个参数而报错
    o = o || window;
    //  把Fn函数设置为o的方法
    o.Fn = this;
    // 调用函数
    let res = o.Fn(...args);
    // 不修改函数的原型对象
    delete o.Fn;
    // 原函数有返回值则call函数返回res,没有则仍返回undefind
    return res;
}

Fn.myCall(o, 1, 2, 3);

apply方法

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入,且当第一个参数为null、undefined的时候,默认指向window(在浏览器中),使用apply方法改变this指向后原函数会立即执行,且此方法只是临时改变thi指向一次。

apply方法的使用如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function Fn(a, b, c) {
            console.log(this.clothes, a, b, c);
        }

        const o = {
            clothes: "shirt"
        };

        Fn.apply(o, [12, 23, 34]);
    </script>
</body>

</html>

一个类似apply方法的实现如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function Fn(a, b, c) {
            console.log(this.clothes, a, b, c);
        }

        const o = {
            clothes: "shirt"
        };

        Function.prototype.myApply = function (o, args) {
            o = o || window; // 防止调用myApply不传第一个参数而报错
            o.Fn = this; // 将原函数设置为o的方法
            const newArgs = [];
            let res = null;
            if (!args) {
                res = o.Fn();
            } else {
                for (let i = 0; i < args.length; i++) {
                    newArgs.push("args[" + i + "]");
                }
                // 调用函数,如果有返回值,则记录在res中
                res = eval("o.Fn(" + newArgs + ")");
            }
            // 不修改函数的原型对象
            delete o.Fn;
            // 原函数有返回值则call函数返回res,没有则仍返回undefind
            return res;
        }

        Fn.myApply(o, [1, 3, 5]);
    </script>
</body>

</html>

bind方法

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入,call则必须一次性传入所有参数),但是它改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。

bind的使用示例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function Fn(a, b, c) {
            console.log(this.clothes, a, b, c);
        }

        const o = {
            clothes: "shirt"
        };

        // 分次传参
        let temp = Fn.bind(o, 12);  // 不执行
        temp(23, 34);  // 执行
        // 立即执行
        Fn.bind(o, 45)(56, 67);
    </script>
</body>

</html>


bind类似功能的实现:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        function Fn(a, b, c) {
            console.log(this.clothes, a, b, c);
        }

        const o = {
            clothes: "shirt"
        };

        Function.prototype.mybind = function (o) {
            if (typeof this !== 'function') {
                throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
            }
            let aArgs = Array.prototype.slice.call(arguments, 1),
                fToBind = this,
                fNOP = function () {},
                fBound = function () {
                    // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
                    return fToBind.apply(this instanceof fBound ?
                        this :
                        o,
                        // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                        aArgs.concat(Array.prototype.slice.call(arguments)));
                };
            // 维护原型关系
            if (this.prototype) {
                // 当执行Function.prototype.bind()时, this为Function.prototype 
                // this.prototype(即Function.prototype.prototype)为undefined
                fNOP.prototype = this.prototype;
            }
            // 下行的代码使fBound.prototype是fNOP的实例,因此
            // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
            fBound.prototype = new fNOP();
            return fBound;
        };

        let temp = Fn.mybind(o, 12, 23);
        temp(34);
    </script>
</body>

</html>

call、apply、bind三者的区别

  • 三者都可以改变函数的this对象指向。
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
  • 三者都可以传参,但是apply是数组,而call是参数列表
  • apply和call是一次性传入参数,而bind可以分为多次传入。
  • bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。

为什么要修改this指向

在如下代码可以观察到,正常情况下 say 方法中的 this 是指向调用它的 obj 对象的,而定时器 setTimeout 中的 say 方法中的 this 是指向window对象的(在浏览器中),这是因为 say 方法在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的,但我们需要的是 say 方法中 this 指向obj对象,因此我们需要修改 this 的指向。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var name = "张三";
        let obj = {
            name: "李四",
            say: function () {
                console.log(this.name);
            }
        };
        obj.say(); // 李四,this指向obj对象
        setTimeout(obj.say); // 张三,this指向window对象
    </script>
</body>

</html>

以上是关于手撕JavaScript call apply bind 函数的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# JavaScript之手撕callapply

JavaScript中call和apply方法

Javascript中call和apply的区别与详解

Javascript之模拟实现call,apply

Javascript中call和apply的区别与详解

JavaScript中 call和apply