call的JavaScript实现
1、call是如何运作的
首先call方法就是改变this指向,并且立即调用
function person() {
console.log(this.name)
}
const zs = {
name: '张三'
}
person.call(zs)
其实也就相当于将person函数放进zs对象中,然后调用person函数:
const zs = {
name: '张三',
person: function () {
console.log(this.name)
}
}
zs.person()
2、如何用JavaScript实现
(1)在函数的原型链上挂载一个函数作为自己的call方法
function person() {
console.log(this.name)
}
const zs = {
name: '张三'
}
Function.prototype.myCall = function () {
// 注意,函数的this指向的是person,是整个person函数
console.log(this)
}
// 调用我们自己的函数
person.myCall()
(2)传入被指向的对象
// 类比call传入对象的方式
person.call(zs)
function person() {
console.log(this.name)
}
const zs = {
name: '张三'
}
Function.prototype.myCall = function (zs) {
// 如上面所说,此时的this就是整个person函数,相当于把person函数作为一个方法写进了zs对象
zs.fn = this
// 调用zs对象里面的person方法
zs.fn()
// 删除掉这个方法,不要破坏原有的对象结构
delete zs.fn
}
person.myCall(zs)
// 以上就可以大致实现该改变this指向,但是却不能实现传递参数的功能,所以还得继续
(3)实现传递参数的功能
第一种方法:扩展运算符…arguments
function person(a, b, c) {
console.log(this.name, a, b, c)
}
const zs = {
name: '张三'
}
Function.prototype.myCall = function (zs, ...args) {
zs.fn = this
zs.fn(...args)
delete zs.fn
}
person.myCall(zs, 1, 2, 3)
第二种方法:eval函数
eval函数会将传入的字符串当做 JavaScript 代码进行执行。
这里解释一下为什么要用eval函数
// 我们最终想要达到的目的是一个个的传入参数,就像以下
zs.fn(1,2,3)
// 我们想到可以创建一个空数组
let newArgs = []
// 除第一个参数外,将之后的所有参数推入新的空数组
for (let i = 1; i < arguments.length; i++) {
newArgs.push(arguments[i])
}
// 将数组转成字符串
let str = newArgs.join(",")
zs.fn(str)
// 但是结果却不是我们想要的,参数仅仅只是占了第一个位子
// 输出:张三 1,2,3 undefined undefined
// 所以就得用到eval函数
function person(a, b, c) {
console.log(this.name, a, b, c)
}
const zs = {
name: '张三'
}
Function.prototype.myCall = function (zs) {
zs.fn = this
// 创建一个空数组用于存放传递的参数
let newArgs = []
// 除第一个参数外,将之后的所有参数推入新的空数组
for (let i = 1; i < arguments.length; i++) {
newArgs.push(arguments[i])
}
// eval函数会将里面的字符串作为js代码执行
eval("zs.fn(" + newArgs.join(",") + ")")
delete zs.fn
}
person.myCall(zs, 1, 2, 3)
其中拼接字符串这里还可以进一步化简:
eval("zs.fn(" + newArgs + ")")
// 或者用模版字符串拼接
// eval(`zs.fn(${newArgs})`)
可以不用join来实现数组转字符串的原因,是因为数组拼接一个字符串会自动将数组也变成字符串
console.log(newArgs + "")
// 输出:1,2,3
至此,我们自己的call方法已经完成八成,还有一个点得考虑到,那就是this的指向为null的情况
(4)null的情况
// 如果传入的是null,那么就会出现报错
person.myCall(null, 1, 2, 3)
解决办法,将传入的对象或上window
function person(a, b, c) {
console.log(this.name, a, b, c)
}
const zs = {
name: '张三'
}
Function.prototype.myCall = function (zs) {
// 解决传入null报错问题
zs = zs || window
zs.fn = this
// 创建一个空数组用于存放传递的参数
let newArgs = []
// 除第一个参数外,将之后的所有参数推入新的空数组
for (let i = 1; i < arguments.length; i++) {
newArgs.push(arguments[i])
}
// eval函数会将里面的字符串作为js代码执行
eval("zs.fn(" + newArgs + ")")
// 或者用模版字符串拼接
// eval(`zs.fn(${newArgs})`)
delete zs.fn
}
person.myCall(zs, 1, 2, 3)
(5)返回值问题
function person(a, b, c) {
console.log(this.name, a, b, c)
return 123
}
...
console.log(person.myCall(zs, 1, 2, 3))
// 输出:
// 张三 1 2 3
// undefined
那么怎么养才能获取到函数的返回值呢
function person(a, b, c) {
console.log(this.name, a, b, c)
return a
}
const zs = {
name: '张三'
}
var name = '李四'
Function.prototype.myCall = function (zs) {
zs = zs || window
zs.fn = this
let newArgs = []
for (let i = 1; i < arguments.length; i++) {
newArgs.push(arguments[i])
}
// 这里的函数执行就是上面的person函数,那么返回值也就是person的返回值
let result = eval("zs.fn(" + newArgs + ")")
delete zs.fn
return result
}
console.log(person.myCall(zs, 1, 2, 3))
(6)解决传入的参数不是数字类型的值报错问题
console.log(person.myCall(zs, {}, 2, 3))
// 此时就会报错
首先我们要明确,我们想要传递参数的形式是怎么样的
zs.fn('{}','2','3')
也就是
zs.fn(arguments[1], arguments[2], arguments[3])
但我们不知道传递多少个参数所以不能够写死,得灵活的调用arguments相对应的参数,这也就是为什么要用到eval。
但是使用eval的前提条件是()里面是一个字符串形式的,也就是以下形式:
"zs.fn(arguments[1], arguments[2], arguments[3])"
接下来就是要利用上面的newArgs来动态的获取每一个arguments的值
newArgs = [arguments[1], arguments[2], arguments[3]]
经过字符串拼接,会自动调用toString方法,
"zs.fn("+newArgs+")"
但是这里有一个问题,上面的获得的数组其实会直接显示参数,也就是
newArgs = ['{}','2','3']
那么拼接字符串之后就会变成
"zs.fn({},2,3)"
和最上面的我们想要传递的参数形式对比,括号里面参数没有了引号,这也就是报错的原因,如何解决?
同样的,用字符串的方式隐藏掉参数值
newArgs.push("arguments[" + i + "]")
那么,数组就会变成
newArgs = ['arguments[1]', 'arguments[2]', 'arguments[3]']
经过字符串拼接之后会变成
"zs.fn(arguments[1], arguments[2], arguments[3])"
以上就解决了传递参数只能是数字的问题
其实用扩展运算符的方法可以很简单的实现
function person(a, b, c) {
console.log(this.name, a, b, c)
return a
}
const zs = {
name: '张三'
}
Function.prototype.myCall = function (zs, ...args) {
zs = zs || window
zs.fn = this
const result = zs.fn(...args)
delete zs.fn
return result
}
console.log(person.myCall(zs, {}, 2, 3))
apply的JavaScript实现
apply和call不同之处在于,apply总共就两个参数,第一个是this的指向,第二个就是传递的参数,传递的参数是用数组包裹
(1)在call的基础上,增加传递的数组,注意要判断一下有没有参数传进来,没有的话直接就执行person函数
(2)apply中循环遍历的已经不是arguments了,而是传递进来的arrArgs,且i是从0开始,要遍历所有的参数,这里要格外注意
(3)result也不能在函数中声明,而是要在全局声明,初始值为null,然后在局部进行赋值
function person(a, b, c) {
console.log(this.name)
console.log(a, b, c)
return a
}
const zs = {
name: '张三'
}
Function.prototype.myApply = function (obj, arrArgs) {
obj = obj || window
obj.fn = this
let newArgs = []
let result = null
if (!arrArgs) {
result = obj.fn()
} else {
for (let i = 0; i < arrArgs.length; i++) {
newArgs.push("arrArgs[" + i + "]")
}
result = eval("obj.fn(" + newArgs + ")")
}
delete obj.fn
return result
}
console.log(person.myApply(zs, [{}, 2, 3]))
bind的JavaScript实现
1、bind与call和apply都不同的是,bind返回的是函数
function person() {
console.log(this.name)
}
const zs = {
name: '张三'
}
// 如果仅仅是person.bind(zs),是没有输出结果的,因为直接把函数返回了
person.bind(zs)()
2、传递参数和call相同,是一个个的传递
function person(a, b, c) {
console.log(this.name)
console.log(a, b, c)
}
const zs = {
name: '张三'
}
// 注意这里的参数传递非常灵活,可以分开传递
person.bind(zs, 1, 2)(3)
接下来就实现自己的bind方法
(1)首先return一个函数
function person() {
console.log(this.name)
}
const zs = {
name: '张三'
}
Function.prototype.myBind = function (obj) {
// 这里的this指向person函数
console.log(this)
return function () {
// 这里的this指向window
console.log(this)
}
}
person.myBind(zs)()
因为return出来的函数是指向window的,所以得在外面声明一个变量用于保存指向person函数的this
function person() {
console.log(this.name)
}
const zs = {
name: '张三'
}
Function.prototype.myBind = function (obj) {
// 保存this
let that = this
return function () {
// 用apply而不用call是因为apply更加方便,只要传一个数组就可以了
that.apply(obj)
}
}
person.myBind(zs)()
// 输出:张三
// 这里可以直接用apply的方法,因为我们已经知道了apply的原理,无需再写一遍
当然这里利用箭头函数也可以解决问题,但是这里还是利用ES5的方法
(2)传递参数问题
同样的,我们不需要第一个参数this,而是后面的,所以需要进行切割。但要注意,arguments其实是对象而不是数组,所以不能直接用切割数组的方法,而是需要用call方法,把slice切割方法赋给arguments对象
function person(a, b, c) {
console.log(this.name)
console.log(a, b, c)
}
const zs = {
name: '张三'
}
Function.prototype.myBind = function (obj) {
let that = this
let args1 = Array.prototype.slice.call(arguments, 1)
console.log(args1)
return function () {
that.apply(obj, args1)
}
}
person.myBind(zs, '吃饭', '睡觉', '看电影')()
但是这只能解决在第一个括号里面可以传递参数,第二个不可以,如何解决?
function person(a, b, c) {
console.log(this.name)
console.log(a, b, c)
}
const zs = {
name: '张三'
}
Function.prototype.myBind = function (obj) {
let that = this
let args1 = Array.prototype.slice.call(arguments, 1)
let args2 = null
console.log(args1)
return function () {
// 注意这里的arguments和上面的不一样,上面的是person.myBind第一个括号内的参数,要除去第一个this指向
// 而这里的arguments是person.myBind第二个括号内的参数,没有this指向的参数
args2 = Array.prototype.slice.call(arguments)
console.log(args2)
// 直接将两个数组进行拼接
that.apply(obj, args1.concat(args2))
}
}
person.myBind(zs, '吃饭', '睡觉')('看电影')
// 以上就可以实现两个括号都可以传递参数,但是我们得把两个地方传的参数合并起来,不然第一个括号内的参数会出现undefined,因为被后面的参数占据了位子,利用concat