call、apply、bind这次真的可以学懂了

288 阅读6分钟

看过很多手写call、apply的文章总能让我从入门到放弃。 直到遇到冴羽大佬的系列文章,反复看了好多遍,真上头,感觉自己又行了。本文是对[冴羽]大佬这一篇文章中学习到的call原理的总结以及在《前端开发核心知识进阶》书中提到的apply和bind原理的总结。

call实现原理

call()函数传入this和若干个参数。可以改变函数调用的this值。并且向函数中传入若干个参数。

bar.call(obj); 执行这一句代码将bar的this手动指向为obj. 根据this指向原理,谁调用this就指向谁。所以bar.call(obj)相当于obj.bar();

谁调用,this就指向谁。

  • 冴羽大佬文章中的思路真是妙呀。文章中多次巧妙的运用了this指向问题。
let obj = {
    name: '123',
    // bar: function () {
    // console.log(this.name)
    // }
}
function bar() {
    console.log(this.name)
}
function call2(context) {
    context.fn = this;//谁调用call2函数,this就指向谁
    context.fn();//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
    delete context.fn
}
Function.prototype.call2 = call2;
bar.call2(obj);

输出: 123

还有几点要完善的

  • call()函数允许传null,此时的this指向为window
  • call()函数除了传入this对象之外,还允许传入若干个参数
  • call()函数允许有返回值

解决传入值为null的问题

function call2(context) {
    var context = context || window;//解决传入为空的问题
    context.fn = this;//谁调用call2函数,this就指向谁
    context.fn();//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
    delete context.fn
}
Function.prototype.call2 = call2;

解决传入有参数问题

function call2(context) {
    var argsArr = Array.prototype.slice.apply(arguments);//伪数组转化为数组
    var context = context || window;//解决传入为空的问题
    context.fn = this;//谁调用call2函数,this就指向谁
    var args = argsArr.slice(1);
    context.fn(...args);//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
    delete context.fn
}

解决有返回值问题

function call2(context) {
    var argsArr = Array.prototype.slice.apply(arguments);
    var context = context || window;//解决传入为空的问题
    context.fn = this;//谁调用call2函数,this就指向谁
    var args = argsArr.slice(1);
    var result = context.fn(...args);//相当于正在执行bar函数,执行fn()相当于在执行bar(); 还是根据谁调用this指向谁。所以这里相当于bar()函数执行,其中this是context
    delete context.fn
    return result;
}
Function.prototype.call2 = call2;
let res = bar.call2(obj, 1, 2, 3);

apply实现原理

和call的原理类似,只是传的参数有区别

let obj = {
    name: '123'
}
function bar(...args) {
    console.log(this.name, ...args, "aefffff")
    return 1
}
function apply2(context, arr) {
    var context = context || window;//解决传入为空的问题
    context.fn = this;//谁调用call2函数,this就指向谁
    var result
    if (arr && arr.length) {
        result = context.fn(...arr);
    } else {
        result = context.fn();
    }
    delete context.fn
    return result;
}

Function.prototype.apply2 = apply2;
let res = bar.apply2(obj, [1, 2, 3]);

但是这里需要注意,如果apply上本来就存在fn函数。在使用apply函数时,原有的 属性值会被覆盖,之后会被删除,为了保证键的唯一性,可以使用ES6 的Symbol

参考代码如下

function apply2(context, arr) {
    var context = context || window;//解决传入为空的问题
    context.fn = this;//谁调用call2函数,this就指向谁
    var fn = Symbol();
    var result
    if (arr && arr.length) {
        result = context[fn](...arr);
    } else {
        result = context[fn]();
    }
    delete context[fn]
    return result;
}

Function.prototype.apply2 = apply2;

bind

bind()  方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

bind方法与call,apply的区别

  • bind()返回的是函数
  • 调用bind返回的函数的时候可以向该函数传递参数(柯里化)

第一版 利用apply实现bind函数

function bind2(context) {
    var currentThis = this;//谁调用bind1 this就指向谁, 此时this指向bar
    var arrayArgs = Array.prototype.slice.call(arguments);
    return function () {
        return currentThis.apply(context, arrayArgs.slice(1));//闭包 利用apply再改变bar函数的指向
    }
}
Function.prototype.bind2 = bind2;
function bar() {
    return this.name;
}
let obj = {
    name: 'xxxxxx'
}
let bindFn = bar.bind2(obj);
console.log(bindFn());//输出'xxxxxx'

第一版函数中借用闭包和apply函数实现了一个初级版bind()函数。但是别忘了bind()返回的函数是可以接收参数的。

第二版 让bind返回的函数可以接收参数

这里需要利用柯里化将上面向bindFn()的参数和调用bind()函数的参数做个拼接,如下面所示。

function bind2(context) {
    var currentThis = this;//谁调用bind1 this就指向谁, 此时this指向bar
    var arrayArgs = Array.prototype.slice.call(arguments);
    return function () {
        var bindFnArgs = Array.prototype.slice.call(arguments);
        var args = arrayArgs.slice(1).concat(bindFnArgs);
        return currentThis.apply(context, args);//闭包 利用apply再改变bar函数的指向
    }
}
Function.prototype.bind2 = bind2;
function bar(...arg) {
    return this.name;
}
let obj = {
    name: 'xxxxxx'
}
let bindFn = bar.bind2(obj);
console.log(bindFn(11));//输出'xxxxxx'

但是试着用new bindFn(),将bind()返回的函数作为构造器使用,如下面代码所示;


let obj = {
    name: "小花"
}
function bar() {
    console.log(this.name, "this")//bar实例
}
let barFun = bar.bind(obj);
let barInstance = new barFun();

输出:此时输出undefined, 明明我传的obj对象进去,却找不到obj.name说明此时this指向已经发生了改变,其实此时的this已经指向barInstance实例对象了。

image.png

用第二版的bind2方法执行同样的代码,此时的this一样指向obj对象,说明第二版的bind函数还存在问题。

image.png

第三版 像"bind()"函数一样,解决new关键字调用问题。

先看看new 操作符调用构造函数的时候做了哪些事情

  • 创建一个新的对象
  • 将构造函数的this指向这个新的对象
  • 为这个对象添加属性、方法。
  • 最终返回一个新对象 伪代码如下
var obj = {};
obj._proto_=Foo.prototype
Foo.call(obj)

看看new 构造函数的实例到底指向谁

let obj = {
    name: "小花"
}
function bar() {
    console.log(this.name, "this")//bar实例
}
bar.prototype.name = 'bar上的小花'
let barFun = bar.bind(obj);
let barInstance = new barFun();
console.log(barInstance)

image.png

好家伙,barInstance的constructor竟然是bar函数,那如果是new 调用的情况将bind.prototype==bar.prototype不就可以了

Function.prototype.bind2 = function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var bound = function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof me ? this : context || this, finalArgs);//
    }
    bound.prototype = this.prototype; // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    return bound;
}

然后用这一版的函数执行上面相同的代码,和上面输出的结果一样

image.png

我们在看看这样改变bind2原型实例上的name属性值

let obj = {
    name: "小花"
}
function bar() {
    console.log(this.name, "this")//bar实例
}
bar.prototype.name = 'bar上的小花'
let barFun = bar.bind2(obj);
barFun.prototype.name = "我修改了bind原型上的name值";
console.log(barFun.prototype, 'baFunnnnnnnnnnnnnnn');
console.log(bar.prototype, 'bar')

输出结果如下:上面我们只修改了barFun.prototype.name,却发现bar.prototype上面的name也被修改了。 image.png

用一个空的F构造函数缓存this原型链,代码如下面所示

Function.prototype.bind2 = function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var F = function () { }
    F.prototype = this.prototype
    var bound = function () {
        console.log(this, "this")
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(this instanceof me ? this : context || this, finalArgs);
    }
    bound.prototype = new F(); // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    return bound;
}

输出如下:bar的原型链没有被污染 image.png

用Object.create同样可以解决上面的问题

  Function.prototype.bind2 = function (context) {
            var me = this;
            var args = Array.prototype.slice.call(arguments, 1);
            var bound = function () {
                var innerArgs = Array.prototype.slice.call(arguments);
                var finalArgs = args.concat(innerArgs);
                return me.apply(this instanceof me ? this : context || this, finalArgs);//
            }
            // bound.prototype = this.prototype; // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
            bound.prototype = Object.create(this.prototype);
            return bound;
        }

上面call,apply,bind函数实现得差不多了,感谢[冴羽]大佬的文章。