JavaScript 手写实现call、apply、bind

66 阅读3分钟

call

// 使用原型链往 Function 构造函数上追加一个自定义方法(这样每个 function 中就都有这个自定义方法了)
Function.prototype.myCall = function(thisArg, ...restParams) {
    // 判断使用时是否传递了 this 绑定 如果没有就默认绑定 window(这里使用 ?? 的原因是拦截 null and undefined)
    thisArg = thisArg ?? window;
    // 将结果转为一个对象(必须转因为非对象无非用 .)
    const self = Object(thisArg);
    // 保存函数(这里的 this 隐式绑定的是调用这个自定义方法的函数)
    self.fn = this;
    const result = self.fn(...restParams); // 调用函数并保存结果
    delete self.fn; // 调用之后删除多出的这个属性(因为js底层是c++解析的我们想用js模拟就必须这样写)
    // 返回结果(没有结果的话刚好就是undefined)
    return result;
};

function sum(num1, num2){
    console.log(this);
    
    return num1 + num2;
};

sum(); // window
sum.myCall(); // window

sum.myCall({}); // {}
sum.myCall(1) // 1

sum.myCall(null); // window
sum.myCall(undefined); // window

console.log(sum.myCall('...args', 18, 22)); // ...grgs -- 40

apply

// 因为 apply 传递过来的本身就是个数组所以这里不用用剩余参数...xxx 但是传递过去的时候还是需要扩展运算符...的所以为了判断不传参数的情况这里在不传时默认为空数组防止报错
Function.prototype.myApply = function(thisArg, arrParams = []) {
    thisArg = thisArg ?? window;
    
    const self = Object(thisArg);
    
    self.fn = this;
    const result = self.fn(...arrParams);
    delete self.fn;
    
    return result;
};

function sum(num1, num2) {
    console.log(this);
    
    return num1 + num2;
};

sum(); // window
sum.myApply(); // window

sum.myApply([]); // []
sum.myApply(false); // false

sum.myApply(null); // window
sum.myApply(undefined); // window

console.log(sum.myApply('args', [2, 8])); // args -- 10

bind

Function.prototype.myBind = function(thisArg, ...args) {
    thisArg = thisArg ?? window;
    
    const self = Object(thisArg);
    self.fn = this;
    
    return (...restParams) => {
        const result = self.fn(...[...args, ...restParams]);
        delete self.fn;
        
        return result;
    };
};

function sum(num1, num2) {
    console.log(this);
    
    return num1 + num2;
};

sum(); // window
sum.myBind()(); // window

const newSum = sum.myBind('test', 20, 30);
console.log(newSum()); // test -- 50

console.log(sum.myBind('苏苏', 20)(30)); // 苏苏 -- 50
console.log(sum.myBind('苏苏')(20, 30)); // 苏苏 -- 50

以上不考虑 edge case.

为什么不直接使用arguments? 
因为现在已经不推荐使用arguments
并且箭头函数中并没有arguments
你可能会说或会想:你骗人我log了箭头函数中明明是有arguments的
你错了!因为箭头函数没有自己的作用域当你log一个不存在的变量时就会去上层作用域找
所以本质上你log看见的并不是箭头函数自己的arguments

还有些人可能会说你这说的也不对啊我log直接就是报错的说没有arguments啊(上层也没有)
我告诉你因为你们运行js的环境不一样 node 环境中全局作用域是有arguments的而浏览器没有 

并且arguments得到的不是本质上的数组而是个对象数组
这有什么关系呢?
这关系可就大了代表你不能直接使用数组的方法而需要自己先做一遍处理
怎么处理呢?

one: 手动处理
Array.prototype.mySlice = function(start = 0, end) {
    // 这里的this就是绑定的数组
    const arr = this;
    // 判断是否传入截取位未传就到最后一位
    end = end || arr.length;
    
    const newArr = [];
    for(let i = start; i < end; i++) {
        newArr.push(arr[i]);
    };
    
    return newArr;
};
// 这里用的是立即执行函数(沙箱函数)
(function(){
    console.log(arguments);
    // 原型链方式调用
    console.log(Array.prototype.mySlice.apply(arguments));
    console.log(Array.prototype.mySlice.call(arguments, 1, 2));
    // 字面量方式调用
    console.log([].mySlice.call(arguments));
    console.log([].mySlice.apply(arguments, [1, 2]));
}(1,2,3,5,8,13,21));

two: 自动处理

(function(){
    console.log(arguments);
    // 扩展成数组(不管你是对象还是数组还是数组对象等牛马都是可以的)
    console.log([...arguments]);
    // 字面量方式调用提供好的slice方法(不是刚刚自己写的)
    console.log([].slice.call(arguments));
    // 用es6方法转
    console.log(Array.from(arguments));
}(1,2,3,5,8,13,21));

一个不小心又带大家实现了一个slice
但总之我想说的是以后开发中请不要使用arguments如有不确定参数的需求请使用 剩余参数fn(...xxx)
请记住箭头函数中没有 arguments 但是可以使用剩余参数(可能也会避免闭包)