JavaScript call,apply,bind详解及实现

1.前言

call,apply,bind这三个方法都是用来改变函数的this指向,如果有对this不熟悉的朋友,可以先看看笔者的这篇博客

call & apply

call()语法:

function.call(thisArg, arg1, arg2, ...)
  • thisArg
    可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
  • arg1, arg2, …
    指定的参数列表。
  • 返回值
    使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
    apply()语法
    apply()方法的语法和 call() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。
var person1 = {
  first_name : 'Niall',
  last_name : 'Horan',
  getFull : function (first = this.first_name,last=this.last_name){
    console.log(first + ' ' + last)
  }
}
var person2 = {
  first_name: 'Sam',
  last_name: 'Smith'
}

person1.getFull.call(person2)
person1.getFull.apply(person2)
person1.getFull.call(person2,'Alan','Walker')
person1.getFull.apply(person2,['Alan','Walker'])

输出:

Sam Smith
Sam Smith
Alan Walker
Alan Walker

那么,callapply可以改变函数内this的指向,那么,可以通过这种方式来达到继承的效果:

function Product(name, price) {
	this.name = name;
	this.price = price;
}

function Food(name, price) {
	Product.call(this, name, price); //
	this.category = food;
}

var hotDog = new Food('hotDog', 20);

模拟实现call

当我们使用call()时:

person1.getFull.call(person2,'Alan','Walker')
  • call简单来说就是改变了函数内部this的指向,使person1.getFull中的this指向了person2
  • 执行了person1.getFull()
  • call()传入的参数由两部分组成,新的this,和原函数所需要的参数

Ⅰ. 改变this的指向

如何实现呢?
person1.getFull()中的this是指向person1的,因为getFull()是由person1调用的,使用call方法治好,this指向了person2,换一个思路,如果不是person1调用的getFull()中的this指向变了,而是getFull()由person1调用变成了person2调用?

Function.prototype.myCall = function (obj){
  //函数调用了myCall,在函数内,this指向该函数,记做“调用函数”
  //为obj增加一个属性func,func是一个函数,把“调用函数”赋值给func
  obj.func = this
  //obj调用func(),相当于obj调用了“调用函数”,也就是getFull
  // 注意,我们现在还没有给“调用函数”传递参数
  obj.func()
  delete obj.func//删除obj的func属性,还原如初
}
person1.getFull.myCall(person2)

输出:

Sam Smith

2. 传入参数

Function.prototype.myCall = function (obj){
  //函数调用了myCall,在函数内,this指向该函数,记做“调用函数”
  //为obj增加一个属性func,func是一个函数,把“调用函数”赋值给func
  obj.func = this
  var args = []
  for(let i = 1; i < arguments.length; i++){
    args.push('arguments[' + i + ']');
  }
  //obj调用func(),相当于obj调用了“调用函数”,也就是getFull
  eval('obj.func('+args+')')
  delete obj.func//删除obj的func属性,还原如初
}
person1.getFull.myCall(person2,'Alan','Walker')

输出:

Alan Walker

在给args赋值的时候,应当格外小心,如果这样写,可能会报错:

  for(let i = 1; i < arguments.length; i++){
    args.push('arguments[' + i + ']');
  }

当参数为全为数字时,以上代码不会出现问题,但是当参数不全是数字时,就会出现以下问题:

ReferenceError: Alan is not defined

在执行eval时,会将所有参数安装字符串连接再执行,上面两种写法对应的执行代码分别是:

1.obj.func(arguments[1],arguments[2])
2.obj.func(Alan,Walker)

这样,第二种写法就会报错,因为eval()将Alan当成变量名处理了,而不是字符串。

3. 返回值

虽然这里的person1.getFull()没有返回值,但是函数有返回值的情况并不少见。

Function.prototype.myCall = function (obj){
  //函数调用了myCall,在函数内,this指向该函数,记做“调用函数”
  //为obj增加一个属性func,func是一个函数,把“调用函数”赋值给func
  obj.func = this
  var args = []
  for(let i = 1; i < arguments.length; i++){
    args.push(arguments[i]);
  }
  //obj调用func(),相当于obj调用了“调用函数”,也就是getFull
  var res = eval('obj.func('+args+')')
  delete obj.func//删除obj的func属性,还原如初
  return res
}

4. 完善没有设置新的this时的myCall

在调用call()方法时,我们可以不设定新的this指向,此时,函数中this的指向会自动替换为全局对象:

console.log(Math.max.call(null,1,2,3,4))

输出:

4

完善myCall()

Function.prototype.myCall = function (obj){
  //函数调用了myCall,在函数内,this指向该函数,记做“调用函数”
  //是否存在obj,如不存在,替换为window
  var obj = obj || window
  //为obj增加一个属性func,func是一个函数,把“调用函数”赋值给func
  obj.func = this
  var args = []
  for(let i = 1; i < arguments.length; i++){
    args.push('arguments[' + i + ']');
  }
  //obj调用func(),相当于obj调用了“调用函数”,也就是getFull
  var res = eval('obj.func('+args+')')
  delete obj.func//删除obj的func属性,还原如初
  return res
}

5. bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用 —— MDN

bind和call相比,它将一个改变了this指向的函数保存了下来,可以多次调用该函数:

let person2_getFull = person1.getFull.bind(person2)
person2_getFull('August','Peter')
person2_getFull('Harry','Dank')

输出:

August Peter
Harry Dank

6. 模拟实现bind

person1.getFull.bind(person2)中,bind做了什么呢?

  • 改变了this的指向;
  • 返回了改变后的函数。
Function.prototype.myBind = function (obj){
  var func = this //将调用函数赋值给func
  var obj = obj || window
  //返回一个函数,在该函数中调用了func.apply。
  //记binded = function.myBind(obj),当我们调用binded()时,在binded内部
  //触发了func.apply(obj,arguments),这里的arguments就是我们调用binded()时传入的参数
  //注意,arguments是数组,所以这里使用apply更方便一些
  return function (){
    return func.apply(obj,arguments)
  }
}
let person2_getFull_2 = person1.getFull.myBind(person2)
person2_getFull_2('August','Peter')
person2_getFull_2('Harry','Dank')

输出:

August Peter
Harry Dank

版权声明:本文为Niall_Tonshall原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。