zoukankan      html  css  js  c++  java
  • 491 CALL和APPLY以及BIND语法(含BIND的核心原理),CALL和APPLY的应用(类数组借用数组原型方法),CALL源码解析及阿里面试题

    一、JS中关于this的五种情况分析

    * this:
        全局上下文中的this是window;
        块级上下文中没有自己的this,它的this是继承所在上下文中的this的;【和箭头函数类似。】
        在函数的私有上下文中,this的情况会多种多样,也是接下来重点研究的.
     *
     * this是执行主体,不是执行上下文(EC才是执行上下文)
     *    例如:刘德华拿着加了五个鸡蛋的鸡蛋灌饼去北京大饭店吃早餐(事情本身是吃早餐,刘德华吃早餐,这件事情的主体是刘德华【this】,在北京饭店吃,北京饭店是事情发生所在的上下文【EC】)
     *
     *
     * 如何区分执行主体?
     *    1. 事件绑定:给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的this是当前元素本身(特殊:IE6~8中基于attachEvent方法实现的DOM2事件绑定,事件触发,方法中的this是window,而不是元素本身)。
     * 
     *    2. 普通方法执行(包含自执行函数执行、普通函数执行、对象成员访问调取方法执行等):只需要看函数执行的时候,方法名前面是否有“点”,有“点”,“点”前面是谁,this就是谁,没有“点”,this就是window[非严格模式],严格模式是undefined。
     * 
     *    3. 构造函数执行(NEW XXX):构造函数体中的this是当前类的实例。
     * 
     *    4. ES6中提供了ARROW FUNCTION(箭头函数): 箭头函数没有自己的this,它的this是继承所在上下文中的this。
     * 
     *    5. 可以基于call/APPLY/BIND等方式,强制手动改变函数中的this指向:这三种模式是很直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)。
    
    // 验证 块级上下文中没有自己的this,它的this是继承所在上下文中的this的;【和箭头函数类似】
    console.log(c) // undefined
    if (true) {
        console.log(c) // 函数c
        let b = 2
        function c() {
            console.log(3)
        }
        let e = 5
        console.log(this.b) // undefined
        console.log(this.c) // 函数c
        console.log(this.e) // undefined
    }
    
    console.log(c) // 函数c
    
    
    // 事件绑定 DOM0  DOM2
    let body = document.body;
    body.onclick = function () {
      // 事件触发,方法执行,方法中的this是body
      console.log(this);
    };
    body.addEventListener('click', function () {
      console.log(this); // => body
    });
    
    
    // IE6~8中的DOM2事件绑定
    box.attachEvent('onclick', function () {
      console.log(this); // => window
    });
    
    
    // ----------------------------------
    
    
    // IIFE
    (function () {
      console.log(this); // => window
    })();
    
    
    // ----------------------------------
    
    
    let obj = {
      fn: (function () {
        console.log(this); // => window
        return function () { }
      })() //把自执行函数执行的返回值赋值给obj.FN
    };
    
    
    // ----------------------------------
    
    
    function func() {
      console.log(this);
    }
    let obj = {
      func: func
    };
    func(); // => 方法中的this: window 【前面没有点,window调用func】
    obj.func(); // => 方法中的this: obj【前面有点,obj调用func】
    
    
    // ----------------------------------
    
    
    // => 数组实例基于原型链机制,找到array原型上的SLICE方法([].slice),然后再把SLICE方法执行,此时SLICE方法中的this是当前的空数组
    [].slice();
    array.prototype.slice(); // => SLICE方法执行中的this:array.prototype
    [].__proto__.slice(); // => SLICE方法执行中的this:[].__proto__ === array.prototype
    
    
    // ----------------------------------
    
    
    function func() {
      // this  =>  window
      console.log(this);
    }
    document.body.onclick = function () {
      // this  =>  body
      func();
    };
    
    
    // ----------------------------------
    
    
    function Func() {
      this.name = "F";
      // => 构造函数体中的this在“构造函数执行”的模式下,是当前类的一个实例,并且this.XXX = XXX是给当前实例设置的私有属性
      console.log(this);
    }
    
    Func.prototype.getNum = function getNum() {
      // 而原型上的方法中的this不一定都是实例,主要看执行的时候,“点”前面的内容
      console.log(this);
    };
    
    let f = new Func; // Func {name: "F"}
    f.getNum(); // Func {name: "F"}
    f.__proto__.getNum(); // {getNum: ƒ, constructor: ƒ}
    Func.prototype.getNum(); // {getNum: ƒ, constructor: ƒ}
    
    
    // ----------------------------------
    
    
    let obj = {
      func: function () {
        console.log(this);
      },
      sum: () => {
        console.log(this);
      }
    };
    obj.func(); // => this: obj
    obj.sum(); // => this是所在上下文(EC(G))中的this: window
    obj.sum.call(obj); // this:window,箭头函数是没有this,所以哪怕强制改也没用  
    
    
    // ----------------------------------
    
    
    let obj = {
      i: 0,
      // func:function(){}
      func() {
        // this: obj
        let _this = this;
        setTimeout(function () {
          // this: window,回调函数中的this一般是window(但有特殊情况)
          _this.i++;
          console.log(_this);
        }, 100);
      }
    };
    obj.func();
    
    
    
    // ----------------------------------
    
    
    
    var i = 0
    let obj = {
      i: 0,
      func() {
        let _this = this;
        setTimeout(function () {
          console.log(++_this.i); // 1
          console.log(this.i); // 0
        }, 100);
      }
    };
    obj.func();
    
    
    // ----------------------------------
    
    
    let obj = {
      i: 0,
      func() {
        setTimeout(function () {
          // 基于BIND把函数中的this预先处理为obj
          this.i++;
          console.log(this);
        }.bind(this), 1000);
      }
    };
    obj.func();
    
    
    // ----------------------------------
    
    
    // 建议不要乱用箭头函数(部分需求用箭头函数还是很方法便的)
    let obj = {
      i: 0,
      func() {
        setTimeout(() => {
          // 箭头函数中没有自己的this,用的this是上下文中的this,也就是obj
          this.i++;
          console.log(this);
        }, 1000);
      }
    };
    obj.func();
    
    

    二、call和APPLY以及BIND语法(含BIND的核心原理)

    /*
     * Function.prototype:
     *    call:[function].call([context], params1, params2,...),[function]作为Function内置类的一个实例,可以基于__proto__找到Function.prototype的call方法,并且把找到的call方法执行;
     *    在call方法执行的时候,会把[function]执行,把函数中的this指向为[context],并且把params1,params2...等参数值分别传递给函数
     * 
     *    apply:[function].apply([context], [params1, params2,...]),和call作用一样,只不过传递给函数的参数需要以数组的形式传递给apply。
     * 
     *    bind:[function].bind([context], params1, params2,...),语法上和call类似,但是作用和call/apply都不太一样:
     *    call/apply都是把当前函数立即执行,并且改变函数中的this指向的,而bind是一个预处理的思想,基于bind只是预先把函数中的this指向[context],把params这些参数值预先存储起来,但是此时函数并没有被执行。
     * 
     * 这三个方法都是用来改变函数中的this的。 
    */
    
    
    //  --------------------------------------------
    
    
    let body = document.body;
    let obj = {
        name: "obj"
    };
    
    function func(x, y) {
        console.log(this, x, y);
    }
    
    func(10, 20); // => this: window
    obj.func(); // => Uncaught TypeError: obj.func is not a function
    
    
    // ================================1
    
    
    // call和apply的唯一区别在于传递参数的形式不一样
    func.call(obj, 10, 20);
    func.apply(obj, [10, 20]);
    
    // call方法的第一个参数,如果不传递,或者传递的是null、undefiend,在非严格模式下都是让this指向window(严格模式下传递的是谁, this就是谁, 不传递this, 是undefined)
    func.call(); // window
    func.call(null); // window
    func.call(undefined); // window
    func.call(11); // Number {11}
    
    
    // ================================2
    
    
    // => 把func函数本身绑定给body的click事件行为,此时func并没有执行,只有触发body的click事件,我们的方法才会执行
    body.onclick = func;
    body.onclick = func(10, 20); // => 先把func执行,把方法执行的返回结果作为值绑定给body的click事件
    
    // 需求:把func函数绑定给body的click事件,要求当触发body的点击行为后,执行func,但是此时需要让func中的this变为obj,并且给func传递10,20
    // body.onclick = func.call(obj, 10, 20); // => 这样不行,因为还没点击func就已经执行了
    body.onclick = func.bind(obj, 10, 20); // 使用bind
    
    // 在没有bind的情况下我们可以这样处理(bind不兼容IE6~8)
    body.onclick = function anonymous() {
        func.call(obj, 10, 20); // 不是return,而是执行
    };
    
    
    // ================================3
    
    
    // 【重写bind函数】
    // 执行BIND(BIND中的this是要操作的函数), 返回一个匿名函数给事件绑定或者其它的内容, 当事件触发的时候, 首先执行的是匿名函数,此时匿名函数中的this和BIND中的this是没有关系的。
    // BIND的内部机制就是利用闭包(柯理化函数编程思想),预先把需要执行的函数、改变的this以及后续需要给函数传递的参数信息等都保存到不释放的上下文中,后续使用的时候直接拿来用,这就是经典的预先存储的思想。
    Function.prototype.bind = function bind(context = window, ...params) {
        //this->func
        let _this = this;
        return function anonymous(...inners) {
            // _this.call(context, ...params);
            _this.apply(context, params.concat(inners));
        };
    };
    
    body.onclick = func.bind(obj, 10, 20);
    
    body.onclick = function anonymous(ev) { // => ev事件对象 
        // 这里不是返回func.call(obj,10,20,ev),而是直接执行func.call(obj,10,20,ev),因为一旦触发body.onclick,就要求执行代码
        func.call(obj, 10, 20, ev);
    };
    
    setTimeout(func.bind(obj), 1000);
    // setTimeout(function anonymous() {
    // 
    // }, 1000);
    

    三、call和APPLY的应用(类数组借用数组原型方法)

    **重要**:我不是某个类的实例,不能直接用它原型上的方法,但是我可以让某个类原型上的方法执行,让方法中的this(一般是需要处理的实例)变为我,这样就相当于我在“借用”这个方法实现具体的功能。
    这种借用规则,利用的就是call改变this实现的,也是面向对象的一种深层次应用。
    
    // 需求:需要把类数组转换为数组。
    // 类数组:具备和数组类似的结构(索引、LENGTH,以及具备INTERATOR可迭代性),但是并不是数组的实例(不能用数组原型上的方法),我们把这样的结构称为类数组结构。【类数组的__proto__指向object,而不是array的prototype。】
    function func() {
      // 1.array.from
      let args = array.from(arguments);
      console.log(args);
    
    
      // --------------------------------------
    
    
      // 2.基于ES6的展开运算符
      let args = [...arguments];
      console.log(args);
    
    
      // --------------------------------------
    
    
      // 3.手动循环
      let args = [];
      for (let i = 0; i < arguments.length; i++) {
        args.push(arguments[i]);
      }
      console.log(args);
    
    
      // --------------------------------------
    
    
      // 4.arguments具备和数组类似的结构,所以操作数组的一些代码(例如:循环)也同样适用于arguments;如果我们让array原型上的内置方法执行,并且让方法中的this变为我们要操作的类数组,那么就相当于我们在“借用数组原型上的方法操作类数组”,让类数组也和数组一样可以调用这些方法实现具体的需求
      let args = Array.prototype.slice.call(arguments);
      let args = [].slice.call(arguments);
      console.log(args);
    
      // 借用array.prototype.forEach,让forEach中的this指向arguments
      [].forEach.call(arguments, item => {
        console.log(item);
      });
    }
    
    func(10, 20, 30, 40);
    
    
    // --------------------------------------
    
    
    // 【手动实现一个复制数组元素的方法。mySlice方法不传任何参数,则得到的数组的元素的是原数组的每一项。】
    Array.prototype.mySlice = function mySlice() {
      // this->arr
      let args = [];
      for (let i = 0; i < this.length; i++) {
        args.push(this[i]);
      }
      return args;
    };
    let arr = [10, 20, 30];
    console.log(arr.mySlice());
    
    
    // ================================5
    
    
    // 需求:获取数组中的最大值
    let arr = [12, 13, 2, 45, 26, 34];
    
    // 方法1
    let max = arr.sort((a, b) => b - a)[0];
    console.log(max);
    
    // 方法2
    let max = arr[0];
    arr.forEach(item => {
      if (item > max) {
        max = item;
      }
    });
    console.log(max);
    
    // 方法3
    // Math.max(n1,n2,...)
    let max = Math.max(...arr);
    let max = Math.max.apply(Math, arr); // max中的this还是Math
    console.log(max);
    
    // ------------
    
    // 数组去重
    let s = new Set([11, 22, 22, 33, 11])
    console.log(Array.from(s))
    

    四、call源码解析及阿里面试题

    核心原理:
    给context设置一个属性:属性名尽可能保持唯一, 避免我们自己设置的属性修改默认对象中的结构, 可以基于Symbol实现, 也可以创建一个时间戳名字;
    属性值一定是我们要执行的函数,也就是this, call中的this就是我们要操作的这个函数,就是call的调用者;
    接下来基于context.XXX()成员访问执行方法,就可以把函数执行,并且改变里面的this(还可以把params中的信息传递给这个函数);
    都处理完了,别忘记把给context设置的这个属性删除掉(之前没有, 你自己加, 加完了, 要把它删了)
    如果context是基本类型值,默认是不能设置属性的,此时我们需要把这个基本类型值修改为它对应的引用类型值(也就是构造函数的结果)

    Function.prototype.call = function call(context, ...params) {
      // 【非严格模式下】不传或者传递null、undefined都让this最后改变为window。
      // 条件成立,做什么;条件不成立,啥都不想干,null,用 void 0、undefined,但是不写就报错。
      // 【undefined === undefined、undefined == null 都是true。】
      context == undefined ? context = window : null;
      // 不应该是给context赋值。
      // context = context == undefined ? window : context; 
      // context不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型 
      // 【数字、字符串、布尔、undefined、null也可以用object()转成对象,下面的代码直接一行代码即可:ctx = object(ctx)。】
      if (!/^(object|function)$/.test(typeof context)) {
        if (/^(symbol|bigint)$/.test(typeof context)) {
          // symbol、bigint不能通过new创建对象,要用object()
          context = object(context);
        } else {
          context = new context.constructor(context);
        }
      }
      let key = Symbol('KEY'),
        result;
      context[key] = this;
      result = context[key](...params);
      delete context[key];
      return result;
    };
    
    let obj = {
      name: "obj"
    };
    
    function func(x, y) {
      console.log(this);
      return x + y;
    }
    
    console.log(func.call(obj, 10, 20));
    
    // 只要按照成员访问这种方式执行,就可以让FUNC中的this变为obj【前提obj中需要有FUNC这个属性】,当然属性名不一定是FUNC,只要属性值是这个函数即可
    // obj.$$xxx = func;
    // obj.$$xxx(10,20);
    
    // 创建一个值的两种方法:对于引用数据类型来讲,两种方式没啥区别,但是对于值类型,字面量方式创建的是基本类型值,但是构造函数方式创造的是对象类型值;但是,不管基本类型还是对象类型都是所属类的实例,都可以调用原型上的方法;(基本值无法给其设置属性,但是引用值是可以设置属性的)
    // 1.字面量创建
    let num1 = 10;
    let obj1 = {};
    new num1.constructor(num1);
    
    // 2.构造函数创建
    let num2 = new Number(10);
    let obj2 = new object();
    
    // 阿里面试题
    // 总结:如果前面有多个call,最终执行的是第一个形参代表的实参,因为最后的this会指向第一个形参。
    function fn1(){console.log(1);}
    function fn2(){console.log(2);}
    fn1.call(fn2); // 1
    fn1.call.call(fn2); // 2
    Function.prototype.call(fn1); // 啥也不输出
    Function.prototype.call.call(fn1); // 1,和第二个一样
    
    // 我写的解析
    // 总结:如果前面有多个call,最终执行的是第一个形参代表的实参,因为最后的this会指向第一个形参。
    // fn1.call(fn2);
    // this是fn1, ctx是fn2, ctx.xxx = fn1, ctx.xxx(), fn1()
    
    // fn1.call.call(fn2);
    // this: fn1.call, ctx: fn2, fn2.xxx = fn1.call, fn2.xxx(), call(), 开始新一轮调用call函数,this: fn2, ctx: window, window.xxx = fn2, window.xxx(),   fn2(), => 2
    

    fn1.call.call.call.call(fn2):把最后一个call执行,只是此时call中的this --> fn1.call.call.call.call【call函数】

    【最后一个call指第4个call,即执行第4个call,此时this是fn1.call.call.call,其实就是按照原型链,一级级找,最后找到的是call函数。fn1.call找到了原型链上的call,再.call,还是找到原型链上的call,以此类推。】

    【每一轮执行call,都要重新考虑调用者this、形参ctx。】、

    总结:如果前面有多个call,最终执行的是第一个形参代表的实参,因为最后的this会指向第一个形参


    总结:
    B.call(A, 20, 10);
    一个call:最后执行的是前面的B,并且B中的this变为A,剩下的20、10都传递给B

    B.call.call.call.call.call(A, 20, 10);
    B.call.call.call: 跟B没啥关系,是B作为实例找到的call方法
    第一次最后一个call执行:把call执行(一坨),让他里面的this是A,给他传递20、10
    第二次执行call:“类似于 A.call(20, 10)” 执行的是A, A中的this是20, 传参一个10


    var name = '哈哈';
    function A(x, y) {
      var res = x + y;
      console.log(res, this.name);
    }
    function B(x, y) {
      var res = x - y;
      console.log(res, this.name);
    }
    B.call(A, 40, 30);
    B.call.call.call.call.call.call(A, 20, 10);
    Function.prototype.call(A, 60, 50);
    Function.prototype.call.call.call(A, 80, 70);
    
    
    ----------------------------------
    
    B.call(A, 40, 30);
    找到call方法把它执行,在执行call的过程中:
    this => B, context => A, params => [40, 30]
    A.xxx = B;
    result = A.xxx(40, 30); 让B执行, 让B中的this变为A, 给B传递40、30
    => 10 'A'
    
    
    -----------------------------------
    
    
    B.call.call.call(A, 20, 10);
    把最后一个call执行
    this => B.call.call(call方法) , context => A, params => [20, 10]
    A.xxx = call方法
    A.xxx(20, 10) 再次让call方法执行
    this => A, context => new Number(20) , params => [10]
      (20).xxx = A
        (20).xxx(10) 执行的是A,A中的this是(20) ,传参10
          => NaN  undefined
    
    
    // 我的解析
    /* 
    1、B.call.call.call.call.call: B的作用,只是作为Function的实例,最终找到Function原型上的call
    2、此时,ctx: 就是A, this: 就是call,  通过手写call函数,可知call内部给A添加了唯一的属性xxx,并让A[xxx] = call,  
    3、然后重新执行call, 即上面的A[xxx](20, 10) =  call(20, 10),  A[xxx](20, 10)可以看做A.xxx(20, 10),此时this: 就是A, ctx: 就是new Number(20), (20).xxx = A(10),  执行A函数,传递10, A中的this是new Number(20), => A中的x=10, y=undefined, this=new Number(20) => 输出结果 就是NaN  undefined 
    */
    
    -----------------------------------
    
    
    Function.prototype.call(A, 60, 50);
    把call执行
    this => Function.prototype  context => A  params => [60, 50]
    A.xxx = Function.prototype
    A.xxx(60, 50)  把Function.prototype执行,它中的this是A,传参60 / 50
    => Function.prototype匿名空函数,执行啥事都不干
    
    
    -----------------------------------
    
    
    Function.prototype.call.call.call(A, 80, 70);
    最后一个call执行
    this => Function.prototype.call.call(最终还是call方法)  context => A  params => [80, 70]
    ...
     => NaN undefined
    

    /* call的作用:改变函数中this指向的 */
    Function.prototype.call = function call(context, ...params) {
      // this:就是调用call的函数, context:就是第一个参数, params:就是[形参集合]
    
      // (1)undefined == null 是true;(2)条件成立,context就是window;条件不成立,context就是传进来的值;(3)这里就是处理形参context的值为ndefined、null的情况。
      context == null ? context = window : null;
      // 只有引用数据类型值才能设置对应的属性
      let contextType = typeof context;
      if (!/^(object|function)$/.test(contextType)) {
        // 不是引用类型我们需要把其变为引用类型
        if (/^(symbol|bigint)$/.test(contextType)) {
          // symbol、bigint:基于Object创建对象值
          context = Object(context);
        } else {
          // 其余的可以基于new它的构造函数创建 
          // 【数值、字符串、布尔也可以用Object()转为对象:Object(11):Object(11); Object('aa'):String {"aa"};Object(true):Boolean {true}】
          context = new context.constructor(context);
        }
      }
      // 设置一个唯一的属性名 
      let key = Symbol('key'),
        result;
      // 给当前设置属性, 属性值是要执行的函数
      context[key] = this;
      // 让函数执行, 此时函数中的this => context 【context[key]:成员访问,this指向context。】
      result = context[key](...params);
      delete context[key]; // 用完移除
      return result;
    };
    
    let obj = {
      name: '哈哈'
    };
    
    function func(x, y) {
      console.log(this, x + y);
    }
    
    
    // obj.func(); // => Uncaught TypeError: obj.func is not a function
    // 自己处理:obj.xxx=func  只要让obj.xxx执行,也就相当于把func执行,但是此时方法中的this一定是obj了
    
    //  => func基于原型链查找机制,找到Function.prototype.call方法,把call方法执行
    //  => 在call方法内部执行中,才是把func执行,并且让里面的this变为obj,并且把10、20传递给func
    func.call('xxx', 10, 20);
    

    • this:
      全局上下文中的this是window;
      块级上下文中没有自己的this,它的this是继承所在上下文中的this的;【和箭头函数类似。】
      在函数的私有上下文中,this的情况会多种多样,也是接下来重点研究的.
      *
    • this不是执行上下文(EC才是执行上下文),this是执行主体
    • 例如:刘德华拿着加了五个鸡蛋的鸡蛋灌饼去北京大饭店吃早餐(事情本身是吃早餐,刘德华吃早餐,这件事情的主体是刘德华【this】,在北京饭店吃,北京饭店是事情发生所在的上下文【EC】)
      *
      *
    • 如何区分执行主体?
      1. 事件绑定:给元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的this是当前元素本身(特殊:IE6~8中基于attachEvent方法实现的DOM2事件绑定,事件触发,方法中的this是window,而不是元素本身)。
      1. 普通方法执行(包含自执行函数执行、普通函数执行、对象成员访问调取方法执行等):只需要看函数执行的时候,方法名前面是否有“点”,有“点”,“点”前面是谁,this就是谁,没有“点”,this就是window[非严格模式]/undefined[严格模式]。
      1. 构造函数执行(NEW XXX):构造函数体中的this是当前类的实例。
      1. ES6中提供了ARROW FUNCTION(箭头函数): 箭头函数没有自己的this,它的this是继承所在上下文中的this。
      1. 可以基于call/APPLY/BIND等方式,强制手动改变函数中的this指向:这三种模式是很直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)。

  • 相关阅读:
    links[v1]
    WebSocket handshake: Unexpected response code: 404
    Spring mvc 启动 和 请求分发
    匹配括号
    js parseFloat 精度问题
    遍历查找跳格子逻辑
    Generic type test java
    java高效判断素数
    从数组中取3个数全排列
    vue-cli
  • 原文地址:https://www.cnblogs.com/jianjie/p/13212422.html
Copyright © 2011-2022 走看看