JS 中的 apply, call, bind 详解及手写

85 阅读5分钟

apply, call, bind 是什么

apply, call, bind 都是用于改变 this 指向的方法

  • fn.apply(thisArg, argsArray) 方法:接收两个参数,参数1 thisArg 就是 this 指向的对象,参数2 argsArray 是一个数组或者类数组对象,其中的数组元素将作为单独的参数传给调用函数fn,返回调用函数fn的执行结果,属于立即执行函数。
  • fn.call(thisArg, arg1, arg2, ...) 方法:接收一个或多个参数,参数1 thisArg 就是 this 指向的对象,其余参数传给调用函数 fn,返回调用函数的执行结果,属于立即执行函数。
  • fn.bind(thisArg, arg1, arg2, ...) 方法:接收一个或多个参数,和 call 方法一致,但是其返回一个函数,而不是返回调用函数的执行结果。

有什么作用

apply 作用
  1. 改变 this 指向
var name = '我是 window 对象'
function getName (arg1, arg2) {
    console.log(`${arg1} ${arg2}, ${this.name}`)
}
let obj = {
    name: '我是 obj'
}
getName('你好', '朋友') // 你好 朋友, 我是 window 对象
getName.apply(obj, ['你好', '朋友']) // 你好 朋友, 我是 obj
  1. 求数组的最大值和最小值
const numbers = [7, 8, 4, 9, 2]
const max = Math.max.apply(null, numbers)
console.log(max) // 9

const min = Math.min.apply(null, numbers)
console.log(min) // 2
  1. 将数组各项添加到另一个数组
const array = ['a', 'b']
const elements = [0, 1, 2]
array.push.apply(array, elements)
console.log(array) //  ['a', 'b', 0, 1, 2]

有小伙伴会问,为啥要用 apply 呢?直接用push, concat 或 es6 扩展运算符都可以实现,举例:

const array = ['a', 'b']
const elements = [0, 1, 2]
array.push(elements)
console.log(array) // ['a', 'b', Array(3)] 如果直接用 push, 会把 elements 整个数组当成 array 的元素
const array = ['a', 'b']
const elements = [0, 1, 2]
const temp = array.concat(elements)
console.log(temp) // ['a', 'b', 0, 1, 2] concat 结果符合要求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组
const array = ['a', 'b']
const elements = [0, 1, 2]
const temp = [...array, ...elements]
console.log(temp) // ['a', 'b', 0, 1, 2] es6 扩张运算符, 结果符合要求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组
call 作用
  1. 改变 this 指向
var name = '我是 window 对象'
function getName (arg1, arg2) {
    console.log(`${arg1} ${arg2}, ${this.name}`)
}
let obj = {
    name: '我是 obj'
}
getName('你好', '朋友') // 你好 朋友, 我是 window 对象
getName.call(obj, '你好', '朋友') // 你好 朋友, 我是 obj
  1. 实现将一个具有 length 属性的对象转化为数组(Array.prototype.slice.call)
/**
* 这里的对象属性名除了 length 属性,其他的属性都只能是数字,否者结果为空
* 而且数字不能超过 length 属性值, 否者超过 length 值的属性名对应的值将不显示
/*
let obj1 = {
    0: '张三',
    1: 20,
    length: 2
}
console.log(Array.prototype.slice.call(obj1, 0)) // ['张三', 20] 

let obj2 = {
    0: '李四',
    1: 20
}
console.log(Array.prototype.slice.call(obj2, 0)) // []
  1. 返回数据结构的类型 (Object.prototype.toString.call),解决 typeof 只能准确判断基本类型的弊端
let string = '大不了从头再来'
let boolean = true
let number = 10
let nullType = null
let undefinedType = undefined 
let object = { name: '张三'}
console.log(typeof string, Object.prototype.toString.call(string)) // string [object String]
console.log(typeof boolean, Object.prototype.toString.call(boolean)) // boolean [object Boolean]
console.log(typeof number, Object.prototype.toString.call(number)) // number [object Number]
console.log(typeof nullType, Object.prototype.toString.call(nullType)) // object [object Null]
console.log(typeof undefinedType, Object.prototype.toString.call(undefinedType)) // undefined [object Undefined]
console.log(typeof object, Object.prototype.toString.call(object)) // object [object Object]
  1. 实现函数的继承
function Product (name, price) {
    this.name = name 
    this.price = price
}

function Food (name, price) {
    this.category = 'food'
    // 此 this 指 Food, 通过改变 this 指向, 使 Product 里的 this 指向 Food
    // 从而实现 Food 拥有 Product 里的 name 属性和 price 属性,也就实现了继承
    Product.call(this, name, price)
}

let food = new Food('cheese', 5)
console.log(food.name) // cheese
bind 作用
  1. 改变 this 指向
var name = '我是 window 对象'
function getName (arg1, arg2) {
    console.log(`${arg1} ${arg2}, ${this.name}`)
}
let obj = {
    name: '我是 obj'
}
getName('你好', '朋友') // 你好 朋友, 我是 window 对象
let getNewName = getName.bind(obj, '你好', '朋友')
getNewName() // 你好 朋友, 我是 obj
  1. 偏函数(使一个函数拥有预设的初始参数)
function list() {
  return Array.prototype.slice.call(arguments);
}

function addArguments(arg1, arg2) {
    return arg1 + arg2
}

var list1 = list(1, 2, 3); // [1, 2, 3]

var result1 = addArguments(1, 2); // 3

// 创建一个函数,它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37);

// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var list2 = leadingThirtysevenList();
// [37]

var list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]

var result2 = addThirtySeven(5);
// 37 + 5 = 42

var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42,第二个参数被忽略

手写 apply

// apply 方法是属于函数原型上的方法,因此 myApply 绑定到函数原型上
Function.prototype.myApply = function (thisObj, args) {
    // 此 this 就是调用 myApply 的函数,如果不是函数,抛出异常
    if (typeof this !== 'function') {
        throw new TypeError('not a function')
    }
    // thisObj 就是改变 this 指向的对象, 
    // 如果thisObj为 null 或 undefined, thisObj 指向 window 
    if (thisObj === null || thisObj === undefined) {
        thisObj = window
    } else {
        // thisObj如果是原始值,转换为原始对象的实例对象, 如果是对象,总是返回对象
        thisObj = Object(thisObj)
    }
    let fn = Symbol() // 避免和thisObj对象上原来的属性重名
    thisObj[fn] = this // 在thisObj对象上绑定fn,并把this对象帮定到fn属性上(this 就是调用myApply的函数)
    // 将第二个参数开始的所有参数以参数列表的方式传入,并保留执行的结果(相当于执行了调用myApply的函数)
    // 由于此时的fn是thisObj的属性,故fn函数中的this指向是thisObj,即调用myApply传入的第一个参数的对象
    // 从而达到改变this指向的功能(fn函数中this是指向thisObj而thisObj是自己传入的对象)
    let result = thisObj[fn](...args) 
    delete thisObj[fn] // 删除thisObj 的属性fn
    return result
}

手写 call

// call 方法是属于函数原型上的方法,因此 myCall 绑定到函数原型上
Function.prototype.myCall = function (thisObj, ...args) {
    // 此 this 就是调用 myCall 的函数,如果不是函数,抛出异常
    if (typeof this !== 'function') {
        throw new TypeError('not a function')
    }
    // thisObj 就是改变 this 指向的对象, 
    // 如果thisObj为 null 或 undefined, thisObj 指向 window 
    if (thisObj === null || thisObj === undefined) {
        thisObj = window
    } else {
        // thisObj如果是原始值,转换为原始对象的实例对象, 如果是对象,总是返回对象
        thisObj = Object(thisObj)
    }
    let fn = Symbol() // 避免和thisObj对象上原来的属性重名
    thisObj[fn] = this // 在thisObj对象上绑定fn,并把this对象帮定到fn属性上(this 就是调用 myCall 的函数)
    // 将第二个参数开始的所有参数以参数列表的方式传入,并保留执行的结果(相当于执行了调用myCall的函数)
    // 从而达到改变this指向的功能(fn函数中this是指向thisObj而thisObj是自己传入的对象)
    let result = thisObj[fn](...args)
    delete thisObj[fn] // 删除thisObj 的属性fn
    return result
}

手写 bind

// bind 方法是属于函数原型上的方法,因此 myBind 绑定到函数原型上
Function.prototype.myBind = function (thisObj, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('not a function')
    }
    // 将原函数进行保存(调用myBind的函数)
    const _this = this
    // 定义需要返回新函数(新函数和原函数一样,也可以接受参数)
    let funBind = function(...secondArgs) {
        // 判断返回的新函数funBind是不是作为构造函数使用
        // this 是否是funBind的实例,也就是返回funBind是否通过new调用
        const isNew = this instanceof funBind
        // new调用就绑定到this上,否则就绑定到传入的thisObj上
        const context = isNew ? this : Object(objThis)
        // 新函数的返回结果要与原函数一致,因此原函数调用call 传入this指向对象,并传入myBind的入参和新函数的入参数
        return _this.call(context, ...args, ...secondArgs)
    }
    // 判断原函数有无prototype
    if (_this.prototype) {
        // 将原函数的prototype 赋给funBind
        funBind.prototype = Object.create(_this.prototype)
    }
    // 返回新的函数
    return funBind
}