本文首发于个人博客
本文是前端手写系列的第二篇,讲如何手写一个 bind,会从 bind 用法开始,逐渐带你领略手写 bind 的三种境界。
bind 用法
bind 是用来干嘛的?跟 bind 发音其实很像,是用来绑定 this 的。
具体怎么用呢?让我们先来看一个 bind 的简单用法:
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // unboundGetX 声明为全局函数
// expected output: undefined
const boundGetX = unboundGetX.bind(module); // 把 unboundGetX 的 this 绑定成module
console.log(boundGetX());
// expected output: 42
语法很简单,就是函数绑定 this —— function.bind(thisArg[, arg1[, arg2[, ...]]]),它返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
既然 bind 用法不难,大家有没有想过要怎么实现一个 bind 呢?
三重境界实现 bind
- 初阶:ES6
- 优点:代码简洁,使用
const、...操作符 - 缺点:不兼容 IE,不支持
new
- 优点:代码简洁,使用
- 中阶:ES5
- 优点:兼容 IE
- 缺点:参数获取复杂,不支持
new
- 高阶:成年人全都要
- 优点:兼容性最高,且支持
new - 缺点:最复杂
- 优点:兼容性最高,且支持
初阶
这里直接上代码了,不熟悉ES6的可以试着用用展开操作符...,很好很强大
function bind1(asThis, ...args) {
const fn = this; // 这里的 this 就是调用 bind 的函数 func
return function (...args2) {
return fn.apply(asThis, ...args, ...args2);
};
}
中阶
进阶中阶的话,我们需要注意参数不能再用 ...args 拿了,要使用 Array.prototype.slice(arguments) 。
function bind2(asThis) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
var fn = this;
if (typeof fn !== "function") { // 加入了对调用函数类型的判断
throw new Error("cannot bind non_function");
}
return function () {
var args2 = slice.call(arguments, 0);
return fn.apply(asThis, args.concat(args2));
};
}
高阶
终于来到高阶了,写之前,让我们先来看下应当如何判断一个对象实例是否是通过 new 构造函数() 创建出来的。
const temp = new fn(args) 其实等价于如下代码:
const temp = {}
temp.__proto__ = fn.prototype
fn.call(temp, ...args)
return temp
其中最核心的是第二句:temp.__proto__ = fn.prototype,因为 new 出来的对象实例必定满足这个条件,我们便知道可以用 fn.prototype 是否为对象实例的原型来处理类似 new (funcA.bind(thisArg, args)) 的情况。
function bind3(asThis) {
var slice = Array.prototype.slice;
var args1 = slice.call(arguments, 1);
var fn = this;
if (typeof fn !== "function") {
throw new Error("Must accept function");
}
function resultFn() {
var args2 = slice.call(arguments, 0);
return fn.call(
resultFn.prototype.isPrototypeOf(this) ? this : asThis, // new 的情况下 this 改绑成 new 出来的对象实例
args1.concat(args2)
);
}
resultFn.prototype = fn.prototype;
return resultFn;
}