「前端每日一问(38)」列举一些call、apply、bind 的使用场景

954 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

本题难度:⭐ ⭐

答:

image.png

注意:上图列举的是一般情况,如果使用 call 或者 apply 的场景非要用 bind 去实现,返回一个函数再执行,也是可以实现的,不过没必要。

call、apply 的应用场景

call 和 apply 的作用是相同的,只是调用时传参形式不同而已。

精准判断一个数据类型

精准地判断一个数据的类型,可以用到 Object.prototype.toString.call(xxx)。

调用该方法,统一返回格式 [object Xxx]的字符串,用来表示该对象。

// 直接调用
Object.prototype.toString({})      // '[object Object]'

// 加上 call
// 引用类型
Object.prototype.toString.call({})           // '[object Object]'
Object.prototype.toString.call(function(){}) // "[object Function]'
Object.prototype.toString.call(/123/g)       // '[object RegExp]'
Object.prototype.toString.call(new Date())   // '[object Date]'
Object.prototype.toString.call(new Error())  // '[object Error]'
Object.prototype.toString.call([])           // '[object Array]'
Object.prototype.toString.call(new Map())    // '[object Map]'
Object.prototype.toString.call(new Set())    // '[object Set]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(document)     // '[object HTMLDocument]'
Object.prototype.toString.call(window)       // '[object Window]'
Object.prototype.toString.call(this)         // '[object Window]'

// 原始类型
Object.prototype.toString.call(1)           // '[object Number]'
Object.prototype.toString.call('1')         // '[object String]'
Object.prototype.toString.call(true)        // '[object Boolean]'
Object.prototype.toString.call(1n)          // '[object BigInt]'
Object.prototype.toString.call(null)        // '[object Null]'
Object.prototype.toString.call(undefined)   // '[object Undefined]'
Object.prototype.toString.call(Symbol('a')) // '[object Symbol]'

为什么要调用 call

call 是函数的方法,用来改变 this 指向,用 apply 也可以。

如果不改变this指向为我们的目标变量,this将永远指向调用的Object.prototype

Object.prototype.toString([])       // '[object Object]'  this 指向 Object.prototype,判断类型为 Object。
Object.prototype.toString.call([])  // '[object Array]' this 指向 [],判断类型为 Array
// 重写 Object.prototype.toString 方法,把 this 打印出来看一下,进一步加深理解
Object.prototype.toString = function () {
  console.log(this)
}
// 引用类型
Object.prototype.toString([])       // Object.prototype
Object.prototype.toString.call([])  // []

// 原始类型
Object.prototype.toString(1)       // Object.prototype
Object.prototype.toString.call(1)  // Number {1}
// 这里的 Number {1},是一个包装类,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质

image.png

伪数组转数组

伪数组转数组,在 es6 之前,可以使用 Array.prototype.slice.call(xxx)

function add () {
  const args = Array.prototype.slice.call(arguments)
  // 也可以这么写 const args = [].slice.call(arguments) 
  args.push(1) // 可以使用数组上的方法了
}

add(1, 2, 3)

为什么要调用 call

和上文的判断数据类型的原理是一样的,如果不改变this指向为目标伪数组,this将永远指向调用的Array.prototype,就不会生效。

slice 方法原理

循环伪数组,把伪数组的元素挨个放到定义的一个新数组里,再返回新数组,差不多是类似这样的代码:

// 从 slice 方法原理理解为什么要调用 call
Array.prototype.slice = function (start, end) {
  const res = []
  start = start || 0
  end = end || this.length
  for (let i = start; i < end; i++) {
    res.push(this[i]) // 这里的 this 就是伪数组,所以要调用 call
  }
  return res
}

ES5 实现继承

在一个子构造函数中,你可以通过调用父构造函数的 call 方法或者 apply 方法来实现继承。

function Person (name) {
  this.name = name
}

function Student (name, grade) {
  Person.call(this, name)
  this.grade = grade
}

const p1 = new Person('lin')
const s1 = new Student('lin', 100)

上面的代码示例中,构造函数 Student 中会拥有构造函数 Person 中的 name 属性,grade 属性是 Student 自己的。

回调函数 this 丢失问题

执行下面的代码,回调函数会导致 this 丢失。

const obj = {
  lastName: 'lin',
  sayName() {
    console.log(this.lastName)
  }
}

obj.sayName() // 输出 'lin'

function fn(callback) {
  if (typeof callback === 'function') {
    callback()
  }
}

fn(obj.sayName) // 输出 undefined

可以使用 call 或者 apply 改变 this 指向:

const obj = {
  lastName: 'lin',
  sayName() {
    console.log(this.lastName)
  }
}

obj.sayName() // 输出 'lin'

function fn(callback, context) { // 定义一个 context 参数,可以把上下文传进去
  if (typeof callback === 'function') {
    callback.apply(context) // 显式改变 this 值,指向传入的 context
  }
}

fn(obj.sayName, obj) // 输出 'lin'

Vue 用 this.$options.data() 重置组件 data

this.$options.data() 重置组件 data 时,data() 里用 this 获取的 props 或 method 都为undefined,代码简化如下:

export default {
    props: {
        P: Object
    },
    data () {
        return {
            A: {
                a: this.methodA
            },
            B: this.P
        };
    },
    methods: {
        resetData () { // 重置 data 时调用
            Object.assign(this.$data, this.$options.data()) // 这么写就会出 bug
        },
        methodA () {
            // do sth.
        },
        methodB () { 
            this.A.a && this.A.a(); // this.A.a is undefined, this.B is undefined!!!
        }
    }
}

调用resetData()之后,再调用methodB()时,this.A.a和this.B是undefined。

解决,resetData 里这么写:

resetData () { 
    Object.assign(this.$data, this.$options.data.call(this))
}

调用 this.$options.data.call(this),就可以把 props 和 methods 挂载上去。

我为啥连这么偏的知识都知道,因为我踩过坑,hhh。

至于为啥要这样重置一个组件的 data,因为实现起来简单,具体可以参考这篇文章:

Vue中重置data的数据为初始状态

获取数组最大值、最小值

要获取一堆数据中的最大值或者最小值,用 Math.max 或 Math.min 要一个一个地传递参数

要获取数组中的最大值,你可能会这么写:

const arr = [1,2,3,4,5]

function getArrMax (arr) {
  let max = arr[0] 
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] > max) {
      max = arr[i]
    }
  }
  return max
}
console.log(getArrMax(arr)) // 5

实际上,用 apply 就可以很巧妙地把参数一个一个地传进 Math.max 中去:

const arr = [1,2,3,4,5]
console.log(Math.max.apply(Math, arr)) // 5

当然,es6 之后也可以用解构赋值轻松传值:

const arr = [1,2,3,4,5]
console.log(Math.max(...arr)) // 5

将数组各项添加到另一个数组

const arr1 = [1, 2, 3]
const arr2 = ['a', 'b', 'c']

这样两个数组,如何把 arr2 的数组项添加到 arr1。

用数组解构可以轻松解决

const arr1 = [1, 2, 3]
const arr2 = ['a', 'b', 'c']

arr1.push(...arr2)
console.log(arr1) // [1,2,3,'a','b','c']

但是 es6 之前可没有解构赋值,怎么办?

用 concat 不行,因为 concat 不能改变原数组,难道又要使用循环了吗?

其实和获取数组最大值、最小值是一样的,也可以使用 apply 来解决。

const arr1 = [1, 2, 3]
const arr2 = ['a', 'b', 'c']

arr1.push.apply(arr1, arr2)

console.log(arr1)

利用的也是 apply 可以把多个参数转化为一个参数数组的原理。

bind 应用场景

解决循环陷阱问题

一个非常经典的循环陷阱问题,下面这段代码打印出来的值都是 5。

const arr = []
for (var i = 0; i < 5; i++) {
  arr.push(function () {
    console.log(i)
  })
}

arr[0]() // 5
arr[1]() // 5
arr[2]() // 5

这个问题除了用闭包来解决,也可以用 bind 函数来解决。

bind 函数可以显式改变 this 指向,会返回一个新的函数,我们利用 bind 函数会返回一个新函数的特性,来解决循环陷阱的问题。

const arr = []
for (var i = 0; i < 5; i++) {
  arr.push(function (i) {
    console.log(i)
  }.bind(this, i)) // 不用改变 this 指向,我们主要是为了返回一个新的函数
}

arr[0]() // 0
arr[1]() // 1
arr[2]() // 2

关于循环陷阱,更多可参考我的这篇文章:

闭包与循环陷阱

react 的 this 指向问题

import React from 'react'

class App extends React.Component{

  handleClick() {
    console.log(this); // 输出 undefined
  }

  render(){
    return (
      <button onClick={this.handleClick}>
        Click Me
      </button>
    );
  }
}

export default App

执行 handleClick 方法时,输出的 this 是 undefined,和预期的有点不一样啊。

如何解决 this 指向 undefined 问题

可以使用 bind 手动绑定或者使用箭头函数。

import React from 'react'
class App extends React.Component{

  handleClick() {
    console.log(this); // 输出 App
  }

  render(){
    return (
      // 使用 bind
      <button onClick={this.handleClick.bind(this)}> 
        Click Me
      </button>
    )
  }
}

export default App
import React from 'react'
class App extends React.Component{
  
  // 使用箭头函数
  handleClick = () => {
    console.log(this); // 输出 App
  }

  render(){
    return (
      // 使用 bind
      <button onClick={this.handleClick}> 
        Click Me
      </button>
    )
  }
}

export default App

结尾

阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(37)」call、apply 和 bind 的区别是什么?

下一篇:

「前端每日一问(39)」手写 call 函数