js 中 this,apply,call,bind 详解

JavaScript 中关于 this、apply、call、bind 的介绍文章已经有很多了,看的越多,越会一头雾水。下面从常见的应用场景来说明上述这些概念可能会容易理解一些。

this

this 代表函数运行时的环境。注意是函数,是函数,还有就是运行时!

function hello(){
    console.log(this) // window
}
hello()

当 hello 函数执行时,在函数内部有个变量 this,代表这个函数运行环境,上述函数在全局环境下且在浏览器环境下运行,那么这个 this 就指向 window,这个比较好理解。但是许多现在许多 js 框架的设计都是使用构造函数方式设计的,例如下面构造函数:

function Dog(name,color){
    this.name = name;
    this.color = color;
    console.log(this)
}
var a = new Dog("阿黄","黄色")

构造函数首先函数名称首字母都会大写,其次都会使用一个 new 来构造一个实例,new 的时候 构造函数就会运行一次,这个时候构造函数里的this就指向这个实例对象

上述构造函数只是实现了一些属性,其实更多的时候构造函数内部应该根据传入的值来实现一些方法,从而体现良好的封装性。例如使得上述狗类增加一个方法:

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黄","黄色")
ahuang.say() // I am a 黄色 dog ,my name is 阿黄

首先先实例出来一个对象 ahuang ,再调用 ahuang 的 say 方法,say 方法内部可以获取到 color 和 name.

现在稍作改动:

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黄","黄色")
let a = ahuang.say
a() // I am a undefined dog ,my name is 

a 变量只是获取了ahuang的 say 方法定义,还没有执行,此时的 a 定于在全局,如果直接这样执行,那么say方法的this自然也就指向了全局的 window 了,由于全局上没有color和name,所以就成 undefined了。

其实我的理解是:函数一个工具和机器,只负责执行,可以理解为函数独立于对象,犹如一个榨汁机,如果放入橙子,运行时自然就会榨出橙汁,如果放入苹果,自然就会榨出苹果汁。这里苹果和橙子就是运行环境。

对于给定什么环境运行函数,就可能产生不同结果,这样虽然比较自由,但过分自由也会导致一些问题产生,例如上述 Dog 的 say 方法:

 this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
 }

显然这个方法只有运行于狗这个对象才有意义,放在全局虽然也能运行,但却失去了这个方法的意义。
所以需要一种机制能够始终使得这个方法能够在狗对象中运行。

方案一:采用箭头函数来定义
箭头函数绑定了运行环境,即定义在哪个对象内,则这个箭头函数内部 this 始终为这个对象。

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = ()=> {
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黄","黄色")
let a = ahuang.say
a() // I am a 黄色 dog ,my name is 阿黄

上述函数,即使a在全局定义,但是 say 是箭头函数定义的,里面 this 依然指向 ahuang.

方案二 使用 call 或者 apply 来实现。

call 和 apply

call和 apply 方法功能是一样的,只是传入的参数形式不一样,作用都是绑定(劫持)一个特定的执行环境:

func.call(this, arg1, arg2,...);
func.apply(this, [arg1, arg2])

call 第一个参数为执行环境,其余参数为 func 的参数,可以有无数个参数,而apply只有两个参数,第一个为执行环境,第二个为其余数组,通过一个数组来传递。

例如上述狗的构造函数也可以使用call来实现箭头函数效果:

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黄","黄色")
let b = ahuang.say
b.call(ah

使用call 和 apply 还有一些常用的应用。

1.求数组最大值

let arr = [1,2,3]
// 方法一:
Math.max(...arr)
// 方法二
Math.max.apply(Math,arr) // 巧妙地利用了第二个参数为数组特征

2.判断数组

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]' ;
}

bind

bind 和 call,apply 也有相似之处,bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黄","黄色")
let b = ahuang.say.bind(ahuang) // 使用bind后b返回的是一个函数,这个函数内的this永远是ahuang了
b() // I am a 黄色 dog ,my name is 阿黄

可见 bind 绑定执行环境是静态的,关键是在定义时就可以绑定,而不是像 call 那样需要调用时去绑定.有点类似于箭头函数。

bind 一般有几个经典的应用场景:

[1,2,3].forEach(function(){
    console.log(this) // window
})

匿名的回调函数里面 this 一般为指向 window 假如我们要在这个函数内用到一些其他对象的值,则可以通过bind来改变回调函数的值,以Vue框架为例:

let vm = new Vue({
  data(){
    return {
      height:768
    }
  },
  mounted(){
    window.addEventListener("resize",(function(){
      this.height = document.body.clientHeight
    }).bind(this))
  }
})

如果不绑定this,则回调函数内this为 window ,显然读取不到this.height,当然还可以使用箭头函数或者声明一个中间变量来解决:

 mounted(){
    window.addEventListener("resize",()=>{
      this.height = document.body.clientHeight
    })
    // 或者
    let That =this
     window.addEventListener("resize", function(){
      That.height = document.body.clientHeight
    })
  }

所以在 Vue 框架内建议多用箭头函数来定义,forEach,map等方法也是如此!

实现原理

很多面试时候可能会问到 call 和 bind 实现原理,并手写一个。其实这并不难

call

以上述榨汁机的解释,例如有个榨汁机现在在榨橙汁,现在我们想让榨汁机在苹果的环境中运行。

榨汁机.call("苹果")

调用 call 时会传入苹果对象,我们需要在苹果对象上增加榨汁机的方法,再执行一次,执行完毕后再把原属于榨汁机的方法给删掉即可!虽然有点牵强,但实际就是这么干的。

Function.prototype.myCall = function(){
  let args = [...arguments]
  let ctx = args.length>0 ? args.shift() : window
  let s = Symbol() // 生成一个唯一值
  // 在被劫持者对象属性中加入这个属性
  ctx[s] = this
  let result = ctx[s](...args)
  delete ctx[s]
  return result
}

bind

bind 返回的是一个函数,或者说闭包

Function.prototype.myBind = function(){
  let args = [...arguments]
  let ctx = args.length>0 ? args.shift() : window
  let s = Symbol() // 生成一个唯一值
  // 在被劫持者对象属性中加入这个属性
  ctx[s] = this
  return function(){
    let res = ctx[s](...args) // 定义了在特定的上下文运行的结果,当执行时就能得到这个特定上下文的结果。
    delete ctx[s]
    return res
  }
}

完!

https://juejin.im/post/5e339b66f265da3df716e6ab

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论