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指向:这三种模式是很直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)。

  • 相关阅读:
    设计模式之工厂模式-抽象工厂(02)
    1036 跟奥巴马一起编程 (15 分)
    1034 有理数四则运算 (20 分)
    1033 旧键盘打字 (20 分)
    1031 查验身份证 (15 分)
    大学排名定向爬虫
    1030 完美数列 (25 分)二分
    1029 旧键盘 (20 分)
    1028 人口普查 (20 分)
    1026 程序运行时间 (15 分)四舍五入
  • 原文地址:https://www.cnblogs.com/jianjie/p/13212422.html
Copyright © 2011-2022 走看看