依据ECMA规范,手写一个bind函数
Function.prototype.bind 函数,参见ECMA规范地址
如题,这次来实现一个boundFunction函数,不挂载在Function.prototype上,而是一个单独声明的函数。挂载在原型上的bind,可以参考MDN
主要步骤,摘自ECMA规范,如图:
实现思想:当然是依葫芦画瓢,这里,我们借用ES6的...运算符与解构赋值。目的是图省事,实现bind函数,主要是了解其内部的操作流程。
首先,把需要用到的函数,都依照规范声明实现,其中FunctionIsConstructor是自己写的判断一个函数是否为构造函数,比如Proxy就不是构造函数。
而SetFunctionLength是对设置函数length属性的操作的封装,正如其名。
function FunctionIsConstructor(fnc) { let isConstructor = true; try { Object instanceof fnc } catch (e) { if (e instanceof TypeError) { isConstructor = false } } return isConstructor } function BoundFunctionCreate(targetFunction, boundThis, boundArgs) { let proto = Object.getPrototypeOf(targetFunction); let boundFunction = function () { if (new.target) { // 实现构造函数功能 if (FunctionIsConstructor(targetFunction)) { return new targetFunction(...boundArgs) } else { throw new TypeError(`${arguments.callee.name} is not a constructor`) } } else { // 实现函数调用功能 return targetFunction.call(boundThis, [...boundArgs, ...arguments]) } } delete boundFunction.name; Object.setPrototypeOf(boundFunction, proto) return boundFunction; } function isCallable(Target) { if (typeof Target === 'function') return true; return false; } function ToInteger(arg) { let number = Number(arg); if (number !== number) return +0; if (number === 0 || number === Infinity || number === -Infinity) return number; return Math.floor(Math.abs(number)); } function SetFunctionName(F, name, prefix) { if (typeof name === 'symbol') { let description = name.description if (description === undefined) { name = '' } else { name = `[${description}]` } } if (prefix) { name = `${prefix} ${name}` } return Object.defineProperty(F, 'name', { value: name, writable: false, enumerable: false, configurable: true }) } function SetFunctionLength(F, Target, args) { let targetHasLength = Target.hasOwnProperty('length'); let L; if (targetHasLength) { let targetLen = Target.length; if (typeof targetLen !== 'number') { L = 0; } else { targetLen = ToInteger(targetLen) L = Math.max(0, targetLen - args.length) } } else { L = 0; } Object.defineProperty(F, 'length', { value: L, writable: false, enumerable: false, configurable: true }) }
然后,把这些函数按照规范的流程,组装起来,完全对应。
function boundFuntion(targetFunction, thisArg, ...args) { let Target = targetFunction; if (!isCallable(Target)) { throw new TypeError(`${Target.name}.bind is not a function`) } let F = BoundFunctionCreate(Target, thisArg, args); SetFunctionLength(F, Target, args) let targetName = Target.name if (typeof targetName !== 'string') targetName = ''; SetFunctionName(F, targetName, 'bound') // 支持直接new调用创建的绑定函数 return new.target ? new F() : F }
如此,一个手写的bind函数就出来。函数最后一行,用new.target来判断,以支持直接使用new调用创建的绑定函数,如new boundFunction(fnc)
最后,简单测试一下。
var modules = { x: 42, getX: function() { console.log('this', this === modules, this.x) return this.x; } } var unboundGetX = modules.getX; console.log('unbounnd ', unboundGetX()); // The function gets invoked at the global scope // // expected output: unbounnd undefined var boundGetX = boundFuntion(unboundGetX, modules); console.log('bounnd ', boundGetX()); // expected output: bounnd 42
总结
手写bind函数,主要是利用闭包功能,将传入的this固定在新函数里,并对原型链进行处理,以免丢失继承关系。
其他的错误处理,比如判断是否构造函数,是否用new调用当前函数,也是值得去了解的。