【JavaScript——手写call、apply、bind】


前言

当函数调用时,会创建执行上下文,this时上下文的一个属性,在函数执行时会用到。this有四种绑定方式:默认绑定、隐式绑定、显示绑定、new绑定。显示绑定时,会用到call()、apply()、bind()函数,那这些函数是怎么实现的呢?让我们手写一下把。
this的解析参考:this全面解析

一、手写call()函数

call()函数的特点:改变this的指向,使用时传递的第一个参数是需要绑定的this,后面可以再传递多个参数。

let obj = {name: 'obj'}
let name = 'window';
function foo(...args) {
	console.log(this.name, ...args);
}
foo.call(obj, 1, 2, 3) // obj 1 2 3

根据以上逻辑,可以写出自己的call()函数:

// call 可以传递多个参数,第一个参数是this,立即执行
Function.prototype.myCall = function (context,...args) {
    context = context || window;
    // this可理解为调用call方法的函数
    // fn是新添加的属性,属性值为this(次数的this实际上指的是foo函数,谁调用指向谁)
    context.fn = this; // 隐式绑定
    // 调用方法
    context.fn(...args);
    // 删除属性
    delete context.fn;

}
let name = 'window'
let obj = {
    name: 'obj'
}
function foo(...args) {
    console.log(this.name, ...args)
}
// foo.myCall(obj, '1', '2', '3')

上面用到的隐式绑定,可以参考下面的代码:

let name = 'window';
let obj = {
	name: 'obj',
	foo: function(...args) {
		console.log(this.name, ...args);
	}
}
obj.foo() // obj

二、手写apply()函数

call()函数的特点:改变this的指向,使用时传递的第一个参数是需要绑定的this,后面可以再传递多个参数, 但是这多个参数需要以数组的方式传递。

let obj = {name: 'obj'}
let name = 'window';
function foo(args) {
	console.log(this.name, ...args);
}
foo.apply(obj, [1, 2, 3]) // obj 1 2 3

根据以上逻辑,可以发现跟call()的实现方式是一样的,只是要修改下参数的格式:

Function.prototype.myApply = function (context,args) {
    context = context || window;
    // this可理解为调用call方法的函数
    // fn是新添加的属性,属性值为this
    context.fn = this;
    // 调用方法
    args = args || [];

    context.fn(...args);
    // 删除属性
    delete context.fn;

}
// foo.myApply(obj, [1, 2, 3])

三、手写bind()函数

bind()函数的特点:
1、改变this的指向,使用时传递的第一个参数是需要绑定的this,后面可以再传递多个参数。
2、会返回一个函数,在需要的时候调用。

let obj = {name: 'obj'}
let name = 'window';
function foo(args) {
	console.log(this.name, ...args);
}
let bind = foo.bind(obj, [1, 2, 3])
bind() // obj 1 2 3

1、简单版

bind()函数相比于appy()和call()函数,不同的地方在于不会立即执行,可以在适当的时候再调用,因此,手写的bind()应该返回一个函数:

// bind 返回函数,再传入参数
Function.prototype.myBind = function (context,...args1) {
    context = context || window;
    let self = this
    let bindFn =  function (...args2) {
        let args = [...args1, ...args2];
        context.fn = self;
        // 调用方法
        context.fn(...args);
        // 删除属性
        delete context.fn;
    }
    return bindFn;
}
function person(a,b,c) {
    console.log(this.name,a, b, c);
}
let obj = {name: 'lisa'}
let bind = person.myBind(obj);
bind(1,3,3); // lisa 1 3 3

2、中级版(不使用ES6)

…扩展运算符是ES6的语言,我们用ES5经典的Array.prototype.slice.call()方法也可以实现:

// bind 返回函数,再传入参数
Function.prototype.myBind = function (context) {
    context = context || window;
    let self = this
   	if (typeof self !== "function") { // 加入了对调用函数类型的判断
    	throw new Error("cannot bind non_function");
  	}
    let args1 = Array.prototype.slice.call(arguments,1);// 将类数组转为数组
    let bindFn =  function () {
    	let args2 = Array.prototype.slice.call(arguments);
        let args = [...args1, ...args2];
        context.fn = self;
        // 调用方法
        context.fn(...args);
        // 删除属性
        delete context.fn;
    }
    return bindFn;
}
function person(a,b,c) {
    console.log(this.name,a, b, c);
}
let obj = {name: 'lisa'}
let bind = person.myBind(obj);
bind(1,3,3); // lisa 1 3 3

3、高级版(考虑new)

Function.prototype.myBind = function (obj) {
    let that = this;// Person
    let arr = Array.prototype.slice.call(arguments, 1);
    let newF = function () {
        var arr2 = Array.prototype.slice.call(arguments);
        let arrSum = arr.concat(arr2);
        if (this instanceof newF) {
            // this 是newF的实例
            that.apply(this, arrSum);
        } else {
            that.apply(obj, arrSum)
        }
    }
    // 返回的函数与Person链接起来,使实例对象能用到Person函数的属性和方法
    newF.prototype = that.prototype;
    // 下面三行是上行代码的优化:原型链共享的特性导致上行代码容易修改Person的原型,所以,需要使用new操作转接一下
    // let o = function () {}
    // o.prototype = that.prototype;
    // newF.prototype = new o;
    return newF;
}
function person(a,b,c) {
    console.log(this.name,a, b, c);
}
person.prototype.name = 'shelly'
let obj = {name: 'lisa'}
let bind = person.myBind(obj);
bind(1,3,3); // lisa 1 3 3
let baz = new bind(1,3,3) // shelly 1 3 3

总结

call()函数和apply函数含义一样,第一个参数都是传递this,不同的地方在于后面的参数形式,前者一个一个传,后者以数组的方式传递。它们调用之后会立即执行。
bind()函数相比于call()、apply(),多返回了一个方法,可以在合适的地方调用,不会立即执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值