call
call 函数改变函数执行时的 this 指向并立即执行,可以分开传递参数给函数。
/**
* 实现一个 call,改变 this 指向并立即执行,分开传递传参
* 1. 任意一个方法都能调用 call 函数;
* 2. 该方法中的 this 修改为 call 函数的第一个参数;
* 3. 调用该方法,并将 call 函数的参数分别传递进去;
* @param {Object} context
* @return
*/
Function.prototype.myCall = function (context) {
context = context ? Object(context) : window; // context 为 null 或 undefined 时,this 指向 window,普通值转换为对象
context.fn = this; // 将 this 代表的函数定义到新上下文的属性上
let args = [];
for (let i = 1, len = arguments.length; i < len; i++) { // 注意 arguments 包含了所有参数,所以要去除第一个参数
args.push(arguments[i]);
} // 将参数转换为数组
let result = context.fn(...args); // 传入参数,执行上下文的函数,通过 this 机制的隐式绑定,函数中的 this 指向执行上下文
delete context.fn;
return result;
}
console.log(Math.min.myCall(myCreate(null), 1, 4, 2, 8));
apply
apply 函数改变函数执行时的 this 指向并立即执行,可以传递数组形式参数给函数。
/**
* 实现一个 apply,改变 this 指向并立即执行,传递数组传参
* 1. 任意一个方法都能调用 apply 函数;
* 2. 该方法中的 this 修改为 apply 函数的第一个参数;
* 3. 调用该方法,并将 apply 函数的参数分别传递进去;
* @param {Object} context
* @return
*/
Function.prototype.myApply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this; // 将 this 代表的函数定义到新上下文的属性上
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn;
return result;
}
console.log(Math.min.myApply(myCreate(null), [1, 4, 2, 8]));
bind
bind,与 call 和 apply 功能类似,只是不立即执行,而是返回一个更换了 this 指向的函数。可以调用该函数并传参,只有 new 调用才能改变函数的 this 指向。
原理
/*
* 实现 bind,与 call 和 apply 功能类似,只是不立即执行,而是返回一个更换了 this 指向的函数。可以调用该函数并传参,只有 new 调用才能改变函数的 this 指向。
* 返回一个函数,绑定 this,传递预置参数
* bind 返回的函数可以作为构造函数使用。故作为构造函数时应使得 this 失效,但是传入的参数依然有效
*/
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('bind 只能用于函数');
}
let target = this;
let args = Array.prototype.slice.myCall(arguments, 1);
var fn = function () {};
let bound = function () {
let newArgs = Array.prototype.slice.myCall(arguments);
console.log(target, target.constructor);
// 1.this.constructor取的实例对象的原型上的属性。
// 2.判断是否等于构造函数用 target,而不是 fn。因为虽然new 执行的是 fn,但实际内部执行的方法是 target。
// 3.如果是 new 方式调用则用当前 this,否则用 context 作为上下文。
return target.myApply(this instanceof bound ? this : context, args.concat(newArgs));
};
// 上面仅仅只是改了new 调用的 this 指向,还得构造原型链。即新函数的原型对象为旧函数的原型对象。
// 不过做一下寄生中转,以防被旧函数的实例通过 __proto__ 修改原型。但其实还是可以通过 _proto__.__proto__ 修改原型的,只是稍微做一下防护。
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fn.prototype = this.prototype;
}
// 下⾏的代码使bound.prototype是fNOP的实例,因此
// 返回的bound若作为new的构造函数,new⽣成的新对象作为this传⼊bound,新对象的__proto__就是fn的实例
bound.prototype = new fn();
return bound;
}
// let min = Math.min.myBind(myCreate(null), 1);
// console.log(new min(4, 2, 8));
// var z = 0;
// var obj = {
// z: 1
// };
// function fn(x, y) {
// this.name = '听风是风';
// console.log(this.z);
// console.log(x);
// console.log(y);
// };
// fn.prototype.age = 26;
// var bound = fn.myBind(obj, 2);
// var person = new bound(3); //undefined 2 3
// console.log(person.name); //听风是风
// console.log(person.age); //26
// person.__proto__.age = 18;
// var person = new fn();
// console.log(person.age); //26
应用
业务场景一 表单校验
结合 Vue3 + TS + Element-Plus。
自定义校验规则校验手机号码。因为 Element-Plus 的表单校验函数只能接收 rule, value, cb 三个参数,当使用 TS 进行类型限制时,无法传入业务需要的其它参数,可以通过 bind 函数调用返回一个已经有初始化参数的校验函数,即将业务需要的参数作为初始化参数进行赋值。
// 业务参数
const errorMessage = ref('')
const rules = reactive<FormRules>({
phone: [
{
required: true,
trigger: [],
// bind 返回的函数已经去除初始化参数,所以 TS 类型校验正确
validator: validatePhoneNumber.bind(this, errorMessage)
}
]
})
// 手机号校验
export function validatePhoneNumber(
message: Ref,
rule: any,
value: any,
cb: any
) {
if (message.value !== '') {
cb(new Error(message.value))
} else if (value === '') {
cb(new Error('请输入手机号'))
} else if (!regExpPhone.test(value)) {
cb(new Error('手机号格式错误'))
} else {
cb()
}
}
validatePhoneNumber 原函数形参个数,为4。
validatePhoneNumber 通过 bind 创建的新函数形参个数,为 3。说明已经去除初始化传入的参数,所以符合 validator 的 TS 参数声明。
PS:除以上方法之外,还可以通过以下方法传递自定义校验参数
const errorMessage = ref('')
const rules = reactive<FormRules>({
phone: [
{
required: true,
trigger: [],
validator: validator: (rule, value, callback) => {
validatePhoneNumber(errorMessage, rule, value, callback)
}
}
]
})