网上有很多bind
模拟实现的文章, 但是偏偏在作为构造函数的实现方法上说得都不太清楚, 这篇笔记循序渐进从零开始, 尝试说清楚bind
的原理和实现.
先简单分析bind
的用法: bind
会返回一个函数, 而并不会像apply
和call
改变this
的绑定后直接执行.
绑定后的函数可以作为构造函数用new
运算符生成原函数的实例.
简单实现
先看个最简单的实现方法.
最简单的实现
Function.prototype.easyBind = function(context) {
// 假如是 foo 这个方法调用了 easyBind, 那么这时候 this 就是 foo;
// 用闭包保留这时候的 this, 放在变量 _foo 里;
var _foo = this;
return function() {
// 当调用 easyBind 后的方法, 实际上这时候是 foo.apply(context);
_foo.apply(context);
}
}
最简单的版本只是利用闭包记录原函数, 在调用时再'拿出来'.
可传参的版本
最简单的版本没有传参的方法, 要实现传参用ES6的扩展运算符说明起来非常简单.
ES6扩展运算符实现方式
Function.prototype.es6Bind = function(context, ...args1){
const _foo = this;
return function(...args2) {
_foo.call(context, ...args1, ...args2);
}
}
再看看用兼容的实现方法.
可传参的传统实现方式
Function.prototype.tdBind = function(context){
var _foo = this;
// 因为第一个参数是 context, 所以要跳过, 直接取第二个以后的参数.
var args1 = Array.prototype.slice.call(arguments, 1);
return function(){
// 利用 slice 会返回新数组, 把 arguments 从类数组变成新数组;
// 相当于 ES6 的 [...arguments];
var args2 = Array.prototype.slice.call(arguments);
// 合并 args1 和 args2;
var args = args1.concat(args2);
_foo.apply(context, args);
}
}
ES6扩展运算符的方法在处理参数时候非常好理解, 但是为什么要用Array.prototype.slice.call(arguments)
而不直接使用arguments.slice()
?
这是因为arguments
是一个类数组对象, 并不是真正的数组, 没有数组独有的slice
方法.
当一个对象拥有length
属性和序列索引, 这个对象即可看作是类数组对象, 举个?:
// 普通数组
var array = ['a', 'b', 'c'];
// 类数组
var arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
因此如果想要操作argument
这个类数组就得借用Array.prototype.slice.call()
方法.
作为构造函数的情况
绑定后的函数也是函数, 既然是函数, 就可以作为构造函数并使用new
操作符创建对象.
先看看MDN对bind
作为构造函数使用的定义:
绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。 - Function.prototype.bind()
举个?:
var bar = {
name: 'bar'
};
function foo(first, second){
console.log('this.name', this.name);
console.log('first', first);
console.log('second', second);
}
var bound = foo.bind(bar, '1st');
var obj = new bound('2nd');
/*
输出:
this.name undefined
first 1st
second 2nd
*/
从上面例子可以看出, obj
的this
已经不是bar
, 这时候的this
应该是obj
本身. 但是参数还是照传的.
我们可以用instanceof
运算符来判断是否是作为构造函数运行. 为什么呢? 下面来仔细分析下:
先看看MDN里对instanceof
的定义:
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
再看看使用instanceof
检测实例对象原型链的?.
// 构造函数
const A = function() {},
B = function() {},
C = function() {},
D = function() {};
B.prototype = A.prototype;
C.prototype = new B();
var E = new C();
console.log(E instanceof C);
// true
// 因为 E.__proto__ === C.prototype
console.log(E instanceof B);
// true
// C.prototype.__proto__ === B.prototype
console.log(E instanceof A);
// true
// C.prototype.__proto__ === B.prototype === A.prototype
console.log(D instanceof A);
// false,因为 E.__proto__ 不在A的原型链上
如果不熟悉或者忘记了原型链, 参考下面的定义:
- js分为函数对象和普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype属性
1.1 属性__proto__是一个对象,它有两个属性,constructor和__proto__;
1.2 原型对象prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建;- Object、Function都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String
- A.prototype.constructor == A
准则1:原型对象(即A.prototype)的constructor指向构造函数本身- E.proto == A.prototype
准则2:实例(即E)的__proto__和原型对象指向同一个地方
我们用ES6扩展运算符的实现方式作为?.
作为普通函数执行的情况
运行bind
后的函数bound()
, 此时相当于运行下面函数, 相当于调用了window.bound()
, 下面函数的this
指向window
.
function(...args2) {
_foo.call(context, ...args1, ...args2);
}
作为构造函数执行的情况
当执行到上面例子里的new bound('2nd')
, 此时新对象obj
的构造函数为:
function(...args2) {
_foo.call(context, ...args1, ...args2);
};
new
运算符会生成一个新实例对象(假如新实例为obj
, 构造函数为fn
), 然后把fn.prototype
的值赋给obj.__proto__
. 最后把新实例obj
作为构造函数fn
的this
.
那么此时构造函数里的this
指向调用者, 即obj
对象实例本身.
可作为构造函数的实现
利用bound
函数的this
可以分辨作为普通函数执行和作为构造函数执行的情况.
Function.prototype.es6Bind = function(context, ...args1) {
const _foo = this; // _foo由于闭包, 始终指向原函数.
const bound = function(...args2) {
// 当作为构造函数时,this 指向实例,_foo 指向原函数,
// 此时结果为 true,当结果为 true 的时候,this 指向实例。
// 当作为普通函数时,this 指向 window,_foo 指向原函数,
// 此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
_foo.call(
this instanceof _foo
? this
: context,
...args1, ...args2
);
};
// bound作为构造函数需要拥有原函数的prototype属性,
// 生成的实例才能继承原函数上定义的属性和方法.
bound.prototype = this.prototype;
return bound;
}
这种实现方法有个缺点, 因为bound.prototype = this.prototype
是浅拷贝, 所以当修改bound.prototype
的属性方法时, 原函数foo
的属性方法也会被改变.
举个?:
function A() {
this.name = 1;
}
function B() {};
console.log(A.prototype.name);
// 1
B.prototype = A.prototype;
B.prototype.name = 2;
console.log(A.prototype.name);
// 2
我们可以构造一个空函数, 让原函数原型赋值给空函数的prototype
, 生成的实例属性方法完全继承自原函数, 但是不会修改到原函数的prototype
.
举个?:
// 构造一个空函数fNOP;
function fNOP() {};
function bound() {}
function _foo() {};
_foo.prototype.name = '_foo';
fNOP.prototype = _foo.prototype;
const fnop = new fNOP();
// 可见 fnop.__proto__ === fNOP.prototype === _foo.prototype;
bound.prototype = fnop;
// 因此假如有属性name, 由于 bound.prototype.name === fnop.name, 所以就算修改 bound.prototype.name, 改变的只是fnop实例的属性.
bound.prototype.name = 'bound'
console.log(bound.prototype.name);
// bound
console.log(_foo.prototype.name);
// _foo
还有一个需要改进的地方, 代码运行到bound.prototype = this.prototype;
时, 如果原函数是一个箭头函数, 那么this.prototype
此时为undefined
.
最终实现
Function.prototype.myBind = function(context, ...args1) {
// 如果不是函数, 直接报错;
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
// _foo由于闭包, 始终指向原函数, 即上面例子的foo.
const _foo = this;
// 构造一个空函数用于中转.
const fNOP = function(){};
const bound = function(...args2) {
// 当作为构造函数时,this 指向实例,_foo 指向原函数,
// 此时结果为 true,当结果为 true 的时候,this 指向实例。
// 当作为普通函数时,this 指向 window,_foo 指向原函数,
// 此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
_foo.call(this instanceof _foo ? this : context, ...args1, ...args2);
};
// 原函数为箭头函数时, this.prototype 为 undefined.
if (this.prototype) {
// 这里用fNOP的prototype记录原函数的prototype.
fNOP.prototype = this.prototype;
}
// bound作为构造函数需要拥有原函数的prototype属性,
// 生成的实例才能继承原函数上定义的属性和方法.
// fNOP的实例继承原函数的所有属性方法, 并使bound.prototype指向fNOP的实例,
// 这样可以使bound.prototype的修改影响不到原函数的属性方法.
bound.prototype = new fNOP();
return bound;
}
MDN的bind实现
最后对比一下MDN的Polyfill实现方式:
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 维护原型关系
if (this.prototype) {
// 当执行Function.prototype.bind()时, this为Function.prototype
// this.prototype(即Function.prototype.prototype)为undefined
fNOP.prototype = this.prototype;
}
// 下行的代码使fBound.prototype是fNOP的实例,因此
// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP();
return fBound;
};
}
不同的是MDN采用的是this instanceof fBound
判断是否是作为构造函数, 其实
fBound.prototype
在fNOP.prototype
的原型链上, 而fNOP.prototype === this.prototype
.
只要在相同的原型链上, instanceof
就能访问到, 效果都一样.