Checkson / blog

Checkson个人博客
12 stars 3 forks source link

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

Open Checkson opened 5 years ago

Checkson commented 5 years ago

背景

相信,很多同学都有过和我同样的经历,在编写一个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等标识符,就好像学校里面老师给我们举例子中的张山、李四、王五,没有特别的意思,只是一种约定成俗的东西。

说明

注意

这里的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))
    }
}

解析

call函数的实现

概念

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

示例

let context = {
    bar: 1
}

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

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

输出

1
你好,世界!

说明

思路

既然,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函数的基本功能了,希望能帮助大家更好地理解这三者的原理和用法。