前言
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
最近看到很多人说各种手撕 call、apply、bind,这边也是做了没多久的前端,看了逻辑确实挺简单,怪不得那么多人手撕,虽然平时用的少吧,不过多了解一些总是没有坏处的,下面我用自己的理解手撕一下
ps:其使用形式或者结果,是不是很像某些语言的扩展或者分类呢,只不过角色转换了而已,从被动加载到指定类,通过对象调用,到主动生成,让其像调用工具类一样实现作为对象的函数调用😂
手撕前的准备
他们大概主要利用了下面几个关键点:
- this指向
- prototype原型
- 函数指针
下面先简单介绍一下上面三个:
1. this指向:
这个也是js面试热点,不过很多东西比较杂,个人从别的编程语言学到的东西来解释相对会一目了然(js底层函数调用逻辑猜测和其他的类似,仅供参考)
我们通过class对象调用其的方法时,实际上隐式传入了该对象,被命名为 this,例如
class Runner {
//一般都是直接给对象,赋与参数太灵活了,typescript就没这个问题了
name = ''
//我们开发看到的
run(runName) {
print(runName)
}
//系统实际调用时,会调用下面结构的隐藏方法,会将this传递为调用者,所以this才能直接调用其内部方法
run(this, runName) {
print(runName)
}
}
可能此时有人会明白,箭头函数的不同了,没错 箭头函数本身没有this,箭头函数里面使用的 this 实际上是使用的外层的 this,因此箭头函数的 this 表现形式一般就是父级对象或者箭头函数所属类,最外层就是 window了
因此我们总结:this默认指向调用者,箭头函数指向 父级对象 或者 函数所属类
2. prototype原型
prototype为原型的意思,所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法
Date对象从Date.prototype继承。Array对象从Array.prototype继承。Person对象从Person.prototype继承。function对象从Function.prototype继承(函数实际上也是一个特殊的对象)
因此我们自定义原型加入内容时,使用的原型对象也会自动继承该内容
3. 函数指针
函数指针,即:指向调用函数的指针,可以传递参数直接执行,由于是直接指向函数实现,所以调用时只能按照定义函数的实现来走
间接形成的结果就是:无法走对象调用时传递this的过程,因此直接通过函数指针调用函数时,this不存在,这是需要注意的(解决方案,函数指针指向我们自定义的函数,间接通过指定对象调用指定方法即可,后面介绍)
手撕call、apply、bind
介绍前,我们先定义两个类,以及介绍这几个函数基础使用
call:Function的原型函数,让自己可以调用其他人的函数,只不过将第一个参数改为传入调用的对象,函数其他参数按照顺序传递即可
apply:Function的原型函数,让自己可以调用其他人的函数,只不过将第一个参数改为传入调用的对象,函数其他参数使用数组的方式顺序传递
bind:Function的原型函数,让自己可以调用其他人的函数,默认传递调用者(自己),返回可调用函数指针,直接当函数调用即可
基础,使用如下所示
class Person {
name = ""
age = 0
constructor(name, age) {
this.name = name
this.age = age
}
printInfo(otherInfo) {
console.log('--name:', this.name, "\n-- age:", this.age)
otherInfo && console.log('--otherInfo:', otherInfo)
}
}
class Animal {
name = ""
age = 0
constructor(name, age) {
this.name = name
this.age = age
}
}
let person = new Person('marshal', 20)
let animal = new Animal('dog', 2)
//系统的案例测试
person.printInfo('basePersonInfo\n')
person.printInfo.call(animal, 'animal-call\n')
person.printInfo.apply(animal, ['animal-apply\n'])
let aniPrint = person.printInfo.bind(animal)
aniPrint('animalPrint-bind\n')
call
我们定义原型函数,名字为 myCall,实现如下所示
前面介绍了this之前,知道了该方法是函数调用的,因此this指向了前面的函数,我们使用时,给要调用的对象增加一个函数,函数指针指向前面的函数,通过调用对象调用该函数即可完成即可,由于一次性,使用完记得删除该函数
//第二个参数为需要展开的多个参数
Function.prototype.mycall = function (ctx, ...args) {
//不存在就是赋值 globalThis 表示window,node下为 global,ctx可能为数字之类的,包装成对象
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
//为了避免键值可能存在的重复,使用 Symbol
const key = Symbol();
//重新定义一个新的属性 symbol,禁止遍历,不然在执行函数过程中遍历的话,可能会获取到该临时symbol
Object.defineProperty(ctx, key, {
value: this,
enumerable: false,
});
const res = ctx[key](...args);
//使用完毕后删除
delete ctx[key];
return res;
};
apply
我们定义原型函数,名字为 myApply,实现如下所示,只不过传递的args是一个数组罢了
//第二个参数直接传递的数组
Function.prototype.mycall = function (ctx, args) {
//不存在就是赋值 globalThis 表示window,node下为 global,ctx可能为数字之类的,包装成对象
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
//为了避免键值可能存在的重复,使用 Symbol
const key = Symbol();
//重新定义一个新的属性 symbol,禁止遍历,不然在执行函数过程中遍历的话,可能会获取到该临时symbol
Object.defineProperty(ctx, key, {
value: this,
enumerable: false,
});
const res = ctx[key](args);
//使用完毕后删除
delete ctx[key];
return res;
};
bind
我们定义原型函数,名字为 myBind,实现如下所示
这里面碰到的问题,不能直接返回 target[symbol]而是返回了一个闭包,相信了解了前面函数指针就知道为什么了,返回一个闭包可以间接通过调用者 target 调用该方法,且 this 指向 target;如果直接返回 target[symbol] ,那么调用时 this 将不存在
//第二个参数为需要展开的多个参数,实际上会发现可以使用apply内容,我们不直接使用apply方法
Function.prototype.mybind = function (ctx, ...args) {
//保留函数,避免后续用到错误的函数
const fn = this;
//返回一个函数
return function (...outArgs) {
//可以通过 new.target 查找是否是new创建的,new创建的返回对象,否则返回undefined
if (new.target) {
//说明调用了new
return new fn(...args, ...outArgs);
}
//return fn.apply(ctx, [...args, ...outArgs])
//不存在就是赋值 globalThis 表示window,node下为 global,ctx可能为数字之类的,包装成对象
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
//为了避免键值可能存在的重复,使用 Symbol
const key = Symbol();
//重新定义一个新的属性 symbol,禁止遍历,不然在执行函数过程中遍历的话,可能会获取到该临时symbol
Object.defineProperty(ctx, key, {
value: fn,
enumerable: false,
});
const res = ctx[key](...args, ...outArgs);
delete ctx[key]; //由于返回的函数指针,可能多次调用,所以不能用完删除
return res
};
};
测试源码
//第二个参数为需要展开的多个参数
Function.prototype.mycall = function (ctx, args) {
//不存在就是赋值 globalThis 表示window,node下为 global,ctx可能为数字之类的,包装成对象
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
//为了避免键值可能存在的重复,使用 Symbol
const key = Symbol();
//重新定义一个新的属性 symbol,禁止遍历,不然在执行函数过程中遍历的话,可能会获取到该临时symbol
Object.defineProperty(ctx, key, {
value: this,
enumerable: false,
});
const res = ctx[key](args);
//使用完毕后删除
delete ctx[key];
return res;
};
//第二个参数直接传递的数组
//第二个参数直接传递的数组
Function.prototype.mycall = function (ctx, args) {
//不存在就是赋值 globalThis 表示window,node下为 global,ctx可能为数字之类的,包装成对象
ctx = ctx === null || ctx === undefined ? globalThis : Object(ctx);
//为了避免键值可能存在的重复,使用 Symbol
const key = Symbol();
//重新定义一个新的属性 symbol,禁止遍历,不然在执行函数过程中遍历的话,可能会获取到该临时symbol
Object.defineProperty(ctx, key, {
value: this,
enumerable: false,
});
const res = ctx[key](args);
//使用完毕后删除
delete ctx[key];
return res;
};
//第二个参数为需要展开的多个参数
Function.prototype.mybind = function (ctx, ...args) {
//保留函数,避免后续用到错误的函数
const fn = this;
//返回一个函数
return function (...outArgs) {
//可以通过 new.target 查找是否是new创建的,new创建的返回对象,否则返回undefined
if (new.target) {
//说明调用了new
return new fn(...args, ...outArgs);
}
//上面不省略,这里省略了
return fn.apply(ctx, [...args, ...outArgs])
};
};
class Person {
name = ""
age = 0
constructor(name, age) {
this.name = name
this.age = age
}
printInfo(otherInfo) {
console.log('--name:', this.name, "\n-- age:", this.age)
otherInfo && console.log('--otherInfo:', otherInfo)
}
}
class Animal {
name = ""
age = 0
constructor(name, age) {
this.name = name
this.age = age
}
}
let person = new Person('marshal', 20)
let animal = new Animal('dog', 2)
//系统的测试结果
person.printInfo('basePersonInfo\n')
person.printInfo.call(animal, 'animal-call\n')
person.printInfo.apply(animal, ['animal-apply\n'])
let aniPrint = person.printInfo.bind(animal)
aniPrint('animalPrint-bind\n')
//自定义测试结果
person.printInfo.myCall(animal, 'animal-call\n')
person.printInfo.myApply(animal, ['animal-apply\n'])
let aniMyPrint = person.printInfo.myBind(animal)
aniMyPrint('animalMyPrint-bind\n')
最后
大家一起尝试一下吧,艺多不压身,this指向是以其他编程语言背后原理,作为一个猜想引入的,方便理解,实际上不一定为真,但是有助于理解方便记忆是真的😂,我们一起进步吧!