JavaScript深入浅出this、call、apply、bind

285 阅读6分钟

前言

在方法原型链中有3个能改变方法调用中的this,分别是apply、call、bind,三个都是Function原型链中的方法,可以在我们定义的方法中调用,下面就开始详细的介绍

目录

  1. 方法内的this是什么
  2. call、apply方法的原理
  3. bind方法的原理
  4. 总结

方法内的this是什么

我们在调用方法是通常在方法体内使用到this这个关键字,主要分以下5种情况

  1. 自然调用this都指向window(浏览器环境下)
// 声明方法后,自然调用
function a() {
  console.log(this);
}
a() // window

// 无论是在方法体嵌套,只要是自然调用都属于window(浏览器环境下)
function b() {
  a() // window
}
b()
  1. 在引用类型中的调用函数中,this属于调用该方法的调用者(通俗来说就是"."之前的那个)
// 在对象体内定义方法
const a = {
  b: function () {
    console.log(this);
  }
}

a.b()  // a

// 在嵌套对象体内定义方法
const c = {
  d: {
    e: function () {
      console.log(this);
    }
  }
}

const f = c.d
c.d.e()  // d
  1. new构造方法其内部的this是该构造方法
// 定义一个构造方法
function Person() {
  console.log(this);
}

const a = new Person() // Person

Ps: new构造方法的内部实现

function custom_new(constructor, ...args) {
  // 保存构造方法的prototype
  const originPrototype = constructor.prototype

  // 利用构造方法的prototype创建一个对象
  const obj = Object.create(originPrototype)

  // 挂载构造方法、并调用
  const special = Symbol('special')
  obj.__proto__[special] = constructor
  const res = obj[special](...args)
  delete obj.__proto__[special]

  // 构造方法调用后的返回值是一个引用类型,则会使用返回值
  return res && res instanceof Object  ? res : obj
}
  1. 方法原型链上提供了一个bind方法用于改变方法的this指向
  • 经过bind返回的方法,其this通常就是bind方法的第一个参数(bind的详细介绍在下方)
  • 但是如果bind方法在用new调用构造方法那就是bind之前的方法本身
function a() {
  console.log(this);
}

const b = { a: 'hello' }

const c = a.bind(b)

c() // b
const d = new c() // a
  1. 箭头函数,其this是声明函数时所在的作用域this(无法被bind改变this指向,只取决于声明时的作用域)
// 声明时的作用域
const a = () => { console.log(this) }
const obj = { a }

obj.a() // window

const b = { o: 'hello' }
const c = a.bind(b)
c() // window

call、apply方法的原理

  • call和apply都是Function原型链上的方法
  • 用于改变方法调用内部的this指向
  • 返回调用方法后返回的结果
function a(){
    console.log(this)
    return this.a + ' world'
}

const b = { a: 'hello'}

const c = a.call(b)  // 方法a里的console.log 打印出 {a: 'hello'}
const d = a.apply(b) // 方法a里的console.log 打印出 {a: 'hello'}

console.log(c)  // hello world
console.log(d)  // hello world

不同点: 参数传递不一样

  • apply传递参数是把参数以数组的形式传递到方法的第二个参数
  • call传递参数则把参数接到方法的第二个参数往后
function a(paramA, paramB, paramC){
    console.log(this, paramA, paramB, paramC)
}
const b = { a: 'hello'}
const paramA = 'A'
const paramB = 'B'
const paramC = 'C'

a.call(b, paramA, paramB, paramC)
a.apply(b, [paramA, paramB, paramC])

call、apply的原理(手写call以及apply方法)

  • 类型处理,call、apply方法下都会将基本类型进行转换成基本类型的引用实例(如,3转化成 Number {3})
  • 方法挂载到隐式原型下
  • 方法调用
  • 删除隐式原型下的方法
Function.prototype.call = function (initThis, ...args){ // apply和call的不同点在这,参数传递不通
    if (typeof this !== 'function'){
        throw new Error('this must be a function')
    }
    let handleThis;
    //类型处理
    switch (typeof initThis){
        case 'string':
            handleThis = new String(initThis)
            break
        case 'number':
            handleThis = new Number(initThis)
            break
        case 'boolean':
            handleThis = new Boolean(initThis)
            break
        default:
            // 浏览器环境下才有window
            handleThis = initThis || window  // null undefined (转化成window) Symbol Object Array function
            break
    }
    //  方法挂载、方法调用、方法销毁
    const special = Symbol('special')
    initThis.__proto__[special] = this  // this指向的是当前的方法 如:a.call() 引用类型调用'.'的前一个为this
    const res = initThis[special](...args)
    delete initThis.__proto__[special]

    return res
}

apply唯一不同点就是参数传递

Function.prototype.apply = function (initThis, args){ // apply和call的不同点在这,参数传递不通
    if (typeof this !== 'function'){
        throw new Error('this must be a function')
    }

    let handleThis;
    //类型处理
    switch (typeof initThis){
        case 'string':
            handleThis = new String(initThis)
            break
        case 'number':
            handleThis = new Number(initThis)
            break
        case 'boolean':
            handleThis = new Boolean(initThis)
            break
        default:
            handleThis = initThis || window  // null undefined (转化成window) Symbol Object Array function
            break
    }
    //  方法挂载、方法调用、方法销毁
    const special = Symbol('special')
    initThis.__proto__[special] = this  // this指向的是当前的方法 如:a.call() 引用类型调用'.'的前一个为this
    const res = initThis[special](...args)
    delete initThis.__proto__[special]

    return res
}

bind方法的原理

  • bind也是Function原型链上的方法
  • 用于改变方法调用内部的this指向
  • 返回一个新的方法(不调用原方法)
function a(){
    console.log(this)
}
const b = { a: 'hello'}
const c = a.bind(b)
c()  // console.log { a: 'hello'}

bind的原理(手写bind方法)

  • 类型处理,原生bind方法下都会将基本类型进行转换成原基本类型的引用实例(如,3转化成 Number {3})
  • 定义返回方法(判断处理,如果是new关键字调用构造方法的,则要使用当前的this)
    • new构造函数调用:使用当前的this进行方法挂载、方法调用、方法删除
    • 普通函数调用:使用处理好的引用类型进行方法挂载、方法调用、方法删除
  • 将返回的方法的prototype关联this.prototype(为了new构造方法时调用的是原构造方法)
  • 返回方法
Function.prototype.bind = function (initThis, ...args){
     if (typeof this !== 'function'){
            throw new Error('this must be a function')
       }
    let handleThis;
    //类型处理
    switch (typeof initThis){
        case 'string':
            handleThis = new String(initThis)
            break
        case 'number':
            handleThis = new Number(initThis)
            break
        case 'boolean':
            handleThis = new Boolean(initThis)
            break
        default:
            handleThis = initThis || window  // null undefined (转化成window) Symbol Object Array function
            break
    }

    const special = Symbol('special')
    const _this = this

    // 返回值
    const returnFn = function (...returnArgs){
        // 判断是否是new关键字
        // new关键字是其构造函数本身,_this指向的就是原方法
        // 普通调用则使用处理好的引用类型
        const callThis = this instanceof _this ? this : handleThis

        //方法挂载、调用、删除
        callThis.__proto__[special] = this
        callThis[special](...[...args, ...returnArgs])
        delete callThis[special](...[...args, ...returnArgs])
    }
    
    // 关联当前的prototype
    if(this.prototype){
        returnFn.prototype = this.prototype
    }
    
    return returnFn
}

总结

this判断

  • 先判断是否是自然调用,自然调用即window(浏览器下)
  • "引用类型.方法名()"调用则就是该引用类型,如果引用类型依旧是方法则需要通过"."的链向上查找判断
  • 经过bind改变的this的方法this指向改变值,但不会影响原型链,所以new出来还是原函数
  • new构造函数this是构造函数本身,但是new箭头函数则会报错

apply、call实现步骤

  • 类型处理,处理成引用类型
  • 在引用类型上,方法挂载、方法调用、方法删除
  • 返回方法调用的结果

bind实现步骤

  • 类型处理,处理成引用类型
  • 定义返回方法(new构造函数调用、普通函数调用)
    • new:使用当前的this进行方法挂载、方法调用、方法删除
    • 普通:使用处理好的引用类型进行方法挂载、方法调用、方法删除
  • 关联原型
  • 返回方法