你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹
bind 模拟实现
MDN对bind参数的介绍
function.bind(thisArg[, arg1[, arg2[, ...]]])
thisArg
调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
arg1, arg2, ...
当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
返回值
返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
MDN对bind的介绍是这样的
bind() 方法调用后返回一个新的函数,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
返回函数模拟实现
基于这一点,我们利用apply,实现一个简单版bind
先看第一个例子
function func() {
console.log(this.name);
}
var obj = {
name: '南一'
}
var returnFunc = func.bind(obj)
returnFunc() // 南一
- 调用
bind返回一个新函数 this被指定为bind()的第一个参数
代码实现第一步
Function.prototype.myBind = function (thisArg) {
var self = this //保存原函数
return function () {
self.apply(thisArg)
}
}
- 考虑到绑定函数可能是有返回值的,看第二个例子
function func() {
return this.name
}
var obj = {
name: '南一'
}
var returnFunc1 = func.bind(obj)
console.log(returnFunc1()); // 南一
代码实现第二步
Function.prototype.myBind = function (thisArg) {
var self = this //保存原函数
return function () {
return self.apply(thisArg)
}
}
传参模拟实现
- 传参数,调用
bind方法传参,执行bind的返回函数也可以传参,第三个例子
function func(name, age) {
console.log(name); // 北三
console.log(age); // 18
console.log(this.name); // 南一
}
var obj = {
name: '南一'
}
var returnFunc1 = f·unc.bind(obj, '北三')
returnFunc1(18)
函数func需要两个参数,调用bind方法传入参数name,调用返回函数returnFunc1传入参数age。实际上是,传给被bind和returnFunc1的参数合并后传入func函数
代码实现第三步
- 采用
arguments对象,对参数进行合并
arguments对象
arguments对象是所有(非箭头)函数中都可用的局部变量。可以使用arguments对象在函数中引用函数的参数。只作用在当前函数作用域。“类数组” 意味着
arguments有长度属性 并且属性的索引是从零开始的,但是它没有Array的 内置方法
Function.prototype.myBind = function (thisArg) {
var self = this //保存原函数
//将类数组arguments转化成数组
var args = Array.prototype.slice.call(arguments)
//去掉数组第一个元素
Array.prototype.shift.call(args)
return function () {
//将两部分参数合并
return self.apply(thisArg, Array.prototype.concat(args, Array.prototype.slice.call(arguments)))
}
}
这里频繁借用Array.prototype上面的方法,Array.prototype.slice将arguments转化成数组, Array.prototype.shift去除数组第一个元素,Array.prototype.concat合并两个数组
构造函数效果的模拟实现
- 一个绑定函数也能使用
new操作符创建对象:这种行为就像把原函数当成构造器。提供的this值被忽略,同时调用时的参数被提供给模拟函数。
第四个例子
function func(name, age) {
console.log(name); // 北三
console.log(age); // 18
console.log(this.name); // undefined
this.skill = 'sing'
}
func.prototype.friend = 'mike'
var obj = {
name: '南一'
}
var returnFunc1 = func.bind(obj, '北三')
var test = new returnFunc1(18)
console.log(test.friend); // mike
console.log(test.skill); // sing
可以看到,函数func输出this.name为undefined,说明绑定的this失效了,此时的func函数内的this是指向new运算符创建的新对象test,所以test可以拿到skill的值和在func原型上的属性。
代码实现第四步
Function.prototype.myBind = function (thisArg) {
var self = this //保存原函数
//将类数组arguments转化成数组
var args = Array.prototype.slice.call(arguments)
//去掉数组第一个元素
Array.prototype.shift.call(args)
//这个bound就是调用bind返回的函数
var bound = function () {
//将传到myBind和bound的参数,转成数组合并起来
var mergeArg = Array.prototype.concat(args, Array.prototype.slice.call(arguments))
//这里判断是用new运算符调用bound函数,还是直接调用:
//1、new调用的话, this会指向以bound为构造函数创建的新对象,所以用instanceof会找到新对象原型链上存在bound的 prototype 属性,就会是true
//2、如果是直接调用,this默认指向全局对象
if (this instanceof bound) {
var result = self.apply(this, mergeArg);
return result instanceof Object ? result : this;
} else {
//将两部分参数合并
return self.apply(thisArg, mergeArg)
}
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
bound.prototype = func.prototype
return bound
}
经过测试实现了同样的输出结果,但是这样写是正确的吗?我们来看看这几处代码
提出问题
1、多余的模拟new操作
var result = self.apply(this, mergeArg);
return result instanceof Object ? result : this;
这两句本意是判断绑定函数(self)返回的值是否为对象,是就返回此对象,否则返回新建的对象(这里this就指向新建对象)。However,没有必要这样写,这里的bound是构造函数,在构造函数内哪里需要这一步操作呢?new运算符已经帮我们做好这一步了。这里我是看到一些博客这样写,我觉得不对。
//正确写法
return self.apply(this, mergeArg);
2、调用bind的不是函数怎么办?
报错处理!!!
if (typeof this !== 'function') {
return new TypeError(this + ' is not a function')
}
3、实例原型可能被改动
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
bound.prototype = func.prototype
这里如果func.prototype被人误改了,那bound.prototype也会跟着一起改变,为了防止被改变,可以用一个空函数来中转
function Empty() { }
Empty.prototype = self.prototype;
bound.prototype = new Empty();
4、绑定函数是箭头函数怎么办呢?
if (this instanceof bound) {
//只有当用了new运算符,而且绑定函数为箭头函数,才需要抛出错误
if (!self.prototype) {
return new TypeError(`Arrow function expressions cannot be a constructor`)
}
return self.apply(this, mergeArg);
}
最终代码实现
Function.prototype.myBind = function (thisArg) {
//1.如果调用bind不是函数,抛出TypeError
if (typeof this !== 'function') {
return new TypeError(this + ' is not a function')
}
var self = this //保存原函数
//将类数组arguments转化成数组
var args = Array.prototype.slice.call(arguments)
//去掉数组第一个元素
Array.prototype.shift.call(args)
//这个bound就是调用bind返回的函数
var bound = function () {
//将传到myBind和bound的参数,转成数组合并起来
var mergeArg = Array.prototype.concat(args, Array.prototype.slice.call(arguments))
//这里判断是用new运算符调用bound函数,还是直接调用:
//1、new调用的话, this会指向以bound为构造函数创建的新对象,所以用instanceof会找到新对象原型链上存在bound的 prototype 属性,就会是true
//2、如果是直接调用,this默认指向全局对象
if (this instanceof bound) {
//只有当用了new运算符,而且绑定函数为箭头函数,才需要抛出错误
if (!self.prototype) {
return new TypeError(`Arrow function expressions cannot be a constructor`)
}
return self.apply(this, mergeArg);
} else {
//将两部分参数合并
return self.apply(thisArg, mergeArg)
}
}
//self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作
if (self.prototype) {
function Empty() { }
Empty.prototype = self.prototype;
bound.prototype = new Empty();
}
return bound
}