JS学习笔记-this、apply、call、bind(经典面试题)

302 阅读4分钟

1.前言

JS中的this、apply、call、bind是一道经典面试题,了解this 的指向和 call、apply、bind 三者的区别,减少在业务代码中出现的报错,使问题得到解决。

2.this 的指向

在 ES5 中,其实 this 的指向,始终坚持一个原理:this 永远指向最后调用它的那个对象。

<script>
var name = "zhangsan";
    function a() {
        var name = "lisi";
        console.log(this.name);          // zhangsan
        console.log("inner:" + this);    // inner: Window
        }
    a();
    console.log("outer:" + this)         // outer: Window
</script>

this 永远指向最后调用它的那个对象,调用 a 的地方 a();,前面没有调用的对象那么就是全局对象 window,这就相当于是 window.a();

注意,这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined。

<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",
        fn: function () {
            console.log(this.name);      // lisi
        }
    }
    a.fn();
</script>

在这个例子中,函数 fn 是对象 a 调用的,所以打印的值就是 a 中的 name 的值。

<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",
        fn: function () {
            console.log(this.name);      // lisi
        }
    }
    window.a.fn()
</script>

这里打印 lisi 的原因也是因为刚刚那句话“this 永远指向最后调用它的那个对象”,最后调用它的对象仍然是对象 a

<script>
    var name = "zhangsan";
    var a = {
        fn: function () {
            console.log(this.name);      // undefined
        }
    }
    window.a.fn();
</script>

这里为什么会打印 undefined 呢?这是因为正如刚刚所描述的那样,调用 fn 的是 a 对象,也就是说 fn 的内部的 this 是对象 a,而对象 a 中并没有对 name 进行定义,所以 log 的 this.name 的值是 undefined。

这个例子还是说明了:this 永远指向最后调用它的那个对象,因为最后调用 fn 的对象是 a,所以就算 a 中没有 name 这个属性,也不会继续向上一个对象寻找 this.name,而是直接输出 undefined。

 <script>
    var name = "zhangsan";
    var a = {
        name: "lisi",
        fn: function () {
            console.log(this.name);      // zhangsan
        }
    }

    var f = a.fn;
    f();
</script>

为什么不是lisi,这是因为虽然将 a 对象的 fn 方法赋值给变量 f 了,但是没有调用,“this 永远指向最后调用它的那个对象”,由于f 并没有调用,所以 fn() 最后仍然是被 window 调用的。所以 this 指向的也就是 window。

可以看出,this 的指向并不是在创建的时候就可以确定的,在 es5 中,永远是this 永远指向最后调用它的那个对象。

<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",

        func1: function () {
            console.log(this.name)
        },

        func2: function () {
            setTimeout(function () {
                this.func1()
            }, 100);
        }

    };
    a.func2()     // Uncaught TypeError: this.func1 is not a function
</script>
<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",

        func1: function () {
            console.log(this.name)
        },
        func2: function () {
            setTimeout(() => {
                this.func1()
            }, 100);
        }
    };
    a.func2()     // lisi
</script>

在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout 的对象是 window,但是在 window 中并没有 func1 函数。

ES6 的箭头函数是可以避免 ES5 中使用 this 的坑的。箭头函数的 this 始终指向函数定义时的 this,而非执行时。“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。

<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",
        func1: function () {
            console.log(this.name)
        },
        func2: function () {
            var _this = this;
            setTimeout(function () {
                _this.func1()
            }, 100);
        }
    };
    a.func2()       // lisi
</script>

如果不使用 ES6,我们是先将调用这个函数的对象保存在变量 _this 中,然后在函数中都使用这个 _this,这样 _this 就不会改变了。

3.使用 apply、call、bind解决this指向问题

3.1 apply

<script>
    var name = "zhangsan"; 
    var a = {
        name: "lisi",
        func1: function () {
            console.log(this.name)
        },
        func2: function () {
            setTimeout(function () {
                this.func1()
            }.apply(a), 100);
        }
    };
    a.func2()            // lisi
</script>

3.2 call

<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",
        func1: function () {
            console.log(this.name)
        },
        func2: function () {
            setTimeout(function () {
                this.func1()
            }.call(a), 100);
        }
    };
    a.func2()            // lisi
</script>

3.3bind

<script>
    var name = "zhangsan";
    var a = {
        name: "lisi",
        func1: function () {
            console.log(this.name)
        },
        func2: function () {
            setTimeout(function () {
                this.func1()
            }.bind(a)(), 100);
        }
    };
    a.func2()            // lisi
</script>

4.bind 和 apply、call 区别

<script>
    var a = {
    fn : function (a,b) {
        console.log( a + b)  // 无打印结果,why?
        }
    }

    var b = a.fn;
    b.bind(a,1,2)
</script>
<script>
    var a = {
    fn : function (a,b) {
        console.log( a + b)  // 30
        }
    }

    var b = a.fn;
    b.bind(a,10,20)()
</script>

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

所以我们可以看出,bind 是创建一个新的函数,我们必须要手动去调用。

总结:call和apply改变了函数的this上下文后便执行该函数,而bind则是返回改变了上下文后的一个函数。

5.bind实现

<script>
        if (!Function.prototype.bind) {
            Function.prototype.bind = function (oThis) {
                if (typeof this !== "function") {
                    // closest thing possible to the ECMAScript 5
                    // internal IsCallable function
                    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
                }

                var aArgs = Array.prototype.slice.call(arguments, 1),
                    fToBind = this, // 此处的 this 指向目标函数
                    fNOP = function () { },
                    fBound = function () {
                        return fToBind.apply(this instanceof fNOP
                            ? this // 此处 this 为 调用 new obj() 时所生成的 obj 本身
                            : oThis || this, // 若 oThis 无效则将 fBound 绑定到 this
                            // 将通过 bind 传递的参数和调用时传递的参数进行合并, 并作为最终的参数传递
                            aArgs.concat(Array.prototype.slice.call(arguments)));
                    };

                // 将目标函数的原型对象拷贝到新函数中,因为目标函数有可能被当作构造函数使用
                fNOP.prototype = this.prototype;
                fBound.prototype = new fNOP();

                return fBound;
            };
        }
    </script>

6.call、apply的区别

fn.call(obj, arg1, arg2, arg3...);
fn.apply(obj, [arg1, arg2, arg3...]);

他们俩之间的差别在于参数的区别,call和aplly的第一个参数都是要改变上下文的对象,而call从第二个参数开始以参数列表的形式展现,apply则是把除了改变上下文对象的参数放在一个数组里面作为它的第二个参数。

6.1求数组中的最大和最小值

var arr = [1,2,33,44,55,-1];

Math.max.apply(Math, arr);
Math.max.call(Math, 1,2,33,44,55,-1);

Math.min.apply(Math, arr);
Math.min.call(Math, 1,2,33,44,55,-1);

6.2 将伪数组转化为数组

 <script>
    var obj = {
        0: 'zhangsan',
        1: 'lisi',
        2: 'wanwu',
        length: 3
    }
    var arr = Array.prototype.slice.call(obj);  
    console.log(arr) //  ["zhangsan", "lisi", "wanwu"]
</script>

上面arr便是一个包含obj元素的真正的数组啦

注意数据结构必须是以数字为下标而且一定要有length属性 没有length这份属性 arr =[]

7 小结

以上就是js中call,apply,bind的区别内容,更多内容可以一起交流。