这三个都是函数原型中的方法,用来改变某一个函数中 this 关键字指向的。

1. call

语法:[fn].call([this], [param],...)

  • 第一个参数都是改变 this 指向的对象
  • 后面的参数是传入该函数中的所有参数
  1. f1.call(obj, a, b, c...); //=> a, b, c... 是传入的参数

call 中不传值,或者传入 nullundefined,在非严格模式下,this 会被改为 window。在严格模式下,传入什么就是什么,不传就是 undefined

区分:

  • fn.call:当前实例(函数 fn)通过原型链的查找机制,找到 Function.prototype 上的 call 方法
  • fn.call():把找到的 call 方法执行

call 方法执行的时候,内部处理了一些事情:

  • 首先将要操作函数中的 this 变为传入的第一个实参值
  • 把第二个即以后的实参获取到
  • 把要操作的函数执行,并把第二个以后传入的实参传给函数

理解 call

  1. Function.prototype.call = function() {
  2. let param1 = arguments[0],
  3. paramOther = []; //=> 获取剩余的参数
  4. //=> this:fn 当前要操作的实例
  5. //=> 把 fn 中的 this 关键字修改为 param1,把 this 中的 this 关键字修改为 param1
  6. //=> 把 fn 执行,把 paramOther 传入 fn
  7. //=> this(paramOther)
  8. if (obj === undefined || obj === null) {
  9. this();
  10. }else{
  11. this.this = obj;
  12. this(paramOther);
  13. }
  14. };
  15. //=> ES6
  16. Function.prototype.call = function (...arg) {
  17. var obj = arg.shift();
  18. if(obj===undefined || obj===null){
  19. this(...arg);
  20. }else{
  21. this.this = obj;
  22. this(...arg);
  23. }
  24. };

思考:怎么通过 callthis 执行?

  1. function f1() {
  2. console.log(1);
  3. }
  4. function f2() {
  5. console.log(2);
  6. }
  7. f2.call();
  8. //=> call 执行时,call 里面的 this 本身就指向 f2
  9. //=> 让 this 执行,也就是让 f2 执行
  10. f2.call(f1); //=>2 执行的是 f2
  11. //=> 让 call 中的 this(f2) 中的 this 改成 f1,然后让 call 中的 this 执行,就是让 f2 执行
  12. f2.call.call(f1);
  13. //=> 执行的是第二个 call,把 f2.call 中的 this(本来是 f2) 指向 f1,然后再执行 this,也就是执行 f2.call
  14. //=> 执行 f2.call,没有传参,就只是执行 this,而这个 this 在前面已经改为了 f1,所以执行 f1
  15. f2.call.call.call(f1);
  16. //=> 后面再多的 call 都已经没有意义,因为倒数第二个 call 中的 this(本来是f2.call) 指向了 f1,那么就是执行 f1,完全不用管前面有多少 call

2. apply

applycall 基本上一模一样,唯一区别在于传参方式。

  1. f2.apply(obj,xxx); //=> xxx 是一个数组或者类数组,是传入的参数组成的

apply 把需要传递的参数放到一个数组(或类数组)中传递进去,虽然写的是数组,但是也是相当于一个个传递给前面的函数。

3. bind

bind 语法和 call 一模一样,唯一的区别在于立即执行还是等待执行。

  1. var f2 = f1.bind(obj,1,2,3);
  2. f2(); //=> 里面的 this 绑定为了 obj
  • bind 改变前面函数中的 this,此时函数并没有执行,而是作为返回值赋给一个变量,在需要的时候执行。
  • 返回的函数执行时,再传参数,会跟 bind 传的参数(除了第一个参数)结合起来
  • bind 返回的函数与原先的函数不再是同一个函数 fn.bind(this) === fn //=> false

bind 不兼容 IE6~8

理解 bind

  1. //=> 执行 bind 形成一个不销毁的作用域,返回一个新的匿名函数(每一次返回的都不一样:不是相同的堆内存)
  2. // [存储的内容]
  3. // THIS: FN(当前要处理的函数)
  4. // CONTEXT: OBJ(需要把 FN 中的 THIS 改变为这个对象)
  5. // ARG: 需要传递给 FN 的实参(数组)
  6. function myBind() {
  7. var _this = arguments[0];
  8. var that = this;
  9. var ary = [];
  10. for (var i = 1; i < arguments.length; i++) {
  11. ary.push(arguments[i])
  12. }
  13. return function() {
  14. var arr = [];
  15. for (var i = 0; i < ary.length; i++) {
  16. arr.push(arguments[i]);
  17. }
  18. that.apply(_this, ary.concat(arr))
  19. }
  20. }
  21. //=> ES6
  22. function myBind(context, ...arg) {
  23. return (...arg2) => {
  24. this.apply(context, arg.concat(arg2));
  25. }
  26. }
  27. Function.prototype.myBind = myBind;

4. 应用

需求一:获取数组中的最大值

  1. //=> 给数组先排序(从大到小),再获取第一项
  2. let ary = [12, 23,78,34,56];
  3. let max = ary.sort(function(a,b) {
  4. return b - a;
  5. })[0];
  6. //=> 假设法:假设第一个值是最大值,依次遍历数组中后面的每一项,与 max 进行比较,如果比假设的值要大,把当前项赋给 max
  7. let ary = [12, 23,78,34,56];
  8. let max = ary[0];
  9. for (let i = 1; i < ary.length; i++) {
  10. max = max < ary[i] ? ary[i] : max;
  11. }
  12. //=> 利用 eval 和 Math.max
  13. var arr = [12, 23,78,34,56];
  14. var str = arr.toString();
  15. var max = eval('Math.max(' + str + ')');
  16. max //=> 78
  17. //=> 括号表达式(小括号的应用)
  18. //=> 用小括号包起来,里面有很多项,每一项用逗号隔开,最后只获得最后一项的内容(但是会把其它内容的项也都过一遍,就是当作普通代码执行一遍)
  19. (function(){
  20. console.log(1);
  21. },
  22. function(){
  23. console.log(2);
  24. })(); //=> 2
  25. let a = 1 === 1?(12, 23, 14):null; //=> a=14
  26. //=> 不建议过多使用括号表达式,因为会改变 this
  27. let fn = function() {console.log(this);}
  28. let obj = {fn: fn};
  29. (fn, obj.fn)(); //=> 执行的是第二个 obj.fn,但是方法中的 this 是 window 而不是 obj
  30. (obj.fn)(); //=> this: obj
  31. //=> 利用 apply,把 arr 中的值全部变成参数传入,因为 Math.max 参数必须一个个传入
  32. var max = Math.max.apply(Math, arr)
  33. //=> ES6 扩展操作符
  34. var max = Math.max(...arr);

需求二:去除最大值,去除最小值,然后求平均数

  1. arr.sort((a,b) => a - b); //=> sort 里面这个函数实际上是在全局定义的,只是在 sort 函数内部执行
  2. // 相当于 function f(){}; arr.sort(f)
  3. arr.shift();
  4. arr.pop();
  5. var num = eval(arr.join('+')) / arr.length;
  6. //=> 思路:
  7. //=> 1. ES6扩展操作符
  8. //=> 2. 通过 for 循环 将 arguments 转化为数组,再使用数组中的方法
  9. //=> 3. 改变 arguments 的 __proto__ 为 Array.prototype,然后使用数组方法
  10. //=> 4. 通过 call 或 apply 将类数组转化为数组,然后利用数组的方法
  11. function f(...arg) {
  12. arg.sort((a,b) => a - b);
  13. arg.shift();
  14. arg.pop();
  15. return eval(arg.join('+')) / arg.length
  16. }
  17. function f2() {
  18. var arr = [];
  19. for (let i = 0; i < arguments.length; i++) {
  20. arr.push(arguments[i]);
  21. }
  22. arg.sort((a,b) => a - b);
  23. arg.shift();
  24. arg.pop();
  25. return eval(arg.join('+')) / arg.length
  26. }
  27. function f3() {
  28. arguments.__proto__ = Array.prototype;
  29. arguments.sort((a,b)=>a-b).shift();
  30. arguments.pop();
  31. return eval(arguments.join('+')) / arguments.length;
  32. }
  33. function f4() {
  34. var arr = [].slice.call(arguments);
  35. //=> 原因在于 slice 内部实现只使用了 length 和索引进行循环,所以改变成类数组之后,也可以使用
  36. }

类数组调用数组中的方法
先去通过数组找到对应的方法,然后用 call / apply 先改变方法里边的 this 指向,然后执行这个方法

数组中有些方法可以用,有些方法不能用。
原因在于,有些方法内部只操作数组的索引和 length 实现,就可以通过类数组调用,而其他的对象就不可以
有些方法不是通过这些实现的,同样也不可以。
如:concat 也可以克隆数组,但是不可以用于类数组
sort 也可以

需求三:改变事件函数中的 this

  1. function fn() {
  2. console.log(this);
  3. }
  4. let obj = {name:"obj"};
  5. document.onclick = fn; //=> 把 fn 绑定给点击事件,点击的时候执行 fn
  6. document.onclick = fn(); //=> 在绑定的时候,先把 fn 执行,把执行的返回值(undefined)绑定给事件,当点击的时候执行的是 undefined
  7. //=> 需求:点击的时候执行 fn,让 fn 中的 this 是 obj
  8. document.onclick = fn.call(this); //=> 虽然 this 确实改为了 obj,但是绑定的时候就把 fn 执行了,(call 是立即执行的)点击的时候执行的是 fn 的返回值
  9. document.onclick = fn.bind(obj); //=> bind 属于把 fn 中的 this 预处理为 obj,此时 fn 没有执行,当点击的时候才会把 fn 执行

需求四:给事件函数传递参数

在事件处理程序中,我们没有办法在执行的时候传递参数,因为执行的时候是不可控的。

而基于 bind 可以预先处理 this,还可以给函数预先传递参数,还可以把事件对象等信息传递给函数。

  1. function fn(x, y, e) {
  2. console.log(this, x, y);
  3. }
  4. let obj = {name: 'aa'}
  5. document.body.onclick = fn.bind(obj, 10, 20);
  6. //=> {name: 'aa'}, 10, 20

返回的函数执行时,再传参数,会跟 bind 传的参数(除了第一个参数)结合起来。也就是说,这里 bind 传入了两个参数 10 20,由 x/y 接受,而 e 是浏览器在执行的时候自动传入的参数,会与前面预先传入的参数结合起来,总是作为最后一个参数。

实际上,可以使用一个匿名函数包裹,里面使用 call 执行 fn,这就是 bind 的原理

  1. document.body.onclick = function(e) {
  2. fn.call(obj, 10, 20, e);
  3. }