bind方法简介
关于bind() 方法的介绍,可以参照这里;
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法
function.bind(thisArg[, arg1[, arg2[, ...]]])
参数
- thisArg
调用绑定函数时作为this
参数传递给目标函数的值。
如果使用new
运算符构造绑定函数,则忽略该值。当使用bind
在setTimeout
中创建一个函数(作为回调提供)时,作为thisArg
传递的任何原始值都将转换为object
。如果bind
函数的参数列表为空,或者thisArg
是null
或undefined
,执行作用域的this
将被视为新函数的 thisArg
。
- arg1, arg2, ...
当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
返回值
返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
实现bind方法1.0
- 首先,我们定义一个函数,这个函数的第一个参数作为
thisArg
,后面的参数作为返回的新方法的参数:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift(); // 一箭双雕的写法
}
也可以利用剩余参数:
Function.prototype.mybind = function (thisArg, ...args) {
// ...
}
剩余参数是ES6的新特性,一般说来,不支持bind的情况一般也不支持剩余参数,所以,不推荐这种写法。
- 然后,
mybind
方法会返回一个新函数,该函数将外层函数的参数与内层函数的参数连接起来一起作为参数:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
return function () {
newArgs = args.concat(Array.from(arguments));
// ...
}
}
- 我们可以使用
apply
来完成this
指向变更,在那之前可以使用变量thisFunc
先保存原函数:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
return function () {
newArgs = args.concat(Array.from(arguments));
return thisFunc.apply(thisArg, newArgs);
}
}
在调用apply必须先检测
thisFunc
是不是Function
,这里没写是因为我懒。
低版本浏览器在不支持bind的情况下会支持apply吗?还真会。
当然,你也可以手动实现apply
:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
// 手动实现需要在第一个参数为null时
let thisArg = args.shift() || window;
let thisFunc = this;
return function () {
newArgs = args.concat(Array.from(arguments));
let fn = Symbol('thisFunc');
thisArg[fn] = _Func;
let res = thisArg[fn](...newArgs);
delete thisArg[fn];
return res;
}
}
数组解构也是个ES6的语法,感觉又把自己绕进去了。
这样,一个类似于bind
的方法就写好了,下面我们调用一下它。
使用mybind 1.0
- 情景1:普通函数
var age = 18;
let myfn = function (a, b, c) {
console.log(this.age, a, b, c);
}
myfn(); // 18 undefined undefined undefined
let xiaohuang = {
age: 12
}
let myfn1 = myfn.mybind(xiaohuang, 1);
myfn1(3); // 12 1 3 undefined
let myfn2 = myfn.bind({ age: 114514 });
myfn2(19, 19, 810); // 114514 19 19 810
myfn2(19, 19, 810); // 114514 19 19 810
没有问题,一切正常。
- 情景2: 构造函数
let Animal = function (name) {
this.name = name;
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
console.log(tom, buly); // {} {name: "tom"}
// expected output: {name: "tom"} {name: "buly"}
哦吼,出问题了。
实现bind方法2.0
看来我们根据具体的情况采取不同的策略,当传入的函数是一个构造函数时,我们不需要更改this
的指向。
如何判断是否为构造函数呢?只需要判断this instanceof 构造方法
的值就可以了。
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 因为需要构造函数,所以不能是匿名函数了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判断是否为构造函数
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
return fBound;
}
使用bind2.0
- 情景2: 构造函数
let Animal = function (name) {
this.name = name;
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
console.log(tom, buly); // {name: "tom"} {name: "buly"}
— 我很高兴你完成了手写bind的全部内容。
— 不好意思,你高兴的太早了。
- 情景3: 带原型对象(
prototype
,下同)的构造函数
let Animal = function (name) {
this.name = name;
}
// 箭头函数中的this会穿透作用域,所以不要用箭头函数哦
Animal.prototype.say = function() {
console.log('hello, my name is ' + this.name);
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
tom.say(); // Error: tom.say is not a function
新返回的函数与原函数的原型对象并没有建立联系,所以new
出来的对象不能访问到原函数的原型对象上的方法。
实现bind方法3.0
让我们简单粗暴一点:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 因为需要构造函数,所以不能是匿名函数了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判断是否为构造函数
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
// 直接将原函数的prototype赋值给绑定函数
fBound.prototype = this.prototype;
return fBound;
}
当然,我们不推荐这种粗暴的继承方式,这种情况下,若更改新函数的原型对象,则原函数的原型对象也会被改变。
- 推荐方法1: 原型式继承
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 中间函数
let fNop = function () { };
// 因为需要构造函数,所以不能是匿名函数了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判断是否为构造函数
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
fNop.prototype = this.prototype;
// 原型式继承
fBound.prototype = new fNop();
return fBound;
}
你可能不太能理解这段东西,我试着简单的解释一下:
如果我们采用ES6中的class
的话,是这样的:
class Animal {
constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
}
let tom = new Cat('tom');
这里,tom
的__proto__
指向Cat的原型对象,tom
的__proto__
的__proto__
指向Animal的原型对象
但ES5只支持构造函数,不支持class
,所以们先创建一个空函数fNop
,然后使其原型对象指向原函数的原型对象。在使得fBound
的原型对象指向fNop
的实例,这也变相实现了fBound extends fNop
。
以上纯属个人理解,仅供参考。
- 推荐方法2:
Object.create
方便书写又方便理解,其实就是浅拷贝。
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 因为需要构造函数,所以不能是匿名函数了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判断是否为构造函数
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
// Object.create拷贝原型对象
fBound.prototype = Object.create(this.prototype);
return fBound;
}
使用bind3.0
- 情景3: 带原型对象(
prototype
,下同)的构造函数
let Animal = function (name) {
this.name = name;
}
// 箭头函数中的this会穿透作用域,所以不要用箭头函数哦
Animal.prototype.say = function() {
console.log('hello, my name is ' + this.name);
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
tom.say(); // hello, my name is tom
和真正的bind不同,使用我们手动实现的bind,最后实例化的对象的构造函数是
fBound
而不是原构造函数。
总结
实现bind的技术要点如下:
将剩余的参数和传入的参数拼接,作为新的参数。知识点有:解构赋值、Array.prototype.concat、Array.prototype.apply;
判断是否为构造函数,使用instanceof操作符;
实现原型继承,参考JS中的继承与原型链。
其他的技术要点,可以参考我之前的文章《如何手写一个call方法》。
MDN提供的polyFill代码
polyfill:补丁。意思是在浏览器不支持bind时,采用该代码以解决此问题:
全部代码如下:
// Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
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 baseArgs= ArrayPrototypeSlice.call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
})();
这里提出2条解读:
- 最外层是匿名函数自调用,是防止浪费存储资源的做法。
-
baseArgs.length = baseArgsLength;
这一步的原因有二。一是闭包导致了所有函数共用一个baseArgs
;二是因为push是一种会修改原数组的API,所以不可避免的会改动baseArgs
。使用concat
或者数组解构可以更好地解决此问题。