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设置一个属性:属性名尽可能保持唯一, 避免我们自己设置的属性修改默认对象中的结构【即context中原本存在某个属性n,然后我们自己设置的属性和原有属性n重名】, 可以基于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指向:这三种模式是很直接很暴力的(前三种情况在使用这三个方法的情况后,都以手动改变的为主)。

posted on 2020-06-30 10:42  冲啊!  阅读(152)  评论(0编辑  收藏  举报

导航