apply,call,bind从使用到简单实现

228 阅读5分钟

this的指向

this的指向会指向其调用者。

let obj = {
	foo: function () {
    	console.log(this)
    }
}
function foo () { console.log(this) }
foo()    //window对象,相当于window.foo()
obj.foo()    //obj对象

简单来讲,谁通过通过“.”调用函数,函数里的this就指向谁(es6箭头函数除外)。那么this就一成不变的指向其调用都了吗?

js中可以通过apply,call,bind来改变函数内部this的指向。

apply

apply用于改变函数对象里this的指向。第一个参数是需要改变this指向的对象,第二个参数是一个数组。

//foo函数里并没有myname属性,全局也没有myname属性
function foo () {
  console.log(this.myname)
  console.log(this)
}
foo()  //此时foo中的this指向window(非严格模式下),打印undefined和window对象
let obj = { myname: 'kricn' }
foo.apply(obj)  //打印出kricn和obj对象

foo调用apply后,其内部的this指向指向apply的第一个参数,这里即是obj

obj里有myname属性,故打印出kricn, this也是指向obj对象

apply接收的第二个参数需要是数组类型,最后这个数据类型的参数会以单个形式传递给foo

function foo (a, b) {
  console.log(this.myname)
  console.log(this)
  console.log(a)
  console.log(b)
  cosnole.log(arguments)
}
foo.apply({myname: 'kricn'}, [1,2,3])
/*
打印结果
kricn
{myname: 'kricn'}
1
2
Arguments(3)
*/

这里传入长度为三的数组,相当于向foo传入了三个参数1,2,3并将foo的this指向{myname:'kricn'}后执行函数

call

和apply的作用一样,不同的是call从第二个参数起接受单个参数

foo.call(obj, 1, 2, 3)

参数确定数量用call,不确定用apply,通过push入数组传递进去, 也可以通过循环arguments来拿到全部参数

bind

bind的作用与call和apply一样,bind的传参和call一样。

bind与call和apply的区别是其返回一个函数,而apply和call是立即调用。

let fn = foo.bind(obj)  //foo函数里的指针指向obj,但foo函数没有执行
fn()  //这样才算真正调用,foo执行

多次调用bind是无效的,即(foo.bind(obj1).bind(obj2).bind(obj3))(); foo的this只想obj1,后面两个bind无效, 原因是bind的实现方式,见下文。

apply, call和bind的实现

apply的实现

//参数context即所要指向的对象,就是对应上文obj
Function.prototype.myApply = function (context) {
  //判断是不是函数在调用这个方法
  if(typeof this !== 'function') throw new Error(`${this} is not a function`)
  //调用这个方法的函数可能有返回值
  let res = undefined;
  //this是个函数,因为函数调用了myApply,如foo.myApply()
  //上文提到,谁通过.调用函数,其函数内部的this就指向谁
  //将this赋给context的一个属性,通过context去调用this这个函数
  context.fn = this;
  //给context一个默认值,没有传参的话就指向window,在node环境下就是object
  context = context || window
  //获取除context之外的其他参数,可能有也可能没有
  let args = arguments[1] //arguments的第一个是context
  if(args) {
    //this本身就是一个函数,通过es6语法将数组展开传入this函数
    //其实就是context在调用函数,这样调用函数的this自然指向调用它的对象,即context
    res = context.fn(...args)
  }else{
    //若没有其他参数,直接执行this函数即可
    context.fn()
  }
  delete context.fn
  //返回res
  return res
} 

call的实现

//参数context即所要指向的对象,就是对应上文obj
Function.prototype.myApply = function (context) {
  //判断是不是函数在调用这个方法
  if(typeof this !== 'function') throw new Error(`${this} is not a function`)
  //调用这个方法的函数可能有返回值
  let res = undefined;
  //this是个函数,即调用myApply的函数,将其赋给context的一个属性,改变函数内部this指向
  context.fn = this;
  //给context一个默认值,没有传参的话就指向window,在node环境下就是object
  context = context || window
  //获取除context之外的其他参数,可能有也可能没有
  //call传的是多个参,而不是一个数组
  let args = [...arguments].slice(1) //arguments的第一个是context
  if(args) {
    //this本身就是一个函数,通过es6语法将数组展开传入this函数
    //其实就是context在调用函数,这样调用函数的this自然指向调用它的对象,即context
    res = context.fn(...args)
  }else{
    //若没有其他参数,直接执行this函数即可
    context.fn()
  }
  delete context.fn
  //返回res
  return res
}

bind的实现 bind会返回一个函数,因此除了直接赋值给一个变量再执行或直接执行外,还可以用new操作符去实例返回的函数,因此先了解下 new做了什么?

//new做了什么
function create(context, ...args){
  //声明一个空对象
  let obj = {}
  //将obj对象的__proto__指向构造函数的的原型
  Object.setPrototypeOf(obj, context.prototype)
  //改变构造函数this指向,指向obj
  let res = context.apply(obj, args)
  //若构造函数有返回值且其类型是个对象,则返回构造函数所返回的值,否则返回之前声明的对象
  return res instanceof Object ? res : obj
}

用new去实例化一个构造函数,其this会指向实例,优先级最高,即

let value = 2
let obj = {value: 1}
function foo () {
  console.log(this.value)
}
let fn = foo.bind(obj)
let f = new fn()

以上会打印出undefined,因为foo中的this指向变量f,而变量f中没有value值。

在bind的实现中,不只是改变函数this的指向然后包在一个函数里边就返回出来, 还要判断返回出去的函数是否用了new操作符。

Function.prototype.mybind = function (context) {
  //保存一份this,this是个函数
  const _this = this
  //获取调用bind时的参数
  const args = [...arguments].slice(1)
  context = context || window
  if(typeof this !== 'function'){
    throw new Error("error")
  }
  const resFn = function () {
    //因为返回的的数可以再传参数,故需要和bind时所传的参数一起收集
    const argus = args.concat([...arguments])
    //此处判断其是否用了new操作符
    //如果用了new,此时this指向其构造函数,即_this
    //若直接执行,this则是window,则是普通的bind
    return _this.apply(this instanceof _this ? this : context, argus)
  }
  //继承构造函数,故将其返回函数的原型指向new出来的实例
  //这里借助一个空函数
  const emptyFn = function () {}
  emptyFn.prototype = this.prototype
  resFn.prototype = new emptyFn()
  return resFn
}

回到之前的问题,为什么多次绑定了bind会无效

let obj1 = {value: 1}, obj2 = {value: 2}
function foo () {console.log(this.value)}
let fn = foo.mybind(obj1)
fn()  //1
let f = fn.mybind(obj2)
f()  //1     函数foo指向obj2无效

是不是mybind在用第二次就无效了呢?

并不是,mybind在用第二次依然有效,它也的确改变了this的指向,但改变的仅仅是它之前整个函数的指向

//(foo.mybind(obj1).mybind(obj2).mybind(obj3))()  拆解成以下代码
let first = foo.mybind(obj1)
let two = first.mybind(obj2)
let three = two.mybind(obj3)
three()
//执行结果依旧是foo执行的结果
//two的this指向obj3,但也仅仅是指向,并没有执行foo函数,而是执行了整个two
//同理,first的this指向obj2,执行first函数,
//一路往上执行到了first时,foo的this指向obj1,执行foo,才得到结果,此时this是指向obj1的
//也就是说first之后调用mybind的,并没有改变foo里this的指向,改变的是其之前整个函数的this的指向

-end-