zoukankan      html  css  js  c++  java
  • this+call、apply、bind的区别与使用

    http://www.ruanyifeng.com/blog/2018/06/javascript-this.html

    https://segmentfault.com/a/1190000018017796

    在函数中 this 到底取何值,是在函数真正被调用执行的时候确定下来的,函数定义的时候确定不了。

    因为 this 的取值是函数执行上下文(context)的一部分,每次调用函数,都会产生一个新的执行上下文环境。当代码中使用了 this,这个 this 的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻。

    关于 this 的取值,大体上可以分为以下七种情况:
    由于严格模式下,禁止this指向全局对象,所以以下示例均运行在非严格模式下

    情况一:全局 & 调用普通函数
    在全局环境中,this 永远指向 window。

    console.log(this === window); //true
    普通函数在调用时候(注意不是构造函数,前面不加 new),其中的 this 也是指向 window。

    var x = 10;
    function foo(){
    console.log(this); //Window
    console.log(this.x); //10
    }
    foo();

    匿名函数中的this指向全局对象window

    var x = 10;
    var foo(){
      a: 20,   
      fn:(function(){

      alert(this.a);

    })()
    }

    foo.fn//10
    foo();

    setInterval和setTimeout定时器中的this指向全局对象window

    情况二:构造函数
    所谓的构造函数就是由一个函数 new 出来的对象,一般构造函数的函数名首字母大写,例如像 Object,Function,Array 这些都属于构造函数。

    function Foo(){
    this.x = 10;
    console.log(this); //Foo {x:10}
    }
    var foo = new Foo();
    console.log(foo.x); //10
    上述代码,如果函数作为构造函数使用,那么其中的 this 就代表它即将 new 出来的对象。

    但是如果直接调用 Foo 函数,而不是 new Foo(),那就变成情况1,这时候 Foo() 就变成普通函数。

    function Foo(){
    this.x = 10;
    console.log(this); //Window
    }
    var foo = Foo();
    情况三:对象方法
    如果函数作为对象的方法时,方法中的 this 指向该对象。

    var obj = {
    x: 10,
    foo: function () {
    console.log(this); //Object
    console.log(this.x); //10
    }
    };
    obj.foo();
    注意:若是在对象方法中定义函数,那么情况就不同了。

    var obj = {
    x: 10,
    foo: function () {
    function f(){
    console.log(this); //Window
    console.log(this.x); //undefined
    }
    f();
    }
    }
    obj.foo();
    可以这么理解:函数 f 虽然是在 obj.foo 内部定义的,但它仍然属于一个普通函数,this 仍指向 window。

    在这里,如果想要调用上层作用域中的变量 obj.x,可以使用 self 缓存外部 this 变量。

    var obj = {
    x: 10,
    foo: function () {
    var self = this;
    function f(){
    console.log(self); //{x: 10}
    console.log(self.x); //10
    }
    f();
    }
    }
    obj.foo();
    如果 foo 函数不作为对象方法被调用:

    var obj = {
    x: 10,
    foo: function () {
    console.log(this); //Window
    console.log(this.x); //undefined
    }
    };
    var fn = obj.foo;
    fn();
    obj.foo 被赋值给一个全局变量,并没有作为 obj 的一个属性被调用,那么此时 this 的值是 window。

    情况四:构造函数 prototype 属性
    function Foo(){
    this.x = 10;
    }
    Foo.prototype.getX = function () {
    console.log(this); //Foo {x: 10, getX: function}
    console.log(this.x); //10
    }
    var foo = new Foo();
    foo.getX();
    在 Foo.prototype.getX 函数中,this 指向的 foo 对象。不仅仅如此,即便是在整个原型链中,this 代表的也是当前对象的值。

    情况五:函数用 call、apply或者 bind 调用。
    var obj = {
    x: 10
    }
    function foo(){
    console.log(this); //{x: 10}
    console.log(this.x); //10
    }
    foo.call(obj);
    foo.apply(obj);
    foo.bind(obj)();
    当一个函数被 call、apply 或者 bind 调用时,this 的值就取传入的对象的值。

    情况六:DOM event this
    在一个 HTML DOM 事件处理程序里,this 始终指向这个处理程序所绑定的 HTML DOM 节点:

    function Listener(){
    document.getElementById('foo').addEventListener('click', this.handleClick); //这里的 this 指向 Listener 这个对象。不是强调的是这里的 this
    }
    Listener.prototype.handleClick = function (event) {
    console.log(this); //<div id="foo"></div>
    }
    var listener = new Listener();
    document.getElementById('foo').click();
    这个很好理解,就相当于是给函数传参,使 handleClick 运行时上下文改变了,相当于下面这样的代码:

    var obj = {
    x: 10,
    fn: function() {
    console.log(this); //Window
    console.log(this.x); //undefined
    }
    };
    function foo(fn) {
    fn();
    }
    foo(obj.fn);
    你也可以用通过 bind 切换上下文:

    function Listener(){
    document.getElementById('foo').addEventListener('click',this.handleClick.bind(this));
    }
    Listener.prototype.handleClick = function (event) {
    console.log(this); //Listener {}
    }
    var listener = new Listener();
    document.getElementById('foo').click();
    前六种情况其实可以总结为: this 指向调用该方法的对象。

    情况七:箭头函数中的 this
    当使用箭头函数的时候,情况就有所不同了:箭头函数内部的 this 是词法作用域,由上下文确定。

    var obj = {
    x: 10,
    foo: function() {
    var fn = () => {
    return () => {
    return () => {
    console.log(this); //Object {x: 10}
    console.log(this.x); //10
    }
    }
    }
    fn()()();
    }
    }
    obj.foo();
    现在,箭头函数完全修复了 this 的指向,this 总是指向词法作用域,也就是外层调用者 obj。

    如果使用箭头函数,以前的这种 hack 写法:

    var self = this;
    就不再需要了。

    var obj = {
    x: 10,
    foo: function() {
    var fn = () => {
    return () => {
    return () => {
    console.log(this); // Object {x: 10}
    console.log(this.x); //10
    }
    }
    }
    fn.bind({x: 14})()()();
    fn.call({x: 14})()();
    }
    }
    obj.foo();
    由于 this 在箭头函数中已经按照词法作用域绑定了,所以,用 call()或者 apply()调用箭头函数时,无法对 this 进行绑定,即传入的第一个参数被忽略。

    【复合场景1】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var someone = {
     name: "Bob",
     showName: function(){
      alert(this.name);
     }
    };
    var other = {
     name: "Tom",
     showName: someone.showName
    }
    other.showName();  //Tom
     
    //以上函数相当于
     
    var other = {
     name: "Tom",
     showName: function(){
      alert(this.name);
     }
    }
    other.showName();  //Tom

    【复合场景2】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var name = 2;
    var a = {
     name: 3,
     fn: (function(){
      alert(this.name);
     })(),
     fn1:function(){
      alert(this.name);
     }
    }
    a.fn;//2[匿名函数中的this指向全局对象]
    a.fn1();//3[对象内部函数的this指向调用函数的当前对象]

    【复合场景3】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    var name = "Bob";
    var nameObj ={
     name : "Tom",
     showName : function(){
     alert(this.name);
    },
     waitShowName : function(){
      var that = this;
      setTimeout(function(){
       that.showName();
      }, 1000);
     }
    };
    nameObj.waitShowName();//"Tom"[that=this改变this的指向,使this从指向全局变量变化到指向nameObj]
     
    var name = "Bob";
    var nameObj ={
     name : "Tom",
     showName : function(){
      alert(this.name);
     },
     waitShowName : function(){
      var that = this;//that指向nameObj
      setTimeout(function(){
       (function(){
        alert(this.name);
       })();
      }, 1000);
     }
    };
    nameObj.waitShowName();// 'Bob'[形成匿名函数,this指向全局变量]

    call 和 apply 的共同点

    它们的共同点是,都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。

    为何要改变执行上下文?举一个生活中的小例子:平时没时间做饭的我,周末想给孩子炖个腌笃鲜尝尝。但是没有适合的锅,而我又不想出去买。所以就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。

    改变执行上下文也是一样的,A 对象有一个方法,而 B 对象因为某种原因,也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?当然是借用 A 对象的啦,既完成了需求,又减少了内存的占用。

    另外,它们的写法也很类似,调用 call 和 apply 的对象,必须是一个函数 Function。接下来,就会说到具体的写法,那也是它们区别的主要体现。

    call 和 apply 的区别

    它们的区别,主要体现在参数的写法上。先来看一下它们各自的具体写法。

    call 的写法

    Function.call(obj,[param1[,param2[,…[,paramN]]]])

    需要注意以下几点:

    • 调用 call 的对象,必须是个函数 Function。
    • call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
    • 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
    function func (a,b,c) {}
    
    func.call(obj, 1,2,3)
    // func 接收到的参数实际上是 1,2,3
    
    func.call(obj, [1,2,3])
    // func 接收到的参数实际上是 [1,2,3],undefined,undefined

    apply 的写法

    Function.apply(obj[,argArray])

    需要注意的是:

    • 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
    • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
    func.apply(obj, [1,2,3])
    // func 接收到的参数实际上是 1,2,3
    
    func.apply(obj, {
        0: 1,
        1: 2,
        2: 3,
        length: 3
    })
    // func 接收到的参数实际上是 1,2,3

    什么是类数组?

    先说数组,这我们都熟悉。它的特征有:可以通过角标调用,如 array[0];具有长度属性length;可以通过 for 循环或forEach方法,进行遍历。

    那么,类数组是什么呢?顾名思义,就是具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组。

    let arrayLike = {
        0: 1,
        1: 2,
        2: 3,
        length: 3
    };

    类数组 arrayLike 可以通过角标进行调用,具有length属性,同时也可以通过 for 循环进行遍历。

    类数组,还是比较常用的,只是我们平时可能没注意到。比如,我们获取 DOM 节点的方法,返回的就是一个类数组。再比如,在一个方法中使用 arguments 获取到的所有参数,也是一个类数组。

    但是需要注意的是:类数组无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。

    call 和 apply 的用途

    下面会分别列举 call 和 apply 的一些使用场景。声明:例子中没有哪个场景是必须用 call 或者必须用 apply 的,只是个人习惯这么用而已。

    call 的使用场景

    1、对象的继承。如下面这个例子:

    function superClass () {
        this.a = 1;
        this.print = function () {
            console.log(this.a);
        }
    }
    
    function subClass () {
        superClass.call(this);
        this.print();
    }
    
    subClass();
    // 1

    subClass 通过 call 方法,继承了 superClass 的 print 方法和 a 变量。此外,subClass 还可以扩展自己的其他方法。

    2、借用方法。还记得刚才的类数组么?如果它想使用 Array 原型链上的方法,可以这样:

    let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

    这样,domNodes 就可以应用 Array 下的所有方法了。

    apply 的一些妙用

    1、Math.max。用它来获取数组中最大的一项。

    let max = Math.max.apply(null, array);

    同理,要获取数组中最小的一项,可以这样:

    let min = Math.min.apply(null, array);

    2、实现两个数组合并。在 ES6 的扩展运算符出现之前,我们可以用 Array.prototype.push来实现。

    let arr1 = [1, 2, 3];
    let arr2 = [4, 5, 6];
    
    Array.prototype.push.apply(arr1, arr2);
    console.log(arr1); // [1, 2, 3, 4, 5, 6]

    bind 的使用

    最后来说说 bind。在 MDN 上的解释是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

    它的语法如下:

    Function.bind(thisArg[, arg1[, arg2[, ...]]])

    bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行。而 apply 和 call 则是立即调用。

    来看下面这个例子:

    function add (a, b) {
        return a + b;
    }
    
    function sub (a, b) {
        return a - b;
    }
    
    add.bind(sub, 5, 3); // 这时,并不会返回 8
    add.bind(sub, 5, 3)(); // 调用后,返回 8

    如果 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。

    总结

    call 和 apply 的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。

    bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。

  • 相关阅读:
    LeetCode Single Number
    Leetcode Populating Next Right Pointers in Each Node
    LeetCode Permutations
    Leetcode Sum Root to Leaf Numbers
    LeetCode Candy
    LeetCode Sort List
    LeetCode Remove Duplicates from Sorted List II
    LeetCode Remove Duplicates from Sorted List
    spring MVC HandlerInterceptorAdapter
    yum
  • 原文地址:https://www.cnblogs.com/leftJS/p/10950378.html
Copyright © 2011-2022 走看看