• 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 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。

  • 相关阅读:
    sql server中的孤立用户
    BWOA开源项目引子
    海边拾贝
    ABAP syntax_error 错误: form send_cmplx_data_015 does not exist.
    一点总结,手机应用开发前景
    这个周末发生了很多事
    品味单反
    远图(FarMap)使用
    And和手机随想
    远图(FarMap)花絮
  • 原文地址:https://www.cnblogs.com/leftJS/p/10950378.html
Copyright © 2020-2023  润新知