js手写call(),apply(),bind() (this 绑定问题)

this绑定

关于实现call(),bind(),apply()最离不开的话题便是this绑定问题

this默认绑定

this的默认绑定也称window绑定,是下面四种绑定之外的this绑定,出现于在js中无任何前缀直接调用函数。例如:

// 非严格模式下
function fun() {
	console.log(this)
}
fun()					// window对象

需要注意的是,在严格模式下,默认绑定的this指向的是undefined。

'use strict';
function fun() {
	console.log(this)
}
fun()					// undefined

this new绑定

博主之前的博文中以前解释过new的实现过程。js 手写new,对于new中的this绑定,指向的是新创建的对象。

function Person() {
	console.log(this instanceof Person)
	this.name = '张三'
}
Person()								// false
let p = new Person()					// true
console.log(p.name)						// 张三

可以看出执行Person()时,this指向的是window,执行new Person()时,this指向的new关键字创建出的新对象,这对于后文bind()的实现有关键作用。

this 箭头函数绑定

es6的箭头函数this的绑定指向取决于外层作用域的函数this指向,外层作用域的函数指向哪,箭头函数的this就指向哪。箭头函数的this无法用call(),apply(),bind()修改。修改箭头函数this的值只能通过修改外层作用域的函数this指向。

var name = '王五'
function fn() {
	console.log('outer: ' + this.name)
  return () => {
  	console.log('inner: ' + this.name)
  }
}
let p1 = {
  name: '张三'
}
let p2 = {
	name: '李四'
}
fn()()						// outer: 王五 inner: 王五
let f = fn.call(p1)	
f.()						// outer: 张三 inner: 张三
f.call(p2)					// outer: 张三 inner: 张三

可以看出第一次外层this和内层this都指向window对象,使用call()修改外层fn指向后,内层的this随着外层fn的指向变化而变化,第三次欲利用call修改内层this指向,但箭头函数只受外层作用域的函数this指向变化,因此修改不成功。

this隐式绑定

若函数在调用时,前面存在调用该函数的对象,this就会绑定到该对象上。

function fn() {
    console.log(this.name);
};
let obj = {
    name: '张三',
    func: fn
};
obj.func()					// 张三

隐式绑定中,离调用方法更近的对象才会作为this的指向。

let obj = {
	name: '张三'
}
let obj1 = {
	name: '李四',
	o: obj
}
console.log(obj1.o.name)					// 张三

this显式绑定

显式绑定指的是通过call(),bind(),apply()方法来修改函数的this指向。

let obj1 = {
    name: '张三'
};
let obj2 = {
    name: '李四'
};
let obj3 = {
    name: '王五'
}
var name = '赵六';

function fn() {
    console.log(this.name);
};
fn()						// 赵六
fn.call(obj1); 				// 张三
fn.apply(obj2); 			// 李四
fn.bind(obj3)(); 			// 王五

其中,call(context,…args)与apply(context,[arguments])比较类似,都是将函数this绑定并执行,call()与apply()后面的参数传递是调用call的函数需要传递的参数;不同的是,call()后面的参数需一个个写进去,并用逗号隔开,apply()后面的参数需包装成数组的形式再传入。而bind()传参与call()一样,但返回的类型是个函数,需加上’()'才能执行。bind()绑定后的函数不可再用显示绑定修改this指向,若修改,则this指向不变。

this绑定优先级

显式绑定 > 隐式绑定 > 默认绑定级
new绑定 > 隐式绑定 > 默认绑定
先使用new绑定再使用显式绑定会报错。

function Person(){
    this.name = '张三';
};
let p = {
    name:'李四'
}
let p1 = new Person().call(p);//报错 call is not a function

先使用bind()再使用new绑定this指向

let p1 = {name: '张三'}
let person = function() {
	this.name = '李四'
}
let pf = person.bind(p1)
let p2 = new pf()
console.log(p2.name)					// 李四

显式绑定 > 隐式绑定

let obj = {
    name:'张三',
    sayName:function () {
        console.log(this.name);
    }
};
obj1 = {
    name:'李四'
};
obj.sayName.call(obj1);					// 李四

new绑定 > 隐式绑定

p = {
    name: '张三',
    sayName: function () {
        this.name = '李四';
    }
};
let person = new p.sayName();
console.log(person.name);							// 李四

隐式与默认这里不做比较,自行测试。

call()实现

Function.prototype.myCall = function(context) {
	// 隐式绑定 this指向调用call的函数
	if(typeof this !== 'function') {
		console.err('type err')
	}
	// 取出参数	result作为接收执行调用call()函数的函数的返回值
	let args = [...args].slice(1),
		result = null
	// 判断是否传入this,若未传入或传入undefined,null,则将执行默认绑定
	context = context || window
	// 将调用函数设为对象方法,为下一步利用隐式绑定修改this做准备
	context.fn = this
	// 执行函数;接收返回值;隐式绑定修改this
	result = context.fn(...args)
	// 删除属性
	delete context.fn
	return reslut
}

原理:利用隐式绑定判断调用call()的类型,并利用隐式绑定修改this绑定。
这里其实也可以解释为什么显式绑定大于隐式绑定的优先级了,因为context绑定fn属性并调用后,context比调用call()函数的函数离方法更近,因此call()优先级大于隐式绑定(下面两个方法同理)。

apply()实现

Function.prototype.myApply(context) {
	// 同上
	if(typeof this !== 'function') {
		throw new TypeError("Error");
	}
	let result = null
	context = context || window
	context.fn = this
	if(auguments[1]) {
		result = context.fn(...auguments[1])
	} else {
		result = context.fn()
	}
	delete context.fn
	return result
}

bind()实现

Function.prototype.myBind = function(context) {
	if(typeof this !== 'function') {
		throw new TypeError("Error");
	}
	// 获取参数 保留调用bind()的函数
	var args = [...arguments].slice(1),
		// 这里this是外层函数
		fn = this
		return function Fn() {
			// 这里this是内层函数里的this,也是bind()绑定后的函数
			// 后为函数柯里化,为将调用bind()函数的参数可以写在bind()的括号里
			return fn.apply(this instanceof Fn ? this : context,args.concat(...auguments))
		}
}

这里可能最难理解的就是第二个return里的语句了。由于js内置的bind()绑定函数再经过new关键字之后,this还是指向创建后的实例对象,因此这里结合下面代码(上文 this new绑定处)就好理解一点了:

function Person() {
	this.name = '张三'
}
let obj = {
	name: '李四'
}
let Fn = Person.bind(obj)
let fn = new Fn()
console.log(fn.name)					// 张三

这里可以发现new绑定之后this指向的是创建后的对象。

function Person() {
	console.log(this instanceof Person)
	this.name = '张三'
}
Person()								// false
let p = new Person()					// true
console.log(p.name)						// 张三

可以发现若函数经过new处理,this会指向创建后的新对象,自然this(创建后的对象)就是bind()返回函数的实例对象,于是第二个return中的代码可以理解为,若new后创建的对象(内层函数中的this)为bind()执行后返回的函数的实例对象(即bind()创建后的函数经过作为构造函数),就将this绑定给new创建的this,否则就绑定给传入的对象。
其实很多博客并没有考虑new这个情况,其实就个人观点来看,这里不考虑其实也没有问题,因为后执行new会自动将this修改,当然为了严谨,加上也没有问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值