Skip to content

如何去实现call、apply、bind函数? #5

@Checkson

Description

@Checkson

背景

相信,很多同学都有过和我同样的经历,在编写一个React组件的时候,常常要为某个监听事件的回调函数,绑定当前组件的上下文,形式大概如下:

import React, { Component } from 'react';

class MyComponent extends Component {
    constructor (props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick (e) {
         // dosomething...
    }
    render () {
        return (<button className="btn"
                        onClick={this.handleClick}>点我</button>);
    }
}

这里我们不讨论为什么React中的绑定事件的回调函数需要手动去绑定组件的上下文,有兴趣的同学可以自行搜索,或者点击这里。bind这个函数有什么大的魅力,能让我们在React开发中这么频繁地使用,背后到底做了什么?JavaScript类似bind的函数还有call和apply,那么它们又是如何去工作的,下面分享一下我自己的探索。

bind函数的实现

概念

bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。(摘自MDN)

好吧,MDN似乎解释得有点云里雾里的。用我的话形容就是bind方法能够改变函数的this指向,并返回一个新的函数。值得注意的是,bind方法是作用于函数的,在接收的参数列表中,默认将第一个参数作为this绑定的对象,之后的一序列参数将会在传递的实参前传入作为它的参数。

那么,自己要实现一个bind函数,首先要知道,bind函数是怎么用的。

示例

let foo = bar.bind(context, ...args);

给当初和我一样好奇的同学:你们看到很多示例代码中用到的foo、bar、baz等标识符,就好像学校里面老师给我们举例子中的张山、李四、王五,没有特别的意思,只是一种约定成俗的东西。

说明

  • context是指要绑定的上下文。
  • args是需要传递的参数。
  • bar是需要绑定context上下文的函数。
  • foo是存储bar绑定context上下文后返回的函数。
  • bind函数定义是在Function.prototype中,不太熟悉JavaScript原型链的同学,请点击这里

注意

这里的bind函数调用返回的是一个绑定context上下文的函数引用,这是区别于call和apply调用后返回函数运行后的返回值。那么,如果我们不传context,那么context默认是指向谁呢?

let foo = function () {
    console.log(this);
}
let bindFoo = foo.bind();

// chrome、firefox、ie
bindFoo();  // window
// node
bindFoo();  // global

可见,我们什么也不传给bind函数的时候,默认上下文是指向全局对象的。

实现

Function.prototype.bind = function (context) {
     // 判断bind方法是否作用在函数上
    if (typeof this !== 'function') {
        // 抛出异常
        throw new Error("bind方法只作用于函数对象");
    }
     // 检测传入要绑定的上下文
    let _context = context || (typeof window === 'undefined' ? global : window);
     // 保存当前的this上下文,此时this是指调用bind方法的函数,这里作为闭包,供后面调用
    let _this = this;
     // 保存参数,除了第一个参数,因为第一次参数要作为绑定的上下文
    let args = [...arguments].slice(1);
    // 返回新的函数
    return function F () {
        // 因为返回了一个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
            return new _this(...args, ...arguments)
        }
        // 为函数绑定新的上下文
        return _this.apply(_context, args.concat(...arguments))
    }
}

解析

  • 首先,从上面的代码可以看出,这个bind函数的定义是挂载在Function这个对象的原型链上的,利于重用。
  • 函数体里面第一句代码就是判断this这个引用是否是函数类型,例如foo.bind,条件判断中的this就是指foo这个引用。
  • _context是存储要绑定新的上下文。
  • _this是指向上面提到的foo,也就是指向调用bind方法的函数。
  • args是指处理第一个参数以后的所有参数,以数组形式存在。
  • 返回结果是一个新的函数,形成闭包,之前存储的变量,都可以在这个返回的新函数里面引用。
  • 这里返回的函数为什么不是匿名的呢?是因为后面有一处语句要判断,调用bind方法的函数是否是该返回函数的实例。也就是说,bind方法既然是返回一个新的函数,那么,我们可以把它当成构造函数使用,例如:
function Foo() {
    // pass
}
// 实例化
let instance = new Foo.bind(context, ...args);
// 等价于
let instance = new Foo(...args);
  • 有的同学就会疑惑了,需要绑定的context去哪里了呢?这个就涉及到各个方式绑定上下文的优先级了。new方法实例化绑定上下文的优先级最高,大于call和apply方法,还有bind方法。
  • 返回的新函数,最后还是返回了最初的函数引用调用了apply方法,其实我们可以看出,bind方法特性也称为函数的柯里化

call函数的实现

概念

call和apply都是为了解决改变 this 的指向,也就是改变函数的上下文,只是传参方式不一样。无论调用call还是apply方法,函数都会被立即执行。

示例

let context = {
    bar: 1
}

function foo (str) {
    console.log(this.bar);
    console.log(str);
}

foo.call(context, '你好,世界!');

输出

1
你好,世界!

说明

  • context也是指需要绑定的上下文,是call方法参数列表中的第一个,其后面所有参数都将作为实参传给调用call方法的函数,例如上面例子中的foo函数。
  • call方法的调用,会立即执行调用bind方法的函数。

思路

既然,call方法能改变函数的this指向,那么我们可以让需要绑定的上下文,可以执行这个函数即可。

实现

Function.prototype.call = function (context) {
    // 获取要绑定的上下文
    let _context = context || (typeof window === 'undefined' ? global : window);
    // 保存旧的fn属性
    let oldFn = _context.fn;
    // 给_context添加这个函数
    _context.fn = this;
    // 将 context 后面的参数截取出来
    let args = [...arguments].slice(1);
    // 调用函数,并保存返回结果
    let result = context.fn(...args)
    // 删除fn属性
    delete context.fn
    // 若旧的fn存在,则还原
    oldFn && (context.fn = oldFn);
    // 返回结果
    return result;
}

解析

这段代码理解起来并不难,但是需要注意的是,要先检测context是否已经存在了fn,若是,我们要先缓存,等call方法调用完后,我们再还原回去,不然,执行完我们自己定义的call方法后,若原来context对象原先已经存在了fn属性的话,则会被我们delete掉。

apply函数的实现

这里就不展开对apply方法的赘述了,它和call函数的区别就在于传参形式是数组。

实现

Function.prototype.apply= function (context) {
    // 获取要绑定的上下文
    let _context = context || (typeof window === 'undefined' ? global : window);
    // 保存旧的fn属性
    let oldFn = _context.fn;
    // 给_context添加这个函数
    _context.fn = this;
    // 获取参数
    let args = arguments[1] || [];
    // 调用函数
    let result = _context.fn(...args);
    // 删除fn属性
    delete context.fn;
    // 若旧的fn存在,则还原
    oldFn && (context.fn = oldFn);
    // 返回结果
    return result;
}

到此为止,我们已经实现了call、apply、bind函数的基本功能了,希望能帮助大家更好地理解这三者的原理和用法。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions