-
Notifications
You must be signed in to change notification settings - Fork 3
Description
背景
相信,很多同学都有过和我同样的经历,在编写一个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函数的基本功能了,希望能帮助大家更好地理解这三者的原理和用法。