2020.4.30
事实上存在new.target这个变量,在函数中会指向构造函数。如果不是以new操作符调用的构造函数,new.target为undefined。因此我们可以通过new.target判断函数是否是new操作符调用。不过各浏览器对bind的支持比对new.target要早,所以在bind函数的polyfill中使用new.target可能不太合适。
我们可能遇到过实现bind函数这样的题目,但似乎并不存在完美模拟原生bind函数的可能。ECMAScript 2015中将bind创建的函数称为exotic function object(怪异函数对象),这很适宜,因为它的确存在一些“怪异”之处。
在继续之前我们需要先了解bind函数。这可以参考MDN的解释:
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
另外还需要了解new
操作符的实现,同样参考MDN的解释:
- 创建一个空的简单JavaScript对象(即
{}
); - 链接该对象(即设置该对象的构造函数)到另一个对象;
- 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
第2步即为对象绑定隐式原型,将对象的__proto__属性指向构造函数的prototype,而函数默认的prototype拥有constructor属性指向构造函数,从而实现了所谓的“链接构造函数”。
这里约定一下,bind()
方法的实现函数叫bind函数,创建的新函数叫做绑定函数,被封装的原函数叫做原函数。
bind函数至少有这些特性:
- 除非是被
new
操作符调用,否则原函数执行环境中的this总是为bind函数被调用时传递的第一个参数; - 绑定函数的prototype为
undefined
; - 通过
new
操作符调用绑定函数时,原函数执行环境的this仍为new
操作符创建的新对象,即bind函数的this绑定被忽略; - 通过
new
操作符调用绑定函数返回的对象是原函数的实例。即:function func() {} var fBound = func.bind({}); var foo = new fBound(); foo instanceof func; // true
MDN推荐的bind函数的Polyfill如下(部分):
Function.prototype.bind = function(that) {
var target = this; //原函数
var args = Array.prototype.slice.call(arguments, 1); //绑定参数
var bound; //绑定函数
//实际上binder才是绑定函数,bound将其再次包装后返回
var binder = function () {
if (this instanceof bound) {
//如果是通过new操作符调用的绑定函数,则绑定函数执行环境的this指向new操作符创建的新对象,而这个对象的隐式原型指向构造函数bound的prototype
//new操作符调用,忽略bind函数绑定的this,以实际执行环境this调用原函数
var result = target.apply(
this,
args.concat(arguments)
);
if (Object(result) === result) {
//如果构造函数返回的是一个对象,返回该对象
return result;
}
return this;
} else {
//非new操作符调用,使用绑定的this
return target.apply(
that,
args.concat(arguments)
);
}
};
//计算bind函数调用时提供的绑定参数数量
var boundLength = Math.max(0, target.length - args.length);
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
boundArgs.push('$' + i);
}
//相当于返回binder。之所以用函数再包一层,应该是为了使函数的length拥有正确的值
bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);
//调整原型链,使绑定函数的实例仍然是能通过原函数的原型链检测(因为绑定函数实例的隐式原型(__proto__)的隐式原型指向原函数的prototype)
//如果原函数没有prototype,则绑定函数具有默认prototype,且不会被外界访问到(除非通过绑定函数的prototype属性)
if (target.prototype) {
//构造一个临时的空函数,防止绑定函数的prototype直接引用原函数的prototype。因为原函数的prototype对外界可见,具有不确定性。如果绑定函数被调用时传递一个隐式原型指向原函数prototype的this参数,直接引用原函数就会造成误判为new操作符调用
var Empty = function Empty() {};
//绑定函数.prototype->空函数实例;空函数实例.__proto__->原函数prototype
//从而实现了,通过new得到的绑定函数实例的原型链上有原函数的prototype(文章开头的特性4),但隐式原型为原函数prototype的对象不是绑定函数的实例,因为绑定函数的原型是一个内部的对象(Empty实例),不太可能作为外部某对象的隐式原型(除非是绑定函数被当作了构造函数[new操作]),从而在前面的binder函数中,根据判断绑定函数执行环境的this是否是绑定函数的实例,正确的判断出是否为new操作符调用的绑定函数
//当然这里的漏洞是,绑定函数的prototype并不是完全不可访问的,因为绑定函数是公开的,从而绑定函数的prototype也就可访问了。但这一般不会出问题,除非你写一些奇怪的代码(后面有分析到)
Empty.prototype = target.prototype;
bound.prototype = new Empty();
Empty.prototype = null;
}
return bound;
};
上面的实现已经接近完美了,几乎也没有更好的实现方案了。我们来看看它实现了文章开头列出的哪些特性。
特性2,绑定函数的prototype为undefined。其实这个很容易实现,但为了实现识别new操作符,只好让绑定函数拥有prototype。幸运的是,这对实际使用几乎没有影响。
特性3,通过new操作符调用绑定函数时忽略bind函数绑定的this。OK,通过new调用时一定可以识别到。
特性4,new操作符调用绑定函数返回的对象是原函数的实例。已实现,虽然原型链上多了一个节点,但并不影响原型链判断。
原生bind函数实例的原型链:原函数prototype->Object.prototype->undefined
polyfill实现的bind函数实例的原型链:内部临时函数Empty.prototype->原函数prototype->Object.prototype->undefined
特性1就耐人寻味了,显然关键在于对new操作符调用的精确判断。如果是new操作符调用,显然绑定函数执行环境的this即绑定函数的实例,会被当做new操作符调用对待(忽略bind函数绑定的this)。但非new操作符调用的情况,是否一定能够被当作一般情况对待?考虑下面的代码:
function foo(name) {
this.name = name;
}
var obj = {};
var bar = foo.bind(obj);
const mock = Object.create(bar.prototype ? bar.prototype : null);
bar.call(mock, 'Jack');
console.log(obj.name);
var alice = new bar('Alice');
console.log(obj.name);
console.log(alice.name);
console.log(mock);
理想的输出是:
Jack
Jack
Alice
[Object: null prototype]
原生bind函数的输出符合预测,但polyfill实现的版本输出:
undefined
undefined
Alice
foo { name: 'Jack' }
bar.call(Object.create(bar.prototype), 'Jack')
被当作了new操作符调用。因此绑定函数忽略了绑定的this,而使用了传入的this参数调用原函数,因此原函数中this.name = name
,this
指向mock
,所以赋值操作发生在mock
上而非obj
。
bind函数被成为怪异对象,怪异就怪异在其内部机制无法完全用合法的JavaScript逻辑模拟(至少我没有找到方法)。不过上面的实现已经可用了,最后的不一致行为其实属于“明知故犯”的怪癖代码,实际生产活动中完全可以杜绝。
参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind