深入JavaScript(4)call、aplly、bind;手写函数的实现

121 阅读4分钟

1.手写call

Function 实例的 call()  方法会以给定的 this 值和逐个提供的参数调用该函数。

我们先看看call的使用和作用

let foo = {
    value: 1
}

function bar() {
    console.log(this.value);
}

bar.call(foo) // 1

有两点:

  • call改变了this的指向,指向到foo
  • bar函数执行了

1.1 第一版

let foo = {
    value: 1,
    bar: function () {
        console.log(this.value)
    }
};

foo.bar(); // 1

这是我们模拟的一个方法,这个时候this就指向foo,我们再用delete删除foo新增的bar属性即可。

拆解步骤为:

  • 将函数设为对象的属性;
  • 执行函数;
  • 删除该函数;

以代码形式展示就是

foo.fn = bar
foo.fn()
delete foo.fn

根据以上思路,写出第一版:

// 第一版
Function.prototype.myCall1 = function (ctx) {
    ctx.fn = this
    ctx.fn()
    delete ctx.fn
}

let foo = {
    value: 1
}

function bar() {
    console.log(this.value);
}

bar.myCall1(foo) // 1

1.2 第二版

call除了指定this,还可以给定参数

let foo = {
    value: 1
}

function bar(a,b) {
    console.log(this.value);
    console.log(a);
    console.log(b);
}

bar.call(foo,'aaa','bbb')
// 1
// aaa
// bbb

那第二版我们就可以使用剩余参数语法,将除了第一个参数的其余参数,以数组形式拿到

// 第二版
Function.prototype.myCall2 = function (ctx, ...rest) {
    ctx.fn = this
    ctx.fn(...rest)
    delete ctx.fn
}

let foo = {
    value: 1
}

function bar(a,b) {
    console.log(this.value);
    console.log(a);
    console.log(b);
}

bar.myCall2(foo,'aaa','bbb')
// 1
// aaa
// bbb

1.3 第三版

this参数有可能为null,undefined,甚至于不是Object,那这种时候,视为指向全局的this

var value = 1

function bar() {
    console.log(this.value)
}

bar.call(null) // 1

并且函数是可以实现返回值的

let foo = {
    value: 1
}

function bar(a, b) {
    console.log(this.value); // 1
    return a + b
}

console.log(bar.call(foo, 2, 3)); // 5

根据这两个要求,写出第三版

// 第三版
Function.prototype.myCall3 = function (ctx, ...rest) {
    ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
    ctx.fn = this
    const result = ctx.fn(...rest)
    delete ctx.fn
    return result
}

let foo = {
    value: 1
}

function bar(a, b) {
    console.log(this.value); // 1
    return a + b
}

console.log(bar.myCall3(foo, 2, 3)); // 5

1.4 最终版

根据第三版进行优化,使用symbol确保key的唯一性

// 第四版
Function.prototype.myCall = function (ctx, ...rest) {
    ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
    const key = Symbol("temp")
    ctx[key] = this
    const result = ctx[key](...rest)
    delete ctx[key]
    return result
}

let foo = {
    value: 1
}

function bar(a, b) {
    console.log(this.value); // 1
    return a + b
}

console.log(bar.myCall(foo, 2, 3)); // 5

2. 手写apply

Function 实例的 apply()  方法会以给定的 this 值和作为数组(或类数组对象)提供的 arguments 调用该函数。

apply和call近乎一样,我们可以以call的最终版进行修改

// apply
Function.prototype.myApply = function (ctx, arr) {
    if (arr !== null && arr !== undefined && typeof arr[Symbol.iterator] !== 'function') {
        throw new Error('第二个参数必须为iterator对象')
    }
    ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
    const key = Symbol("temp")
    ctx[key] = this
    const result = arr ? ctx[key](...arr) : ctx[key]()
    delete ctx[key]
    return result
}

var value = 0
let foo = {
    value: 1
}

function bar(a, b) {
    console.log(this.value); // 1
    return a + b
}

console.log(bar.myApply(foo, [2, 3])); // 5

3.手写bind

Function 实例的 bind()  方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。

var foo = {
    value: 1
};
function bar() {
    console.log(this.value);
}
// 返回了⼀个函数
var bindFoo = bar.bind(foo);
bindFoo(); // 1

可以看出bind有两个特点:

  • 返回一个函数
  • 可以传入参数

3.1 第一版

var value = 2
var foo = {
    value: 1
}

function bar(a, b) {
    this.habit = 'shopping'
    console.log(this.value);
    console.log(a);
    console.log(b);
}


// 第一版
Function.prototype.myBind1 = function (ctx) {
    const fn = this
    return function () {
        return fn.apply(ctx)
    }
}
const bindBar = bar.myBind1(foo)
bindBar()

3.2 第二版

接下来,关于参数的传递

var foo = {
    value: 1
};
function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18

换而言之,就是参数其实可以拼接起来的concat

那根据这个思路写出第二版的代码

var value = 2
var foo = {
    value: 1
}

function bar(a, b) {
    this.habit = 'shopping'
    console.log(this.value);
    console.log(a);
    console.log(b);
}
// 第二版
Function.prototype.myBind2 = function (ctx) {
    const fn = this
    const args = Array.prototype.slice.call(arguments, 1)
    return function () {
        const bindArgs = Array.prototype.slice.call(arguments)
        return fn.apply(ctx, args.concat(bindArgs))
    }
}
const bindBar = bar.myBind2(foo, 12)
bindBar(13)

3.3 第三版

bind还有一个特点,当bind返回的函数作为构造函数的时候,bind时指定的this值就会失效,但传入的参数依然生效。

var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

尽管在全局和foo中都声明了value值,但还是打印undefined,说明绑定的this已经失效了。

通俗来讲,其实就是this的绑定失效,this指回构造函数的实例上

根据这个思路写出第三版

var value = 2
var foo = {
    value: 1
}

function bar(a, b) {
    this.habit = 'shopping'
    console.log(this.value);
    console.log(a);
    console.log(b);
}
// 第三版
Function.prototype.myBind3 = function (ctx) {
    const fn = this
    const args = Array.prototype.slice.call(arguments, 1)
    const fBound = function () {
        const bindArgs = Array.prototype.slice.call(arguments)
        return fn.apply(this instanceof fBound ? this : ctx, args.concat(bindArgs))
    }
    fBound.prototype = this.prototype
    return fBound
}
const bindBar = bar.myBind3(foo, 22)
const binBarNew = new bindBar(33)

3.4 第四版

第三版已经很完整了,但是考虑到我们直接将 fBound.prototype = this.prototype,那我们直接修改fBound.prototype的时候,也会直接修改绑定函数的prototype。

所以我们的思路是通过一个空函数来进行中转。

// 第四版
Function.prototype.myBind4 = function (ctx) {
    const fn = this
    const args = Array.prototype.slice.call(arguments, 1)
    const fNop = function () { }
    const fBound = function () {
        const bindArgs = Array.prototype.slice.call(arguments)
        return fn.apply(this instanceof fNop ? this : ctx, args.concat(bindArgs))
    }
    // fBound.prototype = this.prototype
    fNop.prototype = this.prototype
    fBound.prototype = new fNop()
    return fBound
}

3.5 最终版

该版主要是预处理一些错误提示;

  • 调用bind的不是函数时,提示错误。
  • 绑定的ctx为null或undefined的时候,this指向全局
// 最终版
// 防止调用该函数的this为非函数
Function.prototype.myBind = function (ctx) {
    if (typeof this !== 'function') {
        throw new Error("Function.prototype.myBind - what is trying to be bound is not callable")
    }
    if (ctx === undefined || ctx === null) ctx = globalThis
    const fn = this
    const args = Array.prototype.slice.call(arguments, 1)
    const fNop = function () { }
    const fBound = function () {
        const bindArgs = Array.prototype.slice.call(arguments)
        return fn.apply(this instanceof fNop ? this : ctx, args.concat(bindArgs))
    }
    // fBound.prototype = this.prototype
    fNop.prototype = this.prototype
    fBound.prototype = new fNop()
    return fBound
}