Open Checkson opened 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()方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。(摘自MDN)
好吧,MDN似乎解释得有点云里雾里的。用我的话形容就是bind方法能够改变函数的this指向,并返回一个新的函数。值得注意的是,bind方法是作用于函数的,在接收的参数列表中,默认将第一个参数作为this绑定的对象,之后的一序列参数将会在传递的实参前传入作为它的参数。
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)) } }
解析
foo.bind
foo
_context
_this
args
function Foo() { // pass } // 实例化 let instance = new Foo.bind(context, ...args); // 等价于 let instance = new Foo(...args);
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掉。
context
fn
delete
这里就不展开对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函数的基本功能了,希望能帮助大家更好地理解这三者的原理和用法。
背景
相信,很多同学都有过和我同样的经历,在编写一个React组件的时候,常常要为某个监听事件的回调函数,绑定当前组件的上下文,形式大概如下:
这里我们不讨论为什么React中的绑定事件的回调函数需要手动去绑定组件的上下文,有兴趣的同学可以自行搜索,或者点击这里。bind这个函数有什么大的魅力,能让我们在React开发中这么频繁地使用,背后到底做了什么?JavaScript类似bind的函数还有call和apply,那么它们又是如何去工作的,下面分享一下我自己的探索。
bind函数的实现
概念
好吧,MDN似乎解释得有点云里雾里的。用我的话形容就是bind方法能够改变函数的
this
指向,并返回一个新的函数。值得注意的是,bind方法是作用于函数的,在接收的参数列表中,默认将第一个参数作为this
绑定的对象,之后的一序列参数将会在传递的实参前传入作为它的参数。那么,自己要实现一个bind函数,首先要知道,bind函数是怎么用的。
示例
说明
注意
这里的bind函数调用返回的是一个绑定context上下文的函数引用,这是区别于call和apply调用后返回函数运行后的返回值。那么,如果我们不传context,那么context默认是指向谁呢?
可见,我们什么也不传给bind函数的时候,默认上下文是指向全局对象的。
实现
解析
foo.bind
,条件判断中的this就是指foo
这个引用。_context
是存储要绑定新的上下文。_this
是指向上面提到的foo
,也就是指向调用bind方法的函数。args
是指处理第一个参数以后的所有参数,以数组形式存在。call函数的实现
概念
call和apply都是为了解决改变 this 的指向,也就是改变函数的上下文,只是传参方式不一样。无论调用call还是apply方法,函数都会被立即执行。
示例
输出
说明
foo
函数。思路
既然,call方法能改变函数的this指向,那么我们可以让需要绑定的上下文,可以执行这个函数即可。
实现
解析
这段代码理解起来并不难,但是需要注意的是,要先检测
context
是否已经存在了fn
,若是,我们要先缓存,等call方法调用完后,我们再还原回去,不然,执行完我们自己定义的call方法后,若原来context
对象原先已经存在了fn
属性的话,则会被我们delete
掉。apply函数的实现
这里就不展开对apply方法的赘述了,它和call函数的区别就在于传参形式是数组。
实现
到此为止,我们已经实现了call、apply、bind函数的基本功能了,希望能帮助大家更好地理解这三者的原理和用法。