call、apply和bind的实现

call方法

基础版, 只能修改指向,不能传参

Function.prototype.myCall = function(context) {
    // 获取调用者,这里为bar
    context.fn = this;
    // 运行函数
    context.fn();
    // 删除缓存
    delete context.fn;
}

let foo = {
    value: 1
}

function bar() {
    console.log(this.value);
}

bar.myCall(foo);
// 1

eval版本

Function.prototype.myCall = function(context) {
    // 获取调用者,这里为sayHi
    // 将无调用者的情况转换为有调用者的情况
    // 有调用者那么函数内部的this就指向调用者
    context.fn = this;
    var len = arguments.length;
    // 保存传递的参数
    var args = Array(len - 1);
    for (var i = 1; i < len; i++) {
        // 这里只是把获取参数的字符串拼接了,供eval调用
        args[i - 1] = 'arguments[' + i + ']';
    }
    // 不直接调用而用eval是因为参数个数不一定
    // 在这里相当于是在执行context.fn(arguments[1],arguments[2]);
    eval('context.fn('+ args +')');
    delete context.fn;
}

let person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex)
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male

ES6 版本

Function.prototype.myCall = function(context) {
    context.fn = this;
    // 将arguments转换为数组并调用slice方法去除第一个参数
    // 再使用...将数组打散
    context.fn(...Array.from(arguments).slice(1));
    delete context.fn;
}

let person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male

ES6 升级版

Function.prototype.myCall = function(context, ...args) {
    context.fn = this;
    // 相比使用arguments,这里只是用了ES6的剩余参数
    context.fn(...args);
    delete context.fn;
}

let person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male

最终版

之前的版本如果传入的对象原本的fn属性或方法会被覆盖,然后被删除;而且传入第一个参数如果不是对象,会报错,所以还需要一些容错处理

// 保存一个全局变量作为默认值
const root = this;

Function.prototype.myCall = function(context, ...args) {
    if (typeof context === 'object') {
        // 如果参数是null,使用全局变量
        context = context || root;
    } else {
        // 参数不是对象的创建一个空对象
        context = Object.create(null);
    }
    // 使用Symbol创建唯一值作为函数名
    let fn = Symbol();
    context[fn] = this;
    context[fn](...args);
    delete context[fn];
}

let person = {
    name: 'Wango',
    fn: function() {
        console.log(this.name);
    }
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male
sayHi.myCall(null, 24, 'male');
// undefined 24 male
sayHi.myCall(123, 24, 'male');
// undefined 24 male
// 原函数不受影响
person.fn();
// Wango

call的实现最核心的部分就是将没有调用者的情况转换为有调用者的情况,函数内部的this自然就指向调用者

apply方法

apply的实现思路和call方法是一样的,只是只接收两个参数,第二个参数为类数组

const root = this;

Function.prototype.myApply = function(context, arg) {
    if (typeof context === 'object') {
        context = context || root;
    } else {
        context = Object.create(null);
    }

    const fn = Symbol();
    context[fn] = this;
    context[fn](...arg);
    delete context[fn];
}

let person = {
    name: 'Wango',
    fn: function() {
        console.log(this.name);
    }
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myApply(person, [24, 'male']);
// Wango 24 male

bind方法

// bind 改变this指向但是不立即执行函数,而是返回一个绑定了this的函数

// bind方法在绑定this指向的同时也可以传递参数
Function.prototype.myBind = function(context, ...innerArgs) {
    // 不需要对参数类型进行判断,后边调用call方法时会进行处理
    const fn = this;
    // 不执行函数,而是返回一个函数等待调用
    return function(...finalArgs) {
        // 通过已经实现的call方法实现对this指向的改变,并传入参数执行
        return fn.call(context, ...innerArgs, ...finalArgs);
    }
}

const person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

const personSayHi_1 = sayHi.myBind(person, 24, 'male');
personSayHi_1();
// Wango 24 male
const personSayHi_2 = sayHi.myBind(person);
personSayHi_2(24, 'male');
// Wango 24 male

bind方法的核心在于在返回的函数中调用call方法

参考网址:call、apply和bind的实现

posted @ 2021-02-02 16:21  LiuWango  阅读(22)  评论(0编辑  收藏