前言
bind的实现,涉及到javascript的很多方面的知识,也是前端面试常考的手写基础题,如果能完全掌握,可以帮你你更深的理解这门语言,闲言少叙,我们开始进入正题。(阅读这个章节之前,你需要提前掌握的知识点:原型链,this指向,bind、apply用法。)
1.改变this指向
bind最常用的方式是改变this指向,比如在setTimeout中调用函数默认会指向window(默认绑定),obj.fn this会指向obj(隐式绑定), bind给我们提供了一种方式去实现调用函数时改变默认this的方式,这种方式也被称之为显示绑定。
var name = "de1ck";
var person = {
name: "juejin",
};
function getName() {
console.log(this.name);
}
getName(); // de1ck 在全局执行环境中this指向全局对象。
var bindName = getName.bind(person);
bindName(); // juejin
根据这个特点,我们实现一下第一版的bind:
Function.prototype.bind = function (bindThis) {
var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
// bind函数的结果是返回一个待执行的函数
return function () {
// 这里的函数在全局环境中执行时,默认指向全局对象
return self.apply(bindThis); // 这里通过函数原型链上的apply方法对getName函数指定this执行
};
};
2.偏函数
bind 的另一个最简单的用法是使一个函数拥有预设的初始参数。
var name = "de1ck";
var human = {
name: "juejin",
};
function person(age, job, gender) {
console.log(this.name, age, job, gender);
}
person(25, "engineer", "male");
// de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
bindHuman("male");
// juejin 25 engineer male
基于此很容易得到一下实现:
Function.prototype.bind = function (bindThis) {
var self = this; // 这里的self对应的就是getName
var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
// bind函数的结果是返回一个待执行的函数
return function () {
// 参数合并
var args = [].concat(partArgs, [].slice.call(arguments));
// 这里的函数在全局环境中执行时,默认指向全局对象
return self.apply(bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
};
};
3.作为构造函数使用的绑定函数
当绑定后函数遇到构造函数时,this会指向构造函数实例,我们可以验证一下
var name = "de1ck";
var human = {
name: "juejin",
};
// 定义一个变量
var bindThis;
function person(age, job, gender) {
// 把this赋值给这个变量,以便后续验证
bindThis = this;
console.log(this.name, age, job, gender);
}
person(25, "engineer", "male"); // de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
// new
var newHuman = new bindHuman("male"); // // undefined 25 'engineer' 'male'
console.log(bindThis === human, bindThis === newHuman); // false true
从输出结果可以看出,使用构造函数会覆盖默认bind的this,在第1章节中介绍了隐式绑定, 这里new调用会绑定到新创建的对象。(具体可以查阅 你不知道的JavaScript中关于this的讲解)
所以,这里可以得到第三版的实现
Function.prototype.bind = function (bindThis) {
var self = this; // 这里的self对应的就是getName
var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
// bind函数的结果是返回一个待执行的函数
var boundFunction = function () {
// 参数合并
var args = [].concat(partArgs, [].slice.call(arguments));
// // 通过instanceof 判断该函数是否是通过new调用
return self.apply(this instanceof boundFunction ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
};
return boundFunction;
};
4.继承函数的原型
作为构造函数调用时,实例还会继承函数的原型。
var name = "de1ck";
var human = {
name: "juejin",
};
var bindThis;
function person(age, job, gender) {
bindThis = this;
this.age = age;
console.log(this.name, age, job, gender);
}
person.prototype.getAge = function () {
console.log(this.age);
};
person(25, "engineer", "male"); // de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
// new
var newHuman = new bindHuman("male"); // // undefined 25 'engineer' 'male'
console.log(bindThis === human, bindThis === newHuman); // false true
// Expected output 25
newHuman.getAge(); // Uncaught TypeError: newHuman.getAge is not a function
但上一版的实现中并没有做到原型的继承,我们可以通过修改返回函数的原型达到这个目的:
Function.prototype.bind = function (bindThis) {
var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
// bind函数的结果是返回一个待执行的函数
var boundFunction = function () {
// 参数合并
var args = [].concat(partArgs, [].slice.call(arguments));
// 通过instanceof 判断该函数是否是通过new调用
return self.apply(this instanceof boundFunction ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
};
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
boundFunction.prototype = self.prototype;
return boundFunction;
};
这个结果就是准确的吗?我们改一下代码,来测试chrome中prototype的原型是怎么继承的,来验证一下最终的结果。
var name = "de1ck";
var human = {
name: "juejin",
};
var bindThis;
function person(age, job, gender) {
bindThis = this;
this.age = age;
console.log(this.name, age, job, gender);
}
person(25, "engineer", "male"); // de1ck 25 engineer male
var bindHuman = person.bind(human, 25, "engineer");
// new
var newHuman = new bindHuman("male"); // undefined 25 'engineer' 'male'
// 主要下面输出结果 Expected output:true true false
console.log(
bindHuman.prototype === person.prototype,
newHuman.__proto__ === person.prototype,
bindHuman.constructor === Function
); // false true true
在浏览器控制台中并没有输出我们预想中的结果,所以我们可以发现原型链并没有直接传递给bind返回的函数,而是传递给了new之后的对象,那么怎么给对象设置他的原型呢,我们可以使用Object.setPrototypeOf,由此我们可以得到最终的实现:
Function.prototype.bind = function (bindThis) {
var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
var partArgs = [].slice.call(arguments, 1); // 获取方法的预设参数
// bind函数的结果是返回一个待执行的函数
var boundFunction = function () {
// 参数合并
var args = [].concat(partArgs, [].slice.call(arguments));
let isInstanceof = false;
if (this instanceof boundFunction) {
// 通过setPrototypeOf设置对象的原型
Object.setPrototypeOf(this, self.prototype);
// 通过 setPrototypeOf 后,this的构造函数变为self了,所以这里需要一个变量来说明是否是new调用
isInstanceof = true;
}
// 通过 isInstanceof 判断该函数是否是通过new调用 (this instanceof boundFunction 的结果为 false)
return self.apply(isInstanceof ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
};
return boundFunction;
};
5.快捷调用
在你想要为一个需要特定的 this 值的函数创建一个捷径(shortcut)的时候,bind() 也很好用。
你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组,就拿它来举例子吧。你可以简单地这样写:
var slice = Array.prototype.slice;
// ...
slice.apply(arguments);
用 bind()可以使这个过程变得简单。在下面这段代码里面,slice 是 Function.prototype 的 apply() 方法的绑定函数,并且将 Array.prototype 的 slice() 方法作为 this 的值。这意味着我们压根儿用不着上面那个 apply()调用了。
// 与前一段代码的 "slice" 效果相同
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);
// ...
slice(arguments);
core-js中很多工具函数使用上述bind来实现各种炫酷的功能,上述代码还有更优雅的方式实现:
var FunctionPrototype = Function.prototype;
var call = FunctionPrototype.call;
var uncurryThisWithBind = FunctionPrototype.bind.bind(call, call);
var slice = uncurryThisWithBind([].slice);
slice({ length: 3 });
我们怎么理解这种调用方式呢?可以把这种方式看做组合函数,是不是更容易理解了呢。
最后,你可以使用这种方式来优化bind的实现吗?
我们知道bind的调用的时候会验证this是否为函数类型,什么情况下能使用到呢?当使用快捷调用的时候很容易触发错误:
// ...
// Expected output: Uncaught TypeError: Bind must be called on a function
Function.prototype.bind.call({});
//
所以这里我们需要新增参数检测:
var FunctionPrototype = Function.prototype;
var call = FunctionPrototype.call;
var uncurryThisWithBind = FunctionPrototype.bind.bind(call, call);
var slice = uncurryThisWithBind([].slice);
var concat = uncurryThisWithBind([].concat);
Function.prototype.bind = function (bindThis) {
// 新增参数检测
if (typeof this !== "function") {
throw TypeError("Bind must be called on a function");
}
var self = this; // 这里的self对应的就是getName(可以考虑一下为什么需要在这里存放当前作用域下的this,而不是在返回函数内部的)
var partArgs = slice(arguments, 1); // 获取方法的预设参数 // bind函数的结果是返回一个待执行的函数
var boundFunction = function () {
// 参数合并
var args = concat(partArgs, slice(arguments));
let isInstanceof = false;
if (this instanceof boundFunction) {
// 通过setPrototypeOf设置对象的原型
Object.setPrototypeOf(this, self.prototype); // 通过 setPrototypeOf 后,this的构造函数变为self了,所以这里需要一个变量来说明是否是new调用
isInstanceof = true;
} // 通过 isInstanceof 判断该函数是否是通过new调用 (this instanceof boundFunction 的结果为 false)
return self.apply(isInstanceof ? this : bindThis, args); // 这里通过函数原型链上的apply方法对getName函数指定this执行
};
return boundFunction;
};
结尾
bind的实现就介绍到这里了,希望对你了解bind有帮助,书写不容易,觉得有帮助,可以点赞收藏。其实bind实现原理涉及到很多知识点,需要提前去复习相关知识点,才能理解其中的原理。
比如原型链,this绑定优先级,new构造函数,函数继承等,这些功能的组合才造就bind函数。基于bind,我们可以实现函数柯里化,偏函数等功能。
参考文献:
core-js
你不知道的JavaScript