记录一下call apply bind

90 阅读4分钟

call的语法

function.call(thisArg, arg1, arg2, ...),第一个参数是可选的在,function 函数运行时使用的 this 值。可选是因为假如没有指定函数的使用的this的时候,就为默认为运行环境的window(浏览器)或者node(global)。第二个参数为指定的参数列表,传入调用call的函数中。

实现代码

// 任何函数都可以调用call方法所以需要把call方法写在Function.prototype上
// 方法传入两个参数,第一参数为调用函数的this,第二个为调用这个函数传入的参数

Function.prototype.MyCall = function (thisArg = window, ...args) {
    const prop = Symbol()
    // 处理传入进来的函数,将它作为thisArg的一个属性
    thisArg[prop] = this
    let res = thisArg[prop](...args)

    delete thisArg[prop]
    return res
}

function fn(a, b) {
    console.log(a, b)
    console.log('this:', this)
    return a + b
}

const obj = {
    name: 'qingcc'
}
const res1 = fn.MyCall(obj,10,20)
console.log(res3)

// 构造函数
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

核心思路:

  • 为传入的上下文对象扩展一个属性并将这个属性指向调用的函数
  • 将上下文之外的所有参数传入这个新的属性
  • 调用这个添加的属性
  • 删除新属性
  • 返回得到的结果

实现的细节

  • ...args收集多余的参数并传入函数
  • 使用Symbol为上下文添加新属性,Symbol是独一无二的不会影响上下文原来的属性值

apply的语法

function.apply(thisArg,argsArr),其语法几乎与call是一样的,只不过其第二参数为数组或者是类数组对象,而call的第二个参数是一个参数列表。

实现代码

// ----------------------------------------------------------------------手写apply的实现
function testApply(a = 12, b = 10) {
    console.log('this:', this)
    console.log(a, b)
    return a + b
}

Function.prototype.MyApply = function (obj = window, arr) {
    obj = (obj !== undefined && obj !== null) ? Object(obj) : window
    const prop = Symbol()
    obj[prop] = this
    const argArr = arr || []
    const res = obj[prop](...argArr)
    delete obj[prop]
    return res
}

const res = testApply.MyApply({ name: 'string', age: 18 }, [1, 2])
console.log(res)

bind的语法

bind和call与apply一样都会改变函数this的指向,但是bind不会执行而是会返回一个函数,这个函数的this指向就是apply绑定的对象,其内部可以通过apply绑定函数的this,这个绑定的this无法通过apply和bind进行修改。

函数柯里化的介绍:

//函数柯里化,函数调用的时候先传递一部分参数进行调用,函数返回新函数再处理剩下的参数
function fn(x, y) {
    return function (y) {
        console.log(x + y);
    };
};
var fn_ = fn(0); // 先传递一个x参数
fn_(1); // 2 此时返回了一个新的fn_函数,在传入一个参数y处理剩余的参数
fn(1)(1) // 2

可以看出上边先给fn传递了一个参数,然后返回了一个fn_函数,然后给fn_又传递了一个参数,这样的话就相当于先给函数传递一部分参数调用,此例先传递了x进行参数调用,然后再给函数返回值传递参数y,新函数再处理剩余部分的参数。

版本一

版本一:实现了改变this指向的功能
Function.prototype.MyBind = function (obj) {
    // 提前保存调用bind的函数
    const self = this
    return function () {
        self.call(obj)
    }
}

function testBind(a = 1, b = 2) {
    console.log('this:', this.name);
    return a + b
}

const bindObj = { name: 'qing', age: 18 };
const bindFun = testBind.MyBind(bindObj)
bindFun()

版本二

这个版本实现的bind改变了this的指向,但是可见其并不支持传参,修改之后使其支持参数传递的功能。

Function.prototype.MyBind = function (obj) {
    const args = Array.prototype.splice.call(arguments,1)
    const self = this
    return function () {
        self.call(obj,args)
    }
}

但是这个版本只是支持简单的参数传递,不支持先传递一部分参数,然后再传递一部分函数也就是函数柯里化。

Function.prototype.MyBind = function (obj) {
    // 获取MyBind传入的参数,这个类数组的第一位是this所以索引从1开始
    const args = Array.prototype.slice.call(arguments, 1)
    const self = this
    return function () {
        // 二次调的时候同样抓取参数
        const params = Array.prototype.slice.call(arguments)
        return self.apply(obj, args.concat(params))
    }
}

那么我们测试一下

function testBind(a, b) {
    console.log('this:', this.name);
    console.log(a+b)
    return a + b
}

const bindObj = { name: 'qing', age: 18 };
const bindFun = testBind.MyBind(bindObj,1)
// 这边加了一个对象的话会出现错误,因为调用返回的bind函数(MyBind)在取参数的时候把arguments和之前的参数都合并了,arguments第一项为这个对象。
console.log(bindFun({name:'change'},2));

这时候这个函数的this既不会失去绑定也完成参数调用以及简单的函数柯里化。还有一个问题是通过bind返回的函数仍然能够通过new 构造,这样通过this绑定的参数会自动失效,且返回的实例还是会继承构造函数的构造器属性与原型属性,并且能正常接收参数。