call方法
我们在手写实现call之前,先来了解一下call的作用
call的作用:改变this的指向,并立即执行函数
call的参数:第一个是this要指向的对象,第二个参数是参数列表
其他特性:第一个参数如果传的是null或undefined,this会指向window
let foo = {
value: 1
}
function bar() {
console.log(this.value)
}
bar(); // undefined 由于bar里面打印this.value, bar函数内没有value属性,并且window上也没有value属性,所以打印undefined
我们可以通过call方法改变this的指向,让this指向foo对象
bar.call(foo) // 1
上面的这种方式相当于在foo对象里面,调用bar函数
let foo = {
value: 1,
bar: function() {
console.log(this.value)
}
}
foo.bar() // 1
这时候 this就是指向foo,但是这样会给foo添加了一个bar属性,所以调用完需要删除它
所以我们模拟call方法的步骤:
1.将函数设置为对象的属性
2.执行该函数
3.删除该函数
手写call
我们在函数原型链上扩展一个自定义call方法
Function.prototype.myCall = function (context) {
// context就是要指向的对象
// 我们给context添加一个fn属性,值为this,这里的this就是调用myCall的函数
// 1.将函数设置为对象的属性
context.fn = this;
// 2.执行该函数
context.fn();
// 3.删除该函数
delete context.fn;
}
经过测试成功
let foo2 = {
value: 2
}
function bar2() {
console.log(this.value)
}
bar2.myCall(foo2) // 2
接着我们继续优化,call的第一个参数 如果是undefined或null,则指向window,所以需要判断一下,另外call还可以传递参数
Function.prototype.myCall = function (context) {
// context就是要指向的对象
// 这里需要做一个判断
context = context || window;
// 收集参数, 获取arguments参数,arguments是一个类数组,所以需要转换成数组,然后从第二个参数开始,因为第一个参数是指向的对象
const args = [...arguments].slice(1);
// 我们给context添加一个fn属性,值为this,这里的this就是调用myCall的函数
// 1.将函数设置为对象的属性
context.fn = this;
// 2.执行该函数, 并传递参数
context.fn(...args);
// 3.删除该函数
delete context.fn;
}
我们继续进行优化,因为函数的返回值可能是一个对象,所以需要返回这个对象
Function.prototype.myCall = function (context) {
// context就是要指向的对象
// 这里需要做一个判断
context = context || window;
// 收集参数, 获取arguments参数,arguments是一个类数组,所以需要转换成数组,然后从第二个参数开始,因为第一个参数是指向的对象
const args = [...arguments].slice(1);
// 我们给context添加一个fn属性,值为this,这里的this就是调用myCall的函数
// 1.将函数设置为对象的属性
context.fn = this;
// 2.执行该函数, 并传递参数
const res = context.fn(...args);
// 3.删除该函数
delete context.fn;
return res;
}
测试
let foo2 = {
value: 2
}
function bar2(...args) {
console.log(this.value) // 1
console.log(args) // [Arguments] { '0': [ 1, 2, 3 ] }
return {
name: 'xxx'
}
}
bar2.myCall(foo2, 1, 2)
console.log(bar2.myCall4(foo2, 1, 2)) // { name: 'xxx' }
由于context对象可能存在fn,如果删除fn可能会将原来的对象的fn删除,所以我们可以用symbol来替代fn
Function.prototype.myCall4 = function (context) {
// context就是要指向的对象
// 这里需要做一个判断
context = context || window;
// 收集参数, 获取arguments参数,arguments是一个类数组,所以需要转换成数组,然后从第二个参数开始,因为第一个参数是指向的对象
const args = [...arguments].slice(1);
// 我们给context添加一个fn属性,值为this,这里的this就是调用myCall的函数
// 1.将函数设置为对象的属性
// 用Symbol替换掉fn
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 2.执行该函数, 并传递参数
const res = context[fnSymbol](...args);
// 3.删除该函数
delete context.fn;
return res;
}
至此,手写call已实现。
apply方法
apply 与 call 的区别在于参数传递方式, apply 接受的是一个数组
手写apply
Function.prototype.myApply = function (context, args) {
// context就是要指向的对象
// 这里需要做一个判断
context = context || window;
// 我们给context添加一个fn属性,值为this,这里的this就是调用myCall的函数
// 1.将函数设置为对象的属性
// 用Symbol替换掉fn
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 2.执行该函数, 并传递参数
const res = context[fnSymbol](args);
// 3.删除该函数
delete context.fn;
return res;
}
// 测试
const foo = {
value: 1
}
function bar() {
console.log(this.value); // 1
console.log(arguments) // [Arguments] { '0': [ 1, 2, 3 ] }
return {
name: 'xxx'
}
}
console.log(bar.myApply(foo, [1,2,3])) //{ name: 'xxx' }
bind方法
先来了解一下bind的用法
let foo = {
value: 1
}
function bar() {
console.log(this.value)
console.log(...arguments) // hello world !
}
// bind会返回一个函数,并且可以分批次传入参数
const barFn = bar.bind(foo, 'hello', 'world')
barFn('!')
手写bind
Function.prototype.myBind = function (context) {
//这里的this是调用myBind的函数,即bar
// 将this保存起来
let self = this
// 获取传入的参数,除了第一个参数
// 因为arguments是类数组,它不存在slice方法,所以这里我们可以将数组原型上的slice指向arguments,这样arguments就拥有了slice方法
let args = Array.prototype.slice.call(arguments, 1)
// bind会返回一个函数
return function () {
// 将this指向context
// 因为bind返回的函数可能被调用多次,所以这里需要将参数进行合并
self.apply(context, [...args, ...arguments])
}
}
测试一下
const barFn2 = bar.myBind(foo, 'hello world', '!');
barFn2('zzz') // hello world ! zzz
bind 还有一个特性,就是可以和new一起使用,即返回的函数可以作为构造函数使用,此时bind绑定的this会失效,但传入的参数有效,此时 this指向的是实例对象,通过测试可以看到this.name为undefined
let person = {
name: "张三"
}
function personFn(age, sex) {
this.hobby = 'Sleep'
console.log(this.name) // undefined
console.log('年龄:', age) // 年龄:18
console.log('性别:', sex) // 性别:男
}
personFn.prototype.say = function () {
console.log('hello')
}
const bindPersonFn = personFn.bind(person, 18)
const bindPersonObj = new bindPersonFn('男')
console.log(bindPersonObj.hobby); // Sleep
console.log(bindPersonObj.say()); // 可以访问原型上的方法
实现上述功能
Function.prototype.myBind = function (context) {
//这里的this是调用myBind的函数,即bar
// 将this保存起来
let self = this
// 获取传入的参数,除了第一个参数
// 因为arguments是类数组,它不存在slice方法,所以这里我们可以将数组原型上的slice指向arguments,这样arguments就拥有了slice方法
let args = Array.prototype.slice.call(arguments, 1)
// bind会返回一个函数
let fnBind = function () {
// 将this指向context
// 因为bind返回的函数可能被调用多次,所以这里需要将参数进行合并
// 判断是否是new调用,如果是new调用,this 会指向实例,将绑定函数的this指向该实例,可以让实例获得绑定函数的值
if (this instanceof fnBind) {
// 如果是new调用,this指向实例对象
self.apply(this, [...args, ...arguments])
} else {
// 如果不是new调用,this指向context
self.apply(context, [...args, ...arguments])
}
}
return fnBind;
}
通过测试发现,已实现上述功能,但调用函数原型上的方法会报错
const bindPersonFn = personFn.myBind(person, 18)
const bindPersonObj = new bindPersonFn('男')
console.log(bindPersonObj.hobby); // Sleep
console.log(bindPersonObj.say()); // 此时调用 函数原型上的值会报错,这里我们还需要将函数原型指向实例对象的原型
我们还需要将返回函数的原型指向调用myBind的函数的原型
Function.prototype.myBind = function (context) {
//这里的this是调用myBind的函数,即bar
// 将this保存起来
let self = this
// 获取传入的参数,除了第一个参数
// 因为arguments是类数组,它不存在slice方法,所以这里我们可以将数组原型上的slice指向arguments,这样arguments就拥有了slice方法
let args = Array.prototype.slice.call(arguments, 1)
// bind会返回一个函数
let fnBind = function () {
// 将this指向context
// 因为bind返回的函数可能被调用多次,所以这里需要将参数进行合并
// 判断是否是new调用,如果是new调用,this 会指向实例,将绑定函数的this指向该实例,可以让实例获得绑定函数的值
if (this instanceof fnBind) {
// 如果是new调用,this指向实例对象
self.apply(this, [...args, ...arguments])
} else {
// 如果不是new调用,this指向context
self.apply(context, [...args, ...arguments])
}
}
fnBind.prototype = this.prototype
return fnBind;
}
通过测试,现在可以正常调用函数原型上的say方法
const bindPersonFn = personFn.myBind(person, 18)
const bindPersonObj = new bindPersonFn('男')
console.log(bindPersonObj.hobby); // Sleep
console.log(bindPersonObj.say()); // 正常调用 say方法
但是在这个写法中,我们直接修改了fnBind的prototype,也会直接修改绑定函数的prototype,这里可以使用空函数进行中转
最终的手写bind版本
Function.prototype.myBind = function (context) {
// 当调用myBind不是函数的时候,提示错误
if (typeof this !== 'function') {
throw new Error('TypeError')
}
//这里的this是调用myBind的函数,即bar
// 将this保存起来
let self = this
// 获取传入的参数,除了第一个参数
// 因为arguments是类数组,它不存在slice方法,所以这里我们可以将数组原型上的slice指向arguments,这样arguments就拥有了slice方法
let args = Array.prototype.slice.call(arguments, 1)
// 创建一个空函数
let fnTemp = function () { };
// bind会返回一个函数
let fnBind = function () {
// 将this指向context
// 因为bind返回的函数可能被调用多次,所以这里需要将参数进行合并
// 判断是否是new调用,如果是new调用,this 会指向实例,将绑定函数的this指向该实例,可以让实例获得绑定函数的值
if (this instanceof fnBind) {
// 如果是new调用,this指向实例对象
self.apply(this, [...args, ...arguments])
} else {
// 如果不是new调用,this指向context
self.apply(context, [...args, ...arguments])
}
}
// 让fnBind继承fnTemp的原型
fnTemp.prototype = this.prototype
fnBind.prototype = new fnTemp()
return fnBind;
}
总结
相同点
call,apply,bind都可以改变函数this的指向,且第一个参数都是要指向的对象,如果第一个参数是null或undefined,则指向全局window。
不同点
call,apply改变this指向后会立即执行,且只会临时改变this一次,而bind改变this指向后不会立即执行,而是会返回一个永久改变this指向的函数。- 参数传递方式不同,
call和bind是传递参数列表,且bind可以分多次传入,而apply是传递一个参数数组。