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修改,当然为了严谨,加上也没有问题。