call/apply和bind的使用和原理并手写实现

我心飞翔 分类:javascript

关于 this

承接上一篇文章JS中几种最基本的this情况分析,加入面向对象的情况,总结一下

关于 this 的几种情况

  1. 给当前元素的某个事件行为绑定方法,方法中的 this 是当前元素本身(排除:DOM2在IE6~8中基于attachEvent进行事件绑定,这样处理方法中的 this ->window)
  2. 方法执行,看方法前面是否有“点”,有“点”,“点”前面是谁 this 就是谁,没有“点”, this 就是window(严格模式下是undefiend
    • 自执行函数中的 this 一般是window/undefined
    • 回调函数中的 this 一般是window/undefined(当然某些方法中会做一些特殊的处理)
    • 括号表达式有特殊性
    • ......
  3. new 执行方式构造函数,构造函数体中的 this 是当前类的实例
  4. 箭头函数中没有 this (类似的还有块级上下文),所以无论怎样去执行,怎样去修改,都没有用,如果函数中出现 this ,一定是其所在的上级上下文中的 this
  5. Function.prototype 提供了三个方法: call / apply / bind ,这三个方法都是用来强制改变函数中 this 指向的(每一个函数都 是Function 的实例,所以都可以调取这三个方法执行)
'use strict'
let obj = {
   name: 'ttt',
   fn() {
       let self = this;
       setTimeout(function () {
           console.log(this);// this -> window
           console.log(self);//self->obj
       }, 1000);

       setTimeout(() => {
           console.log(this);
           // this是上级上下文中的,也就是obj
       }, 1000);
   }
}
obj.fn();
 

call / apply 的使用和手写

call

想在fn执行的时候将其中的 this 转化为 obj ,并且传入其他参数

"use strict";
const fn = function fn(x,y){
   console.log(this.name,x+y);
};
let obj = {
   name: 'obj'
};
fn.call(obj,10,20)//'obj' 30
 

fn.call(obj,10,20) 运行如下:

fn 首先作为 Function 的实例,基于 __proto__ 找到 Function.prototype.call 方法,并且把找到的 call 方法执行

call 方法执行时的一些步骤:

  • call 中的 this -> fn
  • 第一个参数 context -> obj ,是未来要改变的 call 函数中的 this 指向,让其指向第一个参数 context
  • 剩余的参数 params -> [10,20] 存储的是未来要给函数传递的实参

call 方法执行的作用:帮助我们把 fncall 中的 this )执行,并且让方法中的 this 指向 objcontext ),顺便把 10/20params )传递给函数

注意点:

  1. 如果直接执行 fn.call() 一个值都不传的话, fn 中的 this 非严格模式下是 window ,严格模式下是 undefinedxundefinedyundefined

  2. 如果执行 fn.call(null/undefined) ,非严格模式下 thiswindow ,严格模式下是 null / undefined (即传什么就是什么) 。 xundefinedyundefined

总结:即严格模式下传入什么 this 就是什么,非严格模式下,传入 null 或者 undefined thiswindow

例如执行 fn.call(10, 20)this -> 10x -> 20y -> undefined

apply

applycall 只有一个区别: call 方法在设定给函数传递的实参信息的时候,是要求一个个传递实参值。但是 apply 是要求用户把所有需要传递的实参信息以数组/类数组进行管理的。虽然要求以数组方式传进去,但是内部最后处理的时候,和 call 一样,也是一项项的传递给函数的。

fn.apply(obj, [10, 20])
 

所以一个函数创建出来的时候,其中的 this 直到执行的时候才可以确定。这时中, this 在不同的环境可以自动关联的值,也是可以我们使用 call / apply / bind 手动关联的值。

应用场景

场景一:求最大值或最小值

求一个数组中的最大值或者最小值

let arr = [10, 14, 23, 34, 20, 13];
 
  1. 排序处理 时间复杂度稍微高一些( sort 内部处理的时间复杂度是 N^2,即遍历两次)

    console.log(arr.sort((a, b) => b - a)[0])
     
  2. 假设法 时间复杂度N,循环一次即可

    let max = arr[0],
        i = 1,
        len = arr.length,
        item;
    for (; i < len; i++) {
        item = arr[i];
        if (item > max) {
            max = item;
        }
    }
    console.log(max); 
     
  3. Math.max 获取最大值

console.log(Math.max(10, 14, 23, 34, 20, 13)); //=>34
 
console.log(Math.max(arr)); =>NaN
 

方法本身是传入的所有参数中的最大值,需要把比较的数字一项项的传递给 max 方法才可以

那么把数组中的每一项分别传递给 max 方法:

 let max = Math.max(...arr);
 console.log(max); =>34
 
let max = Math.max.apply(null, arr); 
console.log(max);// =>34
 

利用 apply 的机制。虽然传递给 apply 的是一个数组,但是 apply 内部会把数组中的每一项分别传递给对应的函数。而且 Math.max 中用不到 this ,所以 this 改成谁无所谓,就是占个位而已

场景二:任意数求和

任意数求和(不确定实参的个数,所以无法基于设定形参接受)
两种方法:

  • 剩余运算符 ES6
  • arguments ES3
  1. const sum = function sum(...params) {
        if (params.length === 0) return 0;
         //params -> 数组
        return params.reduce((total, item) => total + item, 0);
    }; 
     
  2. 把类数组转换为数组

    • params = Array.from(params); ES6+
    • params = [...params]; ES6+
    • ...

    那么如何使用es5以下方法将伪数组转换为数组?

    最简单的方法:手动一个个转换

     const sum = function sum() {
        let params = arguments; // params -> 类数组「不能直接使用数组的办法」
        if (params.length === 0) return 0;
        let i = 0,
            len = params.length,
            arr = [];
        for (; i < len; i++) {
            arr[arr.length] = params[i]; <==> arr.push(params[i]);
        }
        return arr.reduce((total, item) => total + item, 0);
    }; 
     

    借助 slice

    ary.slice()
    ary.slice(0)
     

    以上都为数组的克隆,把原始数组中的每一项都克隆一份给返回的新数组(浅克隆)

    解释一下为什么 [].slice.call(arguments) 会将伪数组集合变为真的数组

    MDN slice-polyfill

    1. 查看polyfill, slice 原理是用了 for...i 循环+往真是数组里循环存值,所以 [].slice.call(arguments) 会把polyfill代码中的 this 替换为 arguments ,并且循环,然后往真的数组里按序存值,最后返回真的数组

    2. 因为类数组的结果和数组非常的相似(都有下标+ length ),所以大部分操作数组的代码,也同样适用于类数组,在这个前提下,我们只需要把实现好的数组方法执行,让方法中的 this 变为类似组,就相当于类数组在操作这写代码,实现了类数组借用数组方法的目的,我们这种操作叫做“鸭子类型”

    模拟 slice 原理:

     Array.prototype._slice = function slice() {
         //模拟
         //this -> ary
        let i = 0,
            len = this.length,
            arr = [];
        for (; i < len; i++) {
            arr[arr.length] = this[i];
        }
        return arr;
    };
     

    把类数组转换为数组:如果我们能把 slice 执行,并且让 slice 中的 thisarguments ,这样就相当于在迭代 arguments 中的每一项,把每一项赋值给新的数组集合 -> 也就是把类数组转换为数组

    • 如何让 slice 执行 : 使用Array.prototype.slice() / [].slice() .....
    • 如何改变 slice 中的 this : 使用call/apply ->params = [].slice.call(params);
     const sum = function sum() {
        let params = arguments;
        if (params.length === 0) return 0;
        params = [].slice.call(params);
        return params.reduce((total, item) => total + item, 0);
    }; 
     

    或者不转换了,直接借用这种规则即可

    
     const sum = function sum() {
        let params = arguments;
        if (params.length === 0) return 0;
         //不转换了,直接借用即可
        return [].reduce.call(params, (total, item) => total + item, 0);
    }; 
     

    或者直接修改原型链指向,这样调用的时候,里面的 this 就指向了 arguments

    const sum = function sum() {
       let params = arguments;
       //params.__proto__ = Array.prototype; 修改原型链的指向  arguments可以直接使用数组原型上的任何方法
       //或者:
       params.reduce = Array.prototype.reduce;
       if (params.length === 0) return 0;
       return params.reduce((total, item) => total + item, 0);
    };
     

一个有趣的问题:

let obj = {
    2: 3, //1
    3: 4, //2
    length: 2, //从2->3,从3->4
    push: Array.prototype.push
};
obj.push(1)
obj.push(2)
console.log(obj[2])//1
console.log(obj[3])//2
console.log(obj.length)//4
 

原理是 push 中的 this 变为了 obj
那么push做了什么事?
手动模拟一下 push (push是内置的):

Array.prototype.push = function push(val) {
    //这些代码只是一些模拟
    // this -> 操作的数组
    this[this.length]=val;
    this.length++;
};
arr.push(100);
 

push 方法执行,让里面的 this -> obj 类似于

obj.push(1) => [].push.call(obj,1)

  1. obj[obj.length]=1 => obj[2]=1;
  2. obj.length++

obj.push(2)

  1. obj[obj.length]=2 => obj[3]=2
  2. obj.length++

手写 call / apply

用js模拟 call

面试题:完成change函数

~ function () {
    /* 内置CALL实现原理 */
    function change(context, ...params) {
       
    };
    Function.prototype.change = change;
}();
let obj = {
    name: 'Alibaba'
};
function func(x, y) {
    this.total = x + y;
    return this;
}
let res = func.change(obj, 100, 200);
console.log(res);
//res => {name:'Alibaba',total:300}
 

实际上就是内置 call 的实现原理

call 的核心原理很简单:

obj.func = func
console.log(obj.func(100,200))
delete obj.func
 

拿上面例子举例就是让 obj 中有一个 func 属性,然后执行 obj.func ,那么其中的 this 就自动指向了 obj ,最后再删除,打印如下( console.log 打印的是最后运行代码,即删除完 func 的对象)
image。png

~ function () {
    //在没有Symbol之前,我们可以用一个函数随机生成随机值来代替 Symbol('KEY')
    /*const createRandom = () => {
        let ran = Math.random() * new Date();
        return ran.toString(16).replace('.', '');
    };*/

    /* 内置CALL实现原理 */
    function change(context, ...params) {
        // this->要执行的函数func   context->要改变的函数中的this指向(当下例子中为obj) 
        // params->未来要传递给函数func的实参信息{数组} [100,200]
        // 临时设置的属性,不能和原始对象冲突,所以我们属性采用唯一值处理
        context == null ? context = window : null;
        if (!/^(object|function)$/.test(typeof context)) context = Object(context);
        let self = this,
            key = Symbol('KEY'),
            result;
        context[key] = self;
        result = context[key](...params);
        delete context[key];
        return result;
    };

    Function.prototype.change = change;
}();
 

change函数中

  • this ->要执行的函数 func
  • context ->要改变的函数中的 this 指向(当下例子中为 obj
  • params ->未来要传递给函数 func 的实参信息{数组} [100,200]

要注意的是:

  1. context 的要执行的那个属性是临时设置的属性,不能和原始对象冲突,所以我们属性采用唯一值 Symbol 处理。
  2. 传进来的 context 不一定是对象,只有对象设置属性才有用所以添加判断,如果传进来的值不是objec类型的,就要转换为对象 if (!/^(object|function)$/.test(typeof context)) context = Object(context)
  3. 当传递的是 null 或者 undefined 的时候,都应该将 this 指向 window 所以 : context == null ? context = window : null;

在没有 Symbol 之前,我们可以用一个函数随机生成 contextkey

模拟 apply

apply和call唯一的区别就是,apply传入得失参数数组,
所以将上面calll实现的方法去掉 ... 不使用扩展运算符即可

  function call(context, ...params) {}
 
  function apply(context, params) {}
 

伪数组转化为真数组方法

小tips:将获取的参数转化为真正数组的方法:


function toArray(...params) {
    return params;
}
 
function toArray() {
    // arguments 类数组集合
    // return [...arguments];
    // return Array.from(arguments);
    return [].slice.call(arguments);
}

 

bind 的使用与手写

举例子:

要求:

~function () {
    //bind方法在ie6-8中不兼容,接下来我们自己基于原生js实现这个方法
    function bind(context, ...params) {

    }

    Function.prototype.bind = bind;
}();
var obj = {name: 'ali'}

function func() {
    console.log(this, arguments)
    //当点击body 的时候,执行function方法,并且输出obj [100,200,MouseEvent事件对象]
}

document.body.onclick = func.bind(obj, 100, 200)
 

分析:

bind & call / apply 区别:

  • 都是为了改变函数中的 this 指向
  • call / apply :立即把函数执行
  • bind :不是立即把函数执行,只是预先把 this 和后期需要传递的参数存储起来(预处理思想 -> 柯理化函数)

bind 的原理:其实就是利用闭包的机制,把要执行的函数外面包裹一层函数,预先把 this 和后期需要传递的参数存储起来。即柯理化思想的运用

事件相关分析:

function func(ev){
    console.log(this,arguments)
}
document.body.onclick = func
 

浏览器在点击事件触发时,会自动绑定 this 为事件触发的DOM对象(这里是 body )并且会默认传入事件对象作为参数
image。png

假设我们用内置的 bind ,看看结果如何:

var obj = {name: 'ali'}

function func() {
    console.log(this, arguments)
}

document.body.onclick = func.bind(obj, 100, 200)
 

点击之后:

image。png
不仅把原来的参数穿进去,并且最后在末尾,还有传入的事件对象

题目最后目的:把 func 执行, this 改为 obj ,参数 100/200/ev 传递给他即可,如下:

document.body.onclick = function proxy(ev) {
    // 最后的目的:把func执行,this改为obj,参数100/200/ev传递给他即可
    func.call(obj, 100, 200, ev);
}; 
 

手写 bind

了解以上信息之后开始手写:

~ function () {
 
   /* 内置BIND的实现原理 */
   function bind(context, ...params) {
       // this->func  context->obj  params->[100,200]
       let self = this;
       return function proxy(...args) {
           // args->事件触发传递的信息,例如:[ev]
           params = params.concat(args);
           return self.call(context, ...params);
       };
   };

   Function.prototype.bind = bind;
}();
 

回复

我来回复
  • 暂无回复内容