MDN
的实现:链接
本文实现了两个版本的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.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(); // xiaofeiconsole.log(o); // {}console.log(obj); // {name: "xiaofei", age: 18}o instanceof sayName; // falseo 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;}