用法示例:
var name='张三',age = 18;
const obj = {
name:'李四',
age:28
}
function fn(a,b){
console.log(this.name+'年龄是:'+this.age)
console.log(a+b)
}
fn(1,2) // 张三年龄是: 18 3
fn.call(obj,1,2) // 李四年龄是: 28 3
fn.apply(obj,[1,2]) // 李四年龄是: 28 3
fn.bind(obj,1)(2) // 李四年龄是: 28 3
相同点:都是改变this指向的方法
不同点:
- 参数传递方式不同,第一个参数都是this的新指向,但apply只有两个参数,第二个参数为一个数组,需要传输的参数值须全部放到数组中。而call、bind一样,参数用逗号分开。
- call,apply返回的是值,bind返回一个新的函数,并且接受剩余的参数,要再次调用才生效。
手动实现call、apply、bind
call方法实现
Function.prototype.call_ = function (obj) {
// 第一个入参判断,如果是null/undefined,this指向window
obj = obj ? Object(obj) : window
obj.fn = this
// 利用拓展运算符直接将arguments转为数组,并获取除了第一个参数外的所有参数
let args = [...arguments].slice(1)
obj.fn(...args)
delete obj.fn // 删除增加的属性,不然obj属性会越来越多
}
const obj = { age: 18 }
function fn(x, y, z) {
console.log(x + y + z)
console.log(this.age)
}
fn.call_(obj,1,2,3) // 6 18
注意,这里的call_是我们模拟的call方法,我们来解释模拟方法中做了什么。
- 我们通过Function.prototype.call_的形式绑定了call_方法,所有函数都可以直接访问call_。
- fn.call_属于this隐式绑定,所以在执行时call_时内部this指向fn,这里的obj.fn = this就是将方法fn赋予成了obj的一条属性。
- obj现在已经有了fn方法,执行obj.fn,因为隐式绑定的问题,fn内部的this指向obj。
- 最后通过delete删除了obj上的fn方法,毕竟执行完不删除会导致obj上的属性越来越多。
apply方法实现
Function.prototype.apply_ = function (obj, arr) {
obj = obj ? Object(obj) : window
obj.fn = this
let result
// 判断是否是数组
if (!Array.isArray(arr)) {
result = obj.fn()
} else {
result = obj.fn(...arr)
}
delete obj.fn
return result
}
const obj = { age: 18 }
function fn(x, y, z) {
console.log(x + y + z)
console.log(this.age)
}
fn.apply_(obj,[1,2,3]) // 6 18
解释同上,只是传参形式不一样
bind方法实现
Function.prototype.bind_ = function (obj) {
// 非函数调用会报错 if (typeof this !== 'function') {
throw new Error('这是一个非函数调用!')
}
// 保存初始的this
var fn = this
// 创建中间函数,为了给构造函数调用改变属性时多增加一层__proto__
var fn_ = function () {}
// 保存初始参数
var args = [...arguments].slice(1)
var bindFn = function () {
// 保存剩余的参数
// 判断this指向,构造函数和普通函数指向不一样,构造函数指向的是实例对象,普通函数指向的是obj
fn.apply(this.constructor === fn ? this : obj, [...args, ...arguments])
}
// 原型链继承
fn_.prototype = fn.prototype
bindFn.prototype = new fn_()
return bindFn
}
var z = 0;
var obj = { z: 1 }
function fn(x, y) {
this.name = '张三';
console.log(this.z);
console.log(x);
console.log(y)
};
fn.prototype.age = 26
var bound = fn.bind_(obj, 2)
bound(3) // 1 2 3
// 构造函数调用
var person = new bound(3) // undefined 2 3
console.log(person)
// 实例改变原型,不会改变构造函数,因为我们加了一层中间函数(不加就会改变,或者__proto__.__proto__也会改变,大家有兴趣的可以自己去尝试一下)
person.__proto__.age = 18
var person = new fn3()
console.log(person.age) //26
为什么需要加中间函数,不难理解,构造器属性(this.name,)在创建实例时,相当于实例深拷贝了一份,这就是属于实例本身的属性,后面再改与构造函数无关,而实例要用prototype属性时都是顺着原型链上找,构造函数有,就能使用它。我们输出person,可以看出age是绑定在原型上:
所以如果实例直接修改原型上面的属性,也会影响构造函数的。