这篇文章主要探究 call、apply、bind 的实现原理,对于使用方法不做太多概述。如果有不太了解的同学可以查看
     
      JavaScript MDN 官方文档 call apply bind 详解
     
    
   
    
    
    前言
   
我们都知道 call、apply、bind 三个方法是用来改变 this 指向的,但是也只局限于会用状态。具体的实现原理,却没有思考过。所以有必要研究一下。在明白起原理的情况下,才能用的更好。
    
    
    具体实现 call
   
    众所周知这三个方法都是提供给 函数使用的,函数通过这三个方法调用把 this 传入进去就可以改变其 this 指向。通过这个特性我们可以推断出: 这三个方法都是写在
    
     Function
    
    对象的原型上的。也就是
    
     Function.prototype
    
    上
   
    下边我们先在这个原型上写一个自己的 call 方法就叫
    
     newCall
    
    :
   
function person () {
  console.log(this.name)
}
let obj = {
  name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
  console.log(this) // 这里的this指向的是 person 函数
  console.log(obj) // {name: '渣渣辉'}
}
person.newCall(obj)
    运行上边的代码,我们会发现 newCall 原型方法里的 this 指向的是
    
     person
    
    函数, 因为 newCall 这个方法是定义在
    
     Function
    
    对象原型上的,并且是 person 函数调用了 newCall 方法,这就印证了那句话,谁调用了这个方法 this 就指向谁。然后只要在 newCall 方法中想办法执行 this 指向的这个 person 函数就行。
   
然后我们可以再 obj 这个对象上添加一个属性,然后把 this 指向这个属性,然后在执行这个属性就可以了: 代码如下
function person () {
  console.log(this.name)  // 渣渣辉
}
let obj = {
  name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
  console.log(this)
  console.log(obj)
  // 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
  obj.fn = this
  // 执行 fn 函数
  obj.fn()
  // 防止作用域污染,使用完之后删除
  delete obj.fn
}
person.newCall(obj)
    这时候 顶部的 person 函数中
    
     console.log(this.name)
    
    执行的结果就是
    
     渣渣辉
    
基本的 call 的操作已经完成了,但是还需要完善一下:
- call() 调用的时候 第一个参数一般是 this 或者一个对象,或者是 null 或者直接省略不传,这种情况下this 的值将会被绑定为全局对象。
- call(this, 1, 2, 3, 4) 接受一个参数列表
根据上边的特性再完善一下 newCall 方法:
function person (a, b, c, d) {
  console.log(this.name) // 渣渣辉
  console.log(a, b, c, d) // 1, 2, 3, 4
}
let obj = {
  name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
  // 如果参数为 为 null ,则默认为 window, 即访问全局作用域对象
  var obj = obj || window
  // 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
  obj.fn = this
  // 截取作用域对象参数后面的参数
  var args = [...arguments].slice(1)
  // 执行 fn 函数
  obj.fn(...args)
  // 防止作用域污染,使用完之后删除
  delete obj.fn
}
// person.newCall(obj)
person.newCall(obj, 1, 2, 3, 4)
现在离成功又进了一步,但是有时候被call 调用的那个函数也是有返回值的,所以还需要完善一下
function person (a, b, c, d) {
  console.log(this.name) // 渣渣辉
  console.log(a, b, c, d) // 1, 2, 3, 4
  // 有返回值
  return {
    name: this.name,
    a: a, b: b, c: c, d: d
  }
}
let obj = {
  name: '渣渣辉'
}
Function.prototype.newCall = function (obj) {
  // 如果参数为 为 null ,则默认为 window, 即访问全局作用域对象
  var obj = obj || window
  // 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
  obj.fn = this
  // 截取作用域对象参数后面的参数
  var args = [...arguments].slice(1)
  // 执行 fn 函数
  var result = obj.fn(...args)
  // 防止作用域污染,使用完之后删除
  delete obj.fn
  // 执行完成后返回结果
  return result
}
// person.newCall(obj)
// person.newCall(obj, 1, 2, 3, 4)
var res = person.newCall(obj, 1, 2, 3, 4)
console.log(res)  // {name: "渣渣辉", a: 1, b: 2, c: 3, d: 4}
    到此,newCall 方法基本上已经完成了, 上边的写法中
    
     obj.fn(...args)
    
    使用了 ES6 的扩展运算符,但是 call 方法在没有扩展运算符的年代就已经实现了,所以我们有必要在探究一下, 最终找到一个方法
    
     eval()
    
    方法运行 js 然后得到结果,最终版本如下
   
Function.prototype.newCall = function (obj) {
  // 如果参数为 为 null ,则默认为 window, 即访问全局作用域对象
  var obj = obj || window
  // 在 obj 对象上添加一个属性 fn, 并且绑定 this (也就是 person) person 函数是 引用数据类型,是存在堆空间,栈空间通过指针指向 堆空间
  obj.fn = this
  
  // ES 5 的实现方法 ===============
  var args = []
  for (var i = 1; i < arguments.length; i++) {
    args.push('arguments['+ i +']')
  }
  var result = eval('obj.fn('+ args +')')
  // ===================
  //ES 6 ================
  // 截取作用域对象参数后面的参数
  // var args = [...arguments].slice(1)
  // // 执行 fn 函数
  // var result = obj.fn(...args)
  // ===================
  // 防止作用域污染,使用完之后删除
  delete obj.fn
  // 执行完成后返回结果
  return result
}
    
    
    apply
   
    说完了call 再说下 apply ,其实这两个方法功能基本上都是相同的,不同的地方是传参方式不同,
    
    call 接收一个参数列表作为参数, apply 方法接受的是一个包含多个参数的数组。差别不大我们直接把 实现的 newCall 方法拿过来改改就好了,所以这里不在过多叙述实现过程了,直接上代码:
   
Function.prototype.newApply = function (obj, arr) {
  var obj = obj || window
  var result
  obj.fn = this
  if (!arr) {
    obj.fn()
  } else {
    // ES 5 实现方式 =========
    var args = []
    for (var i = 0; i < arr.length; i++) {
      args.push('arr['+ i +']')
    }
    result = eval('obj.fn('+ args +')')
    // =========
    // ES6 =========
    // result = obj.fn(...arr)
    // =========
  }
  delete obj.fn
  return result
}
    
    
    bind
   
- bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用
- 
     bind() 绑定的函数 支持
 
 new 实例化
 
完整代码如下:
Function.prototype.newBind = function (obj) {
  var self = this
  // 截取绑定时的参数
  var arge1 = Array.prototype.slice.call(arguments, 1)
  // 定义中专构造函数,用于通过原型连接绑定后的函数和调用 bind 的函数
  var F = function () {}
  // 定义返回的新函数
  var newF = function () {
    // 截取调用时的参数
    var arge2 = Array.prototype.slice.call(arguments)
    // 参数合并
    var argeSum = arge1.concat(arge2)
    // 支持 new 实例化, 判断是否使用 new 关键字
    // 改变作用域,注:aplly/call是立即执行函数,即绑定会直接调用
    if (this instanceof F) {
      return self.apply(this, argeSum)
    } else {
      return self.apply(obj, argeSum)
    }
  }
  // 将调用函数的原型赋值到中专函数的原型上
  F.prototype = self.prototype
  // 通过原型的方式继承调用函数的原型
  newF.prototype = new F
  return newF
}
这是《JavaScript Web Application》一书中对 bind() 的实现:通过设置一个中转构造函数 F,使绑定后的函数与调用 bind() 的函数处于同一原型链上,用 new 操作符调用绑定后的函数,返回的对象也能正常使用 instanceof,因此这是最严谨的 bind() 实现
然后我们来验证一下
function person (a, b, c, d) {
  console.log(this.name) // 渣渣辉
  console.log(a, b, c, d) // 1, 2, 3, 4
  // 有返回值
  return {
    name: this.name,
    a: a, b: b, c: c, d: d
  }
}
let obj = {
  name: '渣渣辉'
}
person.newBind(obj, 1, 2, 3)(4)
var bb = person.newBind(this, '点赞', '收藏')
var aa = new bb('充电')
// ==================
var people = {
  name: '张三',
  getName: function (age) {
    return this.name + age
  }
}
var temp = people.getName
var context = temp.newBind(people)
console.log(context('18岁')) // 张三18岁
都能正确运行。
 
