手写系列之call、apply、bind

关于call、apply或bind函数,它们主要用来改变this指向,我们经常在许多地方都能看到,特别是一些框架源码中,而且面试官也特别喜欢让你模拟实现这些函数,我之前的文章[ this是谁? ]中介绍了this指向时,在显示绑定场景这块就使用了这些函数,有兴趣的可以看看这边文章。

接下来就让我们看看如何模拟实现这些函数~

一、模拟实现 call 函数

1、语法:
function.call(thisArg, arg1, arg2, ...)
 

第一个参数 thisArg 可选的,表示在 function 函数运行时要绑定 this 值的对象,如果在非严格模式下传入 null 或 undefined 则指向 window。后面参数为执行函数需要的参数,也是可选。

2、例子:
var namee = "黎奔";
let obj = {
    namee: 'liben'
}
function getName(x, m) {
    console.log(">>>>", `${this.namee}:姓${x}${m}`)
}
getName('黎', '奔')  // 黎奔:姓黎名奔
getName.call(obj, "li", "ben") // liben:姓li名ben
 

可以看出使用 call 函数显示的改变了 this 指向了obj 对象。可能细心的人看出了 namee 变量是使用的 var 声明的,为何不用 let 或 const 声明呢,因为使用 let 或 const 声明的变量不会挂载到window,当不使用 call 调用函数,this 会指向 window 从而导致找不到变量打印出 undefined。

3、如何实现:

从上面的例子我们可以了解到要手动实现需要注意以下几点:

  • call 是绑定在Function的原型上的

  • 会立即执行

  • 参数是可选

实现原理:

就是为对象 obj 添加需要调用的方法,接着调用该方法(此时 this 指向 obj),调用过后再删除该方法。

需要注意的是 this绑定的优先级 => [箭头函数 > new > 显式 > 隐式 > 默认绑定],call 属于显示用箭头函数和 new 优先级就不能变了,所以只能用隐式要改变this指向了。

4、实现代码:
Function.prototype._call = function(context, ...args) {

    // 如果context不传,this就默认绑定到window
    const ctx = context || window;

    // 这里this指调用函数
    ctx.fn = this;
    // console.log(this)

    // 立即执行方法
    const res = ctx.fn(...args);

    delete ctx.fn;  // 为何要delete? 不删则绑定的ctx对象会添加一个fn方法进来可能产生副作用

    return res;  // 这里为何要return出去结果呢? _call相当于包装器,ctx.fn函数得到的结果也要return出去

}
 
5、验证:
// 验证例子
let age = 28;
let objCall = {
    age: 23
}
function getAge(x, m) {
    console.log(">>>>call", `姓${x}${m}:年龄${this.age}`)
    return x + m;
}
getAge._call(objCall, "aa", "bb")  // >>>>call 姓aa名bb:年龄23
getAge._call(objCall) // >>>>call 姓undefined名undefined:年龄23
getAge._call(null) // >>>>call 姓undefined名undefined:年龄undefined
console.log(getAge("77", "88")) // >>>>call 姓77名88:年龄undefined  7788

console.log(getAge._call(objCall, 1, 2))  // >>>>call 姓1名2:年龄23  3

console.log(">>>delete?", objCall) // 不过不delete 则objCall对象会添加一个fn方法
 

这里我们就自己模拟实现了一个 call() 函数啦,如果有疑问可以一步步分别打印看看注释是什么意思哦~

二、模拟实现 apply 函数

1、语法:
func.apply(thisArg, [argsArray])
 

第一个参数 thisArg 可选的,表示在 function 函数运行时要绑定 this 值的对象,如果非严格模式下传入 null 或 undefined 则指向 window。后面参数为执行函数需要的参数,也是可选。

注意与 call 的区别:就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

2、例子:
var namee = "黎奔";
let obj = {
    namee: 'liben'
}
function getName(x, m) {
    console.log(">>>>", `${this.namee}:姓${x}${m}`)
}
getName('黎', '奔')  // 黎奔:姓黎名奔
// getName.call(obj, "li", "ben") // liben:姓li名ben
getName.apply(obj, ["黎", "奔"]) // liben:姓黎名奔

getName.apply()   //  >>>> vn:姓undefined名undefined
 
3、如何实现:

实现原理跟 call 一样,区别是第二个参数必须是一个数组

4、实现代码:
Function.prototype._apply = function(context, args) {

    // 如果context不传,this就默认绑定到window
    const ctx = context || window;

    // 这里this指调用函数
    ctx.fn = this;

    // 这里参数为数组
    const res = ctx.fn(...args);

    delete ctx.fn;

    return res;

}
 

与 call 实现的区别就在与参数 args 的使用。

5、验证:
// 验证例子
var addr = "深圳";
let objApply = {
    addr: "岳阳"
}
function getAddress(a, b) {
    console.log(">>>>apply", `现居:${this.addr} - 国籍:${a} - 省:${b}`)
    // return a + b;
}
getAddress("china", "广东")  >>>>apply 现居:深圳- 国籍:china - 省:广东
getAddress._apply(objApply, ["china", "广东"]) // >>>>apply 现居:岳阳 - 国籍:china - 省:广东
getAddress._apply(null, ["china", "广东"])  // >>>>apply 现居:undefined - 国籍:china - 省:广东
 

三、模拟实现 bind 函数

1、语法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
 

第一个参数 thisArg 可选的,表示在 function 函数运行时要绑定 this 值的对象,如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。后面参数为执行函数需要的参数,也是可选。

如果函数需要传 name 和 age 两个参数,竟然还可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age

2、例子:
var a = "a";
let bindObj = {
    a: "bat"
}
function getA(x, y) {
    console.log(">>>>", `${this.a} - ${x} - ${y}`)
    return x + y
}
getA("你", "好")
getA.bind(bindObj, "你", "好")()  // >>>> bat - 你 - 好
getA.bind(bindObj)("你", "好")  // >>>> bat - 你 - 好  同上
getA.bind(bindObj, "可以", "挺狠")("你", "好") // >>>> bat - 可以 - 挺狠
getA.bind(bindObj, "nb")("你", "好") // >>>> bat - nb - 你 会忽略多余的参数
console.log(getA(1, 2))   // 3

// bind返回作为构造函数
let GetAFun = getA.bind(bindObj, 7, 8)
let getAFunIns = new GetAFun()  // >>>> undefined - 7 - 8 按理应该this.a应该返回bat 但这里返回undefined说明绑定失败
 
3、如何实现:
  • 函数定义在Function原型

  • 不立即执行,返回调用的函数

  • 参数是可选

4、实现代码:
Function.prototype._bind = function(context, ...args) {

    // 如果context不传,this就默认绑定到window
    const ctx = context || window;

    ctx.fn = this;
    // console.log(this)

    return function(...subArgs) {

        // return ctx.fn(...args.concat(subArgs))  // 可以先在bind()传入一部分,再执行返回时传入另外参数

        const res = ctx.fn(...args.concat(subArgs))

        delete ctx.fn;

        return res;

    }
}
 

实现原理就是返回一个绑定了this和带有参数的函数,这里 ctx.fn(…args.concat(subArgs)) 可以借助 call 或 apply 实现,就不需要delete 函数了,但是我们不是要模拟吗,还借助 call 可能会懵,哈哈~

5、验证:
var b = "bbbb";
var _bindObj = {
    b: "nb"
}
function getB(x, y) {
    console.log("_bind>>>>", this.b, x, y)
    return x + y;
}
getB(1, 2)
getB._bind(_bindObj, 3, 4)() // _bind>>>> nb 3 4
getB._bind(_bindObj)(5, 6) // _bind>>>> nb 5 6
console.log(getB._bind(_bindObj, 3, 4)())  // 7
console.log(_bindObj)  // {b: "nb", fn: ƒ}

let GetBFun = getB._bind(_bindObj, "liben")
let getBFunIns = new GetBFun('菜鸡啊')  // _bind>>>> nb liben 菜鸡啊 这里this.b应该指向undefined才对
 

这里当我们把 bind 返回作为构造函数时,发现跟官方的实现有差异,this 指向出现了问题,接下来看看最终优化版本如何写。

6、bind终极版:

参考【 MDN Polyfill 究极优化版本 】

Function.prototype.newbind = function(context, ...args) {

    // 如果不是函数调用bind
    if (typeof this !== "function") {
        throw new Error("调用bind的不是函数~")
    }

    // 如果context不传,this就默认绑定到window
    const ctx = context || window;

    // 调用函数
    const self = this;

    const fNOP = function() {};
    const fBound = function(...subArgs) {
        return self.call(this instanceof fNOP ? this : ctx, ...args.concat(subArgs))
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
 

验证例子

var c = "cccc";
var newbindObj = {
    c: "今晚我来C"
}
function getC(x, y) {
    console.log("newbind>>>>", this.c, x, y)
    return x + y;
}
let GetCFun = getC.newbind(newbindObj, "liben")
let getCFunIns = new GetCFun('菜鸡啊')  // newbind>>>> undefined liben 菜鸡啊
 

本文是笔者总结编撰,如有偏颇,欢迎留言指正,若您觉得本文对你有用,不妨点个赞~

关于作者:
GitHub
简书
掘金

(0)
上一篇 2021年5月31日 下午6:21
下一篇 2021年6月1日 下午3:58

相关推荐

发表评论

登录后才能评论