call、apply、bind的用法和手动实现

86 阅读3分钟

用法示例:

 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是绑定在原型上:

所以如果实例直接修改原型上面的属性,也会影响构造函数的。