JS-bind方法的两种实现
本文实现了两个版本的bind:简单版和进阶版。第一章实现了简单版并揭示了简单版存在的问题,第二章深入研究了导致该问题的原理,以及如何解决。
1. 简单版
1.1 实现
简单版就不讲解了,直接看下面的代码:
(备注:简单版不支持使用new调用新创建的构造函数)
Function.prototype.myBind = function (context, ...args) {
context = context || window;
let invokFn = this;
return function () {
// 将两次传进来的参数合并
let finalArgs = args.concat(...arguments);
return invokFn.call(context, ...finalArgs);
}
}
这样,我们就可以实现这样的效果:
let obj = {
name: "xiaofei"
}
function sayName(age, sex){
console.log(this.name, age+"岁", sex);
}
let boundSayName = sayName.myBind(obj, 18);
boundSayName("男"); // xiaofei 18岁 男
1.2 问题
最开始说了,这个实现不支持new调用。下面我们就来看一下如果用new调用这个绑定函数会有什么问题:
let obj = {
name: "xiaofei"
}
function sayName(age){
console.log(this.name); / **(1)** /
this.age = age;
}
let boundSayName = sayName.myBind(obj, 18);
let o = new boundSayName(); // xiaofei
这里打印的还是“xiaofei”,说明上面标注为(1)处执行时的this仍然指向obj。
下面揭示代码存在的问题:
问题1:
如果我们打印一下 obj 和 o,结果如下图:
可以看到,本该属于o实例对象的age属性跑到obj对象上了!其实这也很好理解,因为上面我们分析过,(1)处的代码this指向obj,那么它下一行的代码this.age = age也就相当于在给obj设置age属性。
问题2:
另外,我们进一步探索还可以发现一个问题:o是新绑定函数的实例,而不是旧函数的实例。
请读者仔细想一想,这样的结果合不合理?
合理的结果应该是:o应该同时是这两个函数的实例,即上面的两行代码都应该返回true。
针对这两个问题,我将在第2章深入分析导致该问题的原因,以及如何解决。
2. 升级版
2.1 问题的本质
要研究1.2中提出的问题,我们首先得深入理解new命令到底做了什么?
下面给出new命令的模拟实现代码(没接触过的读者建议研究一下,搞懂每一行代码在做什么):
function myNew(fn) {
let objTemp = {};
objTemp.__proto__ = fn.prototype;
let args = [].slice.call(arguments, 1);
let result = fn.call(objTemp, ...args); /*(2)*/
return (typeof result === 'object' && result != null) ? result : objTemp;
}
我们来分析一下new boundSayName()执行经历了什么:
首先,先执行new指令,当执行到(2)处代码时,用call执行boundSayName函数,这是第一次this指向发生了变化,此时this指向的是new命令底层生成的对象,也就是上面代码中的objTemp对象;
然后,boundSayName函数执行,也就是执行如下图所示中红框内的代码。我们看红框中的最下面那行代码,它又一次地调用了call,使得this指向了context,也就是1.2节中所示代码中的obj。
最后,调用invokFn函数,其实就是 sayName 函数(如下图所示),此时this指向为obj。这样,我们就能解释1.2中所提出的问题了。但是,该如何解决这个问题?下一节给出解决方案。2.2 解决
先来回顾一下:
Function.prototype.myBind = function (context, ...args) {
context = context || window;
let invokFn = this;
return function () {
// 将两次传进来的参数合并
let finalArgs = args.concat(...arguments);
return invokFn.call(context, ...finalArgs);
}
}
let obj = {
name: "xiaofei"
}
function sayName(age){
console.log(this.name); / **(1)** /
this.age = age;
}
let boundSayName = sayName.myBind(obj, 18);
let o = new boundSayName(); // xiaofei
console.log(o); // {}
console.log(obj); // {name: "xiaofei", age: 18}
o instanceof sayName; // false
o instanceof boundSayName; // true
我们实现了一个myBind方法,但是当绑定函数被new调用时,会存在两个问题:
实例o的age属性跑到obj对象上了;
新生成的实例不是sayName的实例。
我们想实现的最终结果:
age属性在o实例上,obj对象上没有age属性;
o既是sayName的实例,也是boundSayName的实例,即:o instanceof sayName 和 o instanceof boundSayName都返回true。
(2.2.1) 我们先来思考第一个问题。
思考一下上图中红框部分的this指向。可以分为两类:
当绑定函数直接调用时,即执行boundSayName()时,红框内代码执行时的this指向window;
当new调用时,即执行new boundSayName()时,根据2.1节的分析,此时this指向new命令底层生成的对象 objTemp。
此外,在new命令底层操作中还有这么一步(如上图箭头所示),我们将fn的原型赋予给了objTemp的--proto--属性,也就是说:objTemp instanceof fn 应为true。而objTemp对应红框中的this;fn对应boundSayName,boundSayName对应红框中的boundFn,因此,我们可以加这么一层判断 (this instanceof boundFn)?this:context,解释如下:
- 当boundFn的prototype出现在this对象的原型链中,说明此时是new调用的,此时call中传入this对象,也就是objTemp,也就是最终生成的实例;
- 如果不是,说明此时是普通执行(此时this指向window,window instanceof boundFn 显然返回false),call中就传入context对象,也就是obj对象。
代码如下:Function.prototype.myBind = function (context, ...args) { context = context || window; let invokFn = this; let boundFn = function () { let finalArgs = args.concat(...arguments); // 看这里!!就多了这里一行代码!! return invokFn.call((this instanceof boundFn) ? this : context, ...finalArgs); } return boundFn; }
(2.2.2) 下面来看第二个问题:新生成的实例不是sayName的实例。
这个问题的解决思路就是:将sayName的prototype赋予给boundFn的原型的--proto--属性。这样新生成实例就会继承boundFn的原型,而这个原型的--proto--又指向sayName的原型。所以最终的效果就是:新生成的实例的原型链中既有boundFn的原型,又有sayName的原型。
完整代码如下:
Function.prototype.myBind = function (context, ...args) {
context = context || window;
let invokFn = this;
let helperFn = function(){}; // 借助这个辅助函数将invokFn的prototype混入boundFn的原型链中.
helperFn.prototype = invokFn.prototype;
let boundFn = function () {
// 将两次传进来的参数合并
console.log(this instanceof invokFn);
let finalArgs = args.concat(...arguments);
return invokFn.call((this instanceof boundFn)?this:context, ...finalArgs);
}
boundFn.prototype = new helperFn();
return boundFn;
}