bind,apply,call三兄弟
实现思想🤨
皆是通过给目标对象赋值一个临时属性来改变this指向。
注意事项😱
- 对于传入的context,当为null | undefined的时候指向全局对象,剩余其它任何类型应调用Object方法。
- 使用globalThis代替window。
- 对于apply来说判断第二个参数是否为数组(类数组),不要直接遍历。
- 判断调用者类型是否为function。
- 注意bind返回的函数的length属性。
apply
//apply
Function.prototype.apply = function (context, args = []) {
if(typeof this !== 'function'){
throw new Error(this + "it's not callable");
}
//事实上args是支持一个类数组对象的,这里没有在进行细致的判断。
//类数组对象:只要有一个 `length` 属性和`(0..length-1)`范围的整数属性。
if(!Array.isArray(args)){
throw new TypeError('CreateListFromArrayLike called on non-object');
}
context = Object(context || globalThis);
const _symbol = Symbol();
context[_symbol] = this;
const result = context[_symbol](...args);
delete context[_symbol];
return result;
};
call
//call
Function.prototype.call = function (context,...args) {
if(typeof this !== 'function'){
throw new Error(this + "it's not callable");
}
//args默认值为[],这里不用进行判断。
context = Object(context || globalThis);
const _symbol = Symbol();
context[_symbol] = this;
let result = context[_symbol](...args);
delete context[_symbol];
return result;
};
bind
Function.prototype.bind = function(context,...args){
if(typeof this !== 'function'){
throw new Error(this + "it's not callable");
}
context = Object(context || globalThis);
const _symbol = Symbol();
const target = this;
return function(...rest){
context[_symbol] = target;
let result = context[_symbol](...args,...rest);
delete context[_symbol];
return result;
}
}
bind-考虑length
下述代码看不懂的可以先看一下juejin.cn/post/705567…
Function.prototype.bind = function (context, ...rest) {
if(typeof this !== 'function'){
throw new Error(this + "it's not callable");
}
let target = this;
const targetLength = Math.max(0, target.length - rest.length);
const list = [];
for (let i = 0; i < targetLength; i++) {
list[i] = `$${i}`;
}
const binder = function(){
return target.apply(context,[...rest,...arguments]);
}
return Function(
`binder`,
`return function(${list.join(",")}){
return binder(...arguments);
}`
)(binder);
};
补充
本篇文章刚发布时我并没有进行认真的测试,导致很多错误的发生😟(在此也感谢评论区的各位指正),目前我已使用了jest对上述代码进行了简单测试,确保类似的低级错误不会发生(当然肯定还会有不那么明显的错误,欢迎各位指正)。
//bind.test.js
Function.prototype.myBind = function (context, ...rest) {
if (typeof this !== "function") {
throw new Error(this + "it's not callable");
}
let target = this;
const targetLength = Math.max(0, target.length - rest.length);
const list = [];
for (let i = 0; i < targetLength; i++) {
list[i] = `$${i}`;
}
const binder = function () {
return target.apply(context, [...rest, ...arguments]);
};
return Function(
`binder`,
`return function(${list.join(",")}){
return binder(...arguments);
}`
)(binder);
};
function fn(a, b, c) {
return [a, b, c];
}
function fn1() {
return this;
}
test("bind", () => {
//常规测试
expect(fn.myBind(this, 1, 2, 3)()).toEqual(fn.bind(this, 1, 2, 3)());
//空参数测试1
expect(fn.myBind(this)()).toEqual(fn.bind(this)());
//空参数测试2
expect(fn1.myBind()()).toEqual(fn1.bind()());
//this指向测试
let objTest = { test: 111 };
expect(fn1.myBind(objTest)()).toEqual(fn1.bind(objTest)());
//多次改变this指向测试
expect(fn1.myBind(objTest).apply({ test: 222 })).toEqual(
fn1.bind(objTest).apply({ test: 333 })
);
//length测试
expect(fn.myBind(this, 1).length).toBe(fn.bind(this, 1).length);
//ToObject测试
expect(typeof fn1.myBind(1)()).toBe(typeof fn1.bind(1)());
});