什么是bind
引用MDN的解释
Function.prototype.bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind
的第一个参数,而其余参数会被指定为新函数的参数,供调用时使用
翻译一下,bind
主要做了三件事
- 返回一个新的函数
- 新函数
this
指向bind
的第一个参数 - 其余参数作为新函数的参数传入
基础的使用
一个最简单的例子
var tea = {
value: '奶茶'
}
function drink(name) {
console.log(`${name} drink ${this.value}`);
}
// 1、返回了一个函数
// 2、新函数的this是第一个参数
// 3、其余参数作为新函数的参数
var drinkTea = drink.bind(tea, 'dongchuan');
drinkTea(); // 'dongchuan drink 奶茶'
简单实现bind
分析了bind
做了的三件事情,我们就可以来简单的实现bind了,我们先使用ES6进行实现,方便快捷好看懂
// 第一版
function _bind(asThis, ...args1) {
let fn = this; // 函数调用时,原this其实就是这个调用函数
return function(...args2) { // 同时,返回的新函数也可以接受参数
return fn.call(asThis, ...args1, ...args2);
}
}
构造效果的模拟实现
其实考虑到上面bind
做的三件事情已经基本完成了bind
的实现,但是在看其他同学些的bind实现时,发现还有一个,如果对返回的新函数使用new
操作符,bind
重新定义的this
会失效!!!
具体参考博客链接 JavaScript深入之bind的模拟实现 #12
其实在MDN上也有相关描述,只是没有在最上方写明
MDN关于使用bind
后再使用new
的介绍
绑定函数也可以使用
new
运算符构造,它会表现为目标函数已经被构建完毕。提供的this
值会被忽略,但前置参数仍会提供给模拟函数
重要信息提取:提供的this
值会被忽略
用一个例子来理解一下
var tea = {
value: '奶茶'
}
function drink(name) {
console.log(`${name} drink ${this.value}`);
}
var drinkTea = drink.bind(tea, 'dongchuan');
drinkTea(); // 'dongchuan drink 奶茶'
var obj = new drinkTea(); // 'dongchuan drink undefined'
console.log(obj.value); // undefined
// 使用new操作符后,this指向为obj
需要注意的点:
drinkTea
的this
其实还是指向的tea
- 对绑定函数
drinkTea
使用new操作符,原来绑定的this
会失效,此时的this
指向obj
所以,我们需要对bind
返回的这个新函数的this
做一个判断
- 如果新函数的
this
是在新函数的原型上,即是用构造函数- (这里可以参考另外一个关于
new
操作符实现的解析如何实现一个new操作符)
// 简单的实现一个new function _new(resultFn) { let tmp = {}; tmp.__proto__ = resultFn.prototype; resultFn.call(tmp); return tmp; // 验证 console.log(tmp instanceof resultFn); // true }
- (这里可以参考另外一个关于
- 否则就是正常情况
重新设计返回新函数的this指向
// 第二版,支持new
function _bind(asThis, ...args1) {
let fn = this;
let resultFn = function(...args2) {
console.log(this instanceof resultFn); // 如果使用new操作符,这里为true
return fn.call(this instanceof resultFn ? this : asThis, ...args1, ...args2);
}
// 针对创建实例做处理,使实例能够继承绑定函数的原型
resultFn.prototype = fn.prototype;
return resultFn;
}
针对返回函数的prototype的优化
这里还有一个问题,如果直接用resultFn.prototype = fn.prototype,修改resultFn.prototype也会造成fn.prototype的修改
- 解决方式:使用一个空函数作为中转
// 第三版
function _bind(asThis, ...args1) {
let fn = this;
let resultFn = function(...args2) {
return fn.call(this instanceof resultFn ? this : asThis, ...args1, ...args2);
}
let fnNo = new Function();
fnNo.prototype = fn.prototype;
resultFn.prototype = new fnNo(); // 使用fnNo做中转 resultFn.prototype.__proto__ === fnNo.prototype === fn.prototype;
return resultFn;
}
用ES5替代ES6的实现
以上所有bind
的实现都是通过ES6实现的,但是如果都需要手写bind
,那么很多ES6新语法支持度可能也不那么高,所以,我们把实现通过ES5进行重写,那我们再重新梳理一下实现的思路
- 返回一个新的函数
- 新函数的
this
在正常情况下指向第一个参数 - 其他参数传给新函数
- 对
new
的支持,包括this
不能被第一个参数替换,支持绑定函数的prototype
属性的访问
使用ES5重写
// 第四版
function _bind(asThis) {
var fn = this;
var args1 = Array.prototype.slice.call(arguments, 1);
var resultFn = function() {
var args2 = Array.prototype.slice.call(arguments);
return fn.apply(this instanceof resultFn ? this : asThis, args1.concat(args2));
}
var fnNo = new Function();
fnNo.prototype = fn.prototype;
resultFn.prototype = new fnNo();
return resultFn;
}
最终代码
最后,加上异常情况判断
- 判断调用
bind
的是不是个函数 - 在线上使用,判断是否存在
bind
所以,最后的实现代码就是:
// 最终版
function _bind(asThis) {
// 判断调用bind的是不是个函数
if (typeof this !== 'function') {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var fn = this;
var args1 = Array.prototype.slice.call(arguments, 1);
var resultFn = function() {
var args2 = Array.prototype.slice.call(arguments);
return fn.apply(this instanceof resultFn ? this : asThis, args1.concat(args2));
}
var fnNo = new Function();
fnNo.prototype = fn.prototype;
resultFn.prototype = new fnNo();
return resultFn;
}
Function.prototype.bind = Function.prototype.bind || _bind;
完成!