原生js:call、apply、bind

141 阅读2分钟

一、call

1、call是什么

call是Function.prototype上的一个方法,通过函数去调用,如:fn.call(thisArg, arg1, arg2, ...)

2、call的参数

  1. thisArg:可选,fn运行时使用的this值
    • 在非严格模式下,不传值,传null或undefined,会自动替换为window
    • 在严格模式下,不传值和传undefined,fn中this指向undefined;传null时fn中this指向null
    • 原始值会被包装类包装
  2. arg1, arg2, ...:从第二个参数开始都是传递给fn的实参

3、call的返回值

返回fn函数中的返回值,如果fn函数没有返回值,则默认返回undefined

4、call的使用场景

1、用来改变调用call的函数中this的指向

var myName = '全局name:小明'
function animal() {
  console.log(this.myName)
}

var dog = {
  myName: '旺财'
}

animal.call() // 全局name:小明,call()中没有传参时,相当于传递了window
animal.call(dog) // 旺财
animal.call(1) // undefined,传递原始值会被包装类包装,所以不会报错,但是在new Number()上又找不到myName,所以结果为undefined
Number.prototype.myName = 'number name'
animal.call(1) // number name

2、子类借用父类的属性或方法 juejin.cn/post/714088…

function Animal(name, food) {
  this.eat = function () {
    console.log(`${name}${food}`)
  }
}

function Dog(name, food) {
  Animal.call(this, name, food)
}

const dog = new Dog('旺财', '骨头')
dog.eat()
console.log(dog instanceof Animal) // false

注意,这是借用,不是继承,只有拥有了父类的原型才是继承

Dog.prototype = Animal.prototype // 这种的才是继承,或者Dog.prototype = new Animal()
const dog1 = new Dog()
console.log(dog1 instanceof Animal) // true

5、手动实现call

animal.call(dog) // 将animal方法添加到dog中,执行完再删除dog中的这个方法

传递到call中的对象(dog),在这个对象身上添加一个方法(fn),这个方法(fn)指向调用call的函数(animal),执行这个方法(fn),call第二个及后面的参数传到fn方法中,执行完删除fn。

在这个过程中,由于fn是对象dog的方法,fn中的this指向的是obj

ES5:

Function.prototype.myCall = function (obj) {
  if (typeof obj === 'number') obj = new Number(obj) // 原始值会包装
  obj = obj || window // null和undefined会自动替换为window
  var arr = []
  for (var i = 1, len = arguments.length; i < len; i++) arr.push('arguments[' + i + ']') // 从第二个参数开始都是传到函数中的实参
  obj.fn = this // 往传入的参数上添加一个方法,方法名任意,值为animal
  var res = eval('obj.fn(' + arr + ')') // 调用fn,也就是调用animal,并接收返回值
  delete obj.fn
  return res
}

ES6:

Function.prototype.myCall = function (obj, ...args) {
  if (typeof obj === 'number') obj = new Number(obj)
  obj.fn = this
  const res = obj.fn(...args)
  delete obj.fn
  return res
}

以上实现myCall方法时,手动将obj对象中的fn函数删除,万一obj中也有一个函数叫fn,便会影响到原obj,这是不可取的,当然你可以随意起一个几乎不会在obj中出现的属性名,但是ES6提供了一个symbol基本数据类型,它一定是obj中属性的唯一标识符:

      Function.prototype.myCall = function (obj, ...args) {
        if (typeof obj === 'number') obj = new Number(obj)
        const symbol = Symbol()
        obj[symbol] = this
        const res = obj[symbol](...args)
        delete obj[symbol]
        return res
      }

二、apply

1、概念:apply和call的唯一区别是传参,apply将call的第2个及以后的参数整合成一个数组

2、apply的手动实现

Function.prototype.myApply = function (obj = window, args) {
  obj.fn = this
  const res = args ? obj.fn(...args) : obj.fn()
  delete obj.fn
  return res
}

3、apply的使用场景

  1. 实现类似concat的功能
const fruits = ['apple', 'banana']
const nums = [1, 2, 3]

const newArr = fruits.concat(nums)
const newArr1 = fruits.push.apply(fruits, nums) && fruits // 将nums追加到fruits中

console.log(newArr) // ['apple', 'banana', 1, 2, 3]
console.log(newArr1) // ['apple', 'banana', 1, 2, 3]

要注意:concat并不会改变fruits,而apply改变了fruits

  1. 有一些需要循环去遍历数组的操作,可以使用apply去完成,避免循环
const nums = [1, 5, 2, 3]

const max = Math.max.apply(null, nums) // 不使用扩展运算符的话,一般用apply

console.log(max)
  1. 通过apply获取实参
      function test(...rest) {
        var args = []
        ;[].push.apply(args, arguments)

        var args1 = Array.prototype.slice.apply(arguments) // 这里使用call是一样的

        console.log(args)
        console.log(args1)
        console.log(rest) // 剩余参数不能完全替代arguments
      }

      test(1, 2, 3)

三、bind

1、bind和call的区别是,bind返回一个偏函数,使用柯里化传参

var username = '小明'
function animal(a, b, c) {
  console.log(this.username, a, b, c)
}

var dog = {
  username: '旺财'
}

animal.bind(dog, 1, 2)(50) // 旺财 1 2 50

2、bind的手动实现

ES5:

Function.prototype.myBind = function (obj) {
  var _this = this
  var outerArgs = Array.prototype.slice.call(arguments, 1)
  return function () {
    var innerArgs = Array.prototype.slice.call(arguments)
    return _this.apply(obj, outerArgs.concat(innerArgs))
  }
}

ES6:

Function.prototype.myBind = function (obj, ...outerArgs) {
  return (...innerArgs) => this.apply(obj, [...outerArgs, ...innerArgs])
}

bind返回的函数,当做构造函数使用时,new操作符会将this指向实例,此时的this指向构造函数中新创建的对象,并且实例对象的原型会继承调动bind函数的原型。

那么实现起来就需要考虑到new的情况:

ES5:

Function.prototype.myBind = function (obj) {
  var _this = this
  var outerArgs = Array.prototype.slice.call(arguments, 1)
  var Buffer = function () {}
  var Callback = function () {
    var innerArgs = Array.prototype.slice.call(arguments)
    var args = outerArgs.concat(innerArgs)
    // 闭包中的this指向window,但如果使用new操作符,this指向Callback函数
    _this.apply(this instanceof Buffer ? this : obj, args)
  }
  Buffer.prototype = this.prototype
  Callback.prototype = new Buffer() // Buffer仅作为继承的中间件
  return Callback
}
function person(a, b) {
  console.log(this.name, a, b)
}

var obj = {
  name: '小明'
}

var callback = person.myBind(obj, 1)
callback(2) // 小明 1 2

var c = new callback(20) // undefined 1 20
console.log(c) // 继承person

3、bind的使用场景:bind更适合用于事件的回调

    <button id="我是button的id值">按钮</button>
    <div id="我是div的id值">div</div>
    <script>
      const btn = document.querySelector('button'),
        div = document.querySelector('div')
      btn.addEventListener('click', handleBtnClick.bind(div))
      function handleBtnClick() {
        console.log(this.id)
      }
    </script>

如果要使用call,就得手动返回handleBtnClick函数

      btn.addEventListener('click', () => handleBtnClick.call(div))