call、apply、bind的JavaScript实现方法


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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值