call、bind、apply的实现

时间:2021-03-17 14:28:08   收藏:0   阅读:0

call和apply的应用场景:

  1. 判断数据类型:

Object.prototype.toString用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:

function isType(data, type) {
    const typeObj = {
        ‘[object String]‘: ‘string‘,
        ‘[object Number]‘: ‘number‘,
        ‘[object Boolean]‘: ‘boolean‘,
        ‘[object Null]‘: ‘null‘,
        ‘[object Undefined]‘: ‘undefined‘,
        ‘[object Object]‘: ‘object‘,
        ‘[object Array]‘: ‘array‘,
        ‘[object Function]‘: ‘function‘,
        ‘[object Date]‘: ‘date‘, // Object.prototype.toString.call(new Date())
        ‘[object RegExp]‘: ‘regExp‘,
        ‘[object Map]‘: ‘map‘,
        ‘[object Set]‘: ‘set‘,
        ‘[object HTMLDivElement]‘: ‘dom‘, // document.querySelector(‘#app‘)
        ‘[object WeakMap]‘: ‘weakMap‘,
        ‘[object Window]‘: ‘window‘,  // Object.prototype.toString.call(window)
        ‘[object Error]‘: ‘error‘, // new Error(‘1‘)
        ‘[object Arguments]‘: ‘arguments‘,
    }
    let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
    let typeName = typeObj[name] || ‘未知类型‘ // 匹配数据类型
    return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(
    isType({}, ‘object‘), // true
    isType([], ‘array‘), // true
    isType(new Date(), ‘object‘), // false
    isType(new Date(), ‘date‘), // true
)
复制代码
  1. 类数组借用数组的方法:

类数组因为不是真正的数组所有没有数组类型上自带的种种方法,所以我们需要去借用数组的方法。

比如借用数组的push方法:

var arrayLike = {
  0: ‘OB‘,
  1: ‘Koro1‘,
  length: 2
}
Array.prototype.push.call(arrayLike, ‘添加元素1‘, ‘添加元素2‘);
console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}
复制代码
  1. apply获取数组最大值最小值:

apply直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.maxMath.min来获取数组的最大值/最小值:

const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
复制代码
  1. 继承

ES5的继承也都是通过借用父类的构造方法来实现父类方法/属性的继承:

// 父类
function supFather(name) {
    this.name = name;
    this.colors = [‘red‘, ‘blue‘, ‘green‘]; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, ‘age‘);
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, ‘foo‘);
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push(‘black‘)
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}

call/apply与bind的区别

执行

返回值:

call的实现

思路

  1. 根据call的规则设置上下文对象,也就是this的指向。
  2. 通过设置context的属性,将函数的this指向隐式绑定到context上
  3. 通过隐式绑定执行函数并传递参数。
  4. 删除临时属性,返回函数执行结果
// es6的语法:...arr
Function.prototype.newCall = function(context, ...arr) {
    if(context === null || context === undefined) {
        context = window;
    }else {
        context = Object(context);
    }
    // 这里要是不用考虑兼容性 可以使用symbol 创建一个独一无二的属性 不会产生跟上下文原属性冲突的风险
    // 也可以尽量使用特殊一点的属性名
    obj.p = this;

    let result = context.p(...arr);
    delete context.p;

    return result;
}

判断函数的上下文对象:

很多人判断函数上下文对象,只是简单的以context是否为false来判断,比如:

// 判断函数上下文绑定到`window`不够严谨
context = context ? Object(context) : window; 
context = context || window; 

经过测试,以下三种为false的情况,函数的上下文对象都会绑定到window上:

// 网上的其他绑定函数上下文对象的方案: context = context || window; 
function handle(...params) {
    this.test = ‘handle‘
    console.log(‘params‘, this, ...params) // do some thing
}
handle.elseCall(‘‘) // window
handle.elseCall(0) // window
handle.elseCall(false) // window

call则将函数的上下文对象会绑定到这些原始值的实例对象上:

技术图片

所以正确的解决方案,应该是像我上面那么做:

// 正确判断函数上下文对象
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }

apply的实现

思路

  1. 传递给函数的参数处理,不太一样,其他部分跟call一样。
  2. apply接受第二个参数为类数组对象, 这里用了JavaScript权威指南中判断是否为类数组对象的方法。
Function.prototype.newApply = function(context) {
    if(context === null || context === undefined) {
        context = window;
    }else {
        context = Object(context);
    }

    // js 权威指南判断是否为类数组对象
    function isArrayLike(o) {
        if(o &&                                     // o不是null、undefined等
            typeof o === ‘object‘ &&                // o是对象
            isFinite(o.length) &&                   // o.length是有限数值
            o.length >= 0 &&                        // o.length为非负值
            o.length === Math.floor(o.length) &&    // o.length是整数
            o.length < 4294967296) {                // o.length < 2^32
            return true;
        }
        return false;
    }

    context.p = this;
    let args = arguments[1];
    let result;

    if(args) {
        if(!Array.isArray(args) && !isArrayLike(args)) {
            throw new TypeError(‘myApply 第二个参数不为数组并且不为类数组对象抛出错误‘);
        }else {
            args = Array.from(args);  //转为数组
            result = context.p(...args);
        }
    }else {
        result = context.p();
    }

    delete context.p;

    return result;
}

bind的实现

  1. 先理解什么是函数的柯里化,bind中也使用了这种方法

    柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

    换句话说,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

  2. 使用bind创建出来的新函数可以使用new,并且当new完之后 新创建出来的实例要继承原函数的原型,并且this指向新创建出来的实例对象(绑定的this将失效)

  //使用圣杯模式继承
Function.prototype.newBind = function(obj) {
    let that = this;
    let arr = Array.prototype.slice.call(arguments, 1);
    // let arr = [...arguments].slice(1);  es6的写法
    let o = function(){};

    let newf = function() {
        let arr2 = Array.prototype.slice.call(arguments);
        let arrSum = arr.concat(arr2);

        //上面两句可写为
        // let arrSum = arr.concat([...arguments]);

        //this是否是newf的实例 也就是返回的newf是否通过new调用
        if(this instanceof newf) {
            that.apply(this, arrSum);
        }else {
            that.apply(obj, arrSum);
        }
    };

    // 原型链继承
    o.prototype = that.prototype;
    newf.prototype = new o;

    return newf;
}

function person(a, b, c) {
    console.log(this.name);
    console.log(a, b, c)
}
person.prototype.collection = ‘收藏‘;

var egg = {name: ‘haha‘}

let bibi = person.newBind(egg, ‘点赞‘, ‘投币‘);
let b = new bibi(‘充电‘);
// bibi这个函数的实例可以用到person原型对象的属性
console.log(b.collection);

要注意arguments其实是对象,不是数组,因此不能直接用数组的切割方法,需要利用call/apply方法把slice切割方法赋给arguments对象。

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!