JavaScript 改变 this call apply bind

410 阅读5分钟

call、apply、bind

  • call、apply 直接调用函数,临时改变
  • call
    • 函数体内的this指向call方法传入的第一个实参
    • call方法后续的实参会依次传入作为原函数的实参传入
  • apply
    • 调用时将剩余的实参以一个数组的方式传参
    • apply与call功能一致
  • bind
    • 不会调用函数,返回新的函数
    • 新函数里 this 会指向之前绑定的对象

call()

call方法使用的语法规则

  • 参数:对象,指定调用时函数中this的指向

函数名称.call( obj,arg1,arg2...argN )

  • this指向call方法传入的第一个实参,后续的实参会依次传入作为原函数的实参传入
let lisi = { names: 'lisi' }
let zs = { names: 'zhangsan' }
function f(age) {
  console.log(this.names)
  console.log(age)
}
f(23) //undefined 23

//将f函数中的this指向固定到对象zs上;
f.call(zs, 32) //zhangsan 32
function setDetails(name, color) {
  this.name = name
  this.color = color
}
let cat1 = {}
let cat2 = {}
setDetails.call(cat1, '大毛', '橘色')
setDetails.call(cat2, '二毛', '黑色')
console.log(cat1.name) //大毛
console.log(cat2.name) //二毛
let person1 = {
  name: 'zs',
  say: function (hobby) {
    console.log(this.name)
    console.log('爱好:' + hobby)
  },
}
let person2 = {
  name: 'ls',
}
person1.say('打游戏') // zs 爱好:打游戏
person1.say.call(person2, '健身') // ls 爱好:健身

实现call

// 第三版
Function.prototype.call2 = function (context) {
  var context = context || window
  context.fn = this

  let arg = [...arguments].slice(1)
  let result = context.fn(...arg)

  delete context.fn
  return result
}

// 测试一下
var value = 2

var obj = {
  value: 1,
}

function bar(name, age) {
  console.log(this.value)
  return {
    value: this.value,
    name: name,
    age: age,
  }
}

bar.call2(null) // 2

console.log(bar.call2(obj, 'kevin', 18))
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

简化版

Function.prototype.call2 = function (context, ...args) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  let fnSymbol = Symbol()
  context[fnSymbol] = this
  let fn = context[fnSymbol](...args)
  delete context[fnSymbol]
  return fn
}

过程

  • 在原型链上挂上我们自定义的call2方法,让所有函数共享此方法
Function.prototype.call2 = function () {
  console.log(this)
}
setDetails.call2()
  • call2中通过this拿到调用call2的原函数

  • this指向点'.'前面的对象(被调用的函数)

Function.prototype.call2 = function (context) {
  //this === 原函数
  console.log(this)

  //将原函数作为cat1的方法调用
  context.setDetails = this
  context.setDetails()
}
  • 其实将原函数作为context的方法调用时,方法名并不影响功能,将方法名写死反而可能会造成方法名冲突

    • 在ES6中可以用Symbol来解决

    • 可以随机生成一个基本不可能冲突的字符串,万一冲突则继续生成到不冲突为止

  • 在方法调用后删除方法,避免给context增加多余的方法

  • 原函数可能有返回值,要将改变this并调用后的返回值也返回

Function.prototype.call2 = function (context) {
  function mySymbol(obj) {
    let unique = (Math.random() + new Date())
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj) //如果还是冲突,递归调用
    } else {
      return unique
    }
  }
  let uniqueName = mySymbol(context)
  //this === 原函数
  //console.log(this);

  //将原函数作为context的方法调用
  context[uniqueName] = this
  let result = context[uniqueName]()
  //用完删除
  delete context[uniqueName]
  return result
}
  • 改进解决参数传递

    • 通过arguments或者剩余参数来获取除了context剩余的参数

    • 将剩余参数传递给context[uniqueName]

Function.prototype.call2 = function (context) {
  function mySymbol(obj) {
    let unique = (Math.random() + new Date())
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj) //如果还是冲突,递归调用
    } else {
      return unique
    }
  }
  let uniqueName = mySymbol(context)

  //获取除了第一个参数外剩余的参数
  let args = Array.from(arguments).slice(1)
  
  //this === 原函数
  //将原函数作为cat1的方法调用
  context[uniqueName] = this
  //使用扩展运算符传参,可以解决参数不确定的问题
  let result = context[uniqueName](...args)

  //用完删除
  delete context[uniqueName]
  return result
}
  • 将剩余参数传递给context[uniqueName]还可以通过eval来拼接调用语句
Function.prototype.call2 = function (context) {
  function mySymbol(obj) {
    let unique = (Math.random() + new Date())
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj) //如果还是冲突,递归调用
    } else {
      return unique
    }
  }
  let uniqueName = mySymbol(context)
  //this === 原函数
  //console.log(this);
  //获取除了第一个参数外剩余的参数
  let args = [];
  for(let i = 1; i < arguments.length; i++) {
    args.push('arguments[' + i + ']');
  }

  //将原函数作为cat1的方法调用
  context[uniqueName] = this

  //使用扩展运算符传参,可以解决参数不确定的问题
  let result = eval('context[uniqueName](' + args.join(',') + ')');

  //用完删除
  delete context[uniqueName]
  return result
}

apply()方法

  • 参数:数组

函数名称.apply(obj,[arg1,arg2...,argN])

let lisi = { name: 'lisi' }
let zs = { name: 'zhangsan' }
function f(age, sex) {
  console.log(this.name + age + sex)
}
//将f函数中的this指向固定到对象zs上
f.apply(zs, [23, 'nan'])

实现apply

Function.prototype.apply = function (context, arr) {
  var context = Object(context) || window
  context.fn = this

  var result
  if (!arr) {
    result = context.fn()
  } else {
    result = context.fn(...arr)
  }

  delete context.fn
  return result
}

简化版

Function.prototype.apply2 = function(context, args) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  let fnSymbol = Symbol()
  context[fnSymbol] = this
  let fn = context[fnSymbol](...args)
  delete context[fnSymbol] 
  return fn
}
```

```javascript
function setDetails(name, color) {
  this.name = name
  this.color = color
}
let cat1 = {}
let cat2 = {}
setDetails.apply(cat1, ['大毛', '橘色'])
setDetails.apply(cat2, ['二毛', '黑色'])
console.log(cat1.name) //大毛
console.log(cat2.name) //二毛

过程

  • 兼容apply第二个参数没有传入的情况
Function.prototype.apply2 = function (context,args) {
  function mySymbol(obj) {
    let unique = (Math.random() + new Date())
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj) //如果还是冲突,递归调用
    } else {
      return unique
    }
  }
  let uniqueName = mySymbol(context)
  //this === 原函数
  //console.log(this)

  args = args || [] //兼容没有传参的情况
  //将原函数作为context的方法调用
  context[uniqueName] = this

  //使用扩展运算符传参,可以解决参数不确定的问题
  context[uniqueName](...args)

  //用完删除
  delete context[uniqueName]
}
  • 使用eval将传入的字符串当做 JavaScript 代码进行执行
Function.prototype.apply2 = function (context, args) {
  function mySymbol(obj) {
    let unique = Math.random() + new Date()
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj)
    } else {
      return unique
    }
  }
  let uniqueName = mySymbol(context)
  //let args = Array.from(arguments).slice(1)
  let arr = []
  for (let i = 0; i < args.length; i++) {
    arr.push('args[' + i + ']')
  }

  context[uniqueName] = this
  /* let result = context[uniqueName](args.join(',')) 
  //等同于context[uniqueName]('大毛','橘色')
  */
  let result = eval('context[uniqueName](' + arr.join(',') + ')')

  //删除临时方法
  delete context[uniqueName]
  return result
}

bind

  • 只做this的绑定

  • 不会立即调用函数

  • 返回一个运行的逻辑与原函数一致的新函数

    • this会指向之前绑定的对象
function setDetails(name,color){
  this.name = name
  this.color = color
}
let cat1 = {}
let setDetails2 = setDetails.bind(cat1)
setDetails2('大毛','橘色')

实现bind

  • 处理边界条件
Function.prototype.myBind = function (target) {
  target = target || {} // 处理边界条件
  return function () {} // 返回一个函数
}
  • 给target添加一个方法,让方法中的this指向该target
Function.prototype.myBind = function (target) {
  target = target || {} // 处理边界条件
  const symbolKey = Symbol()
  target[symbolKey] = this
  return function () { // 返回一个函数
    target[symbolKey]()
    delete target[symbolKey]
  } 
}
  • 传入参数的
const mbs = {
  name: '山河',
  say(prefix, age) {
    console.log(`${prefix},my name is ${this.name},i am ${age} year old`)
  }
}

mbs.say('hello',13) // 'hello,my name is 山河,i am 13 year old'

const B = {
  name: '梳梳'
}

const sayB = mbs.say.bind(B,'hello')

sayB(8) // 'hello,my name is 梳梳,i am 8 year old''
  • bind中传递的参数,和调用bind的返回函数时传入的参数,都传递到say方法中
Function.prototype.myBind = function (target,...outArgs) {
  target = target || {} // 处理边界条件
  const symbolKey = Symbol()
  target[symbolKey] = this
  return function (...innerArgs) { // 返回一个函数
    const res = target[symbolKey](...outArgs, ...innerArgs) // outArgs和innerArgs都是一个数组,解构后传入函数
    // delete target[symbolKey] 这里千万不能销毁绑定的函数,否则第二次调用的时候,就会出现问题
    return res
  } 
}

偏函数应用程序:通过绑定现有函数的一些参数来创建一个新函数

  • 实现两个函数,分别是对传入的数进行翻倍和翻三倍
const double = n => n * 2
const double2 = double(2) // 4
const double4 = double(4) // 8
...
const triple = n => n * 3
const triple2 = triple(2) // 6
const triple4 = triple(4) // 12
...
  • 用偏函数的概念实现
const base = (n,m) => n * m

const double = base.bind(null,2)
const double2 = double(2) // 4
const double4 = double(4) // 8
...

const triple = base.bind(null,3)
const triple2 = triple(2) // 6
const triple4 = triple(4) // 12
...

  • bind返回一个函数,在函数体内调用apply
Function.prototype.bind2 = function (context) {
  let originFn = this
  return function () {
    return originFn.apply(context)
  }
}
  • 调用bind后返回的函数是可以传参的

  • bind方法除了第一个参数,还可以额外传参,可以理解为预传参

function setDetails(name,color){
  this.name = name
  this.color = color
}
let cat1 = {}
let setDetails2 = setDetails.bind(cat1,'大毛')
setDetails2('橘色')
  • 将第一次传参先存起来,在调用时将第一次和第二次传参进行拼接
Function.prototype.bind2 = function (context) {
  let originFn = this;
  let args = Array.from(arguments).slice(1);
  return function () {
    let bindArgs = Array.from(arguments);
    return originFn.apply(context, args.concat(bindArgs));
  }
}
  • bind返回的函数,如果之后是作为构造函数调用,则原函数中的this会指向创建的对象,而不会指向之前绑定的对象,并且生成的实例仍然可以使用原型对象上的属性
function Cat(name, color) {
  this.name = name
  this.color = color
}
Cat.prototype.miao = function(){console.log('喵~!')}
let cat1 = {}
let CatBind = Cat.bind(cat1)
let cat2 = new CatBind('大毛','橘色')
cat2.miao()
  • 返回的函数作为构造函数,通过new xxx()调用时,返回的函数的this指向以返回函数作为构造生成的实例。因此只需要判断this instanceof 返回的函数,另外,返回的函数要继承原函数(将原型链连接起来):
Function.prototype.bind2 = function (context) {
  let originFn = this;
  let args = Array.from(arguments).slice(1);
  function fBind() {
    let bindArgs = Array.from(arguments);
    return originFn.apply(this instanceof fBind ? this /*作为构造函数调用*/: context, args.concat(bindArgs));
  }
  fBind.prototype = Object.create(this.prototype) //继承
  return fBind
}