Call与Apply函数的分析及手写实现

409 阅读4分钟

关于Call与Apply函数其实已经是老生常谈了,面试基本也是必问环节了,其作用主要是设置函数体内this对象的值,以扩充函数赖以运行的作用域,这也没啥好说的了。

前言

JS中调用函数大概四种方法:

// 1.全局调用,this指向全局对象.
function fn1() {
  return this;
}
console.log(fn1()); // window  fn1() => window.fn1()

// 2.对象中调用,this指向该对象.
var obj = {
  fn2: function() {
    return this;
  }
}
console.log(obj.fn2()); // {fn2:function(){return this;}}

// 3.构造函数调, this指向构造函数本身
function fn3() {
  console.log(this);
}
console.log(new fn3()); // fn3 {}

// 4.使用call()或者apply()调用, this指向第一个参数
var obj = {
  name: 'yd'
}
function test(){
  console.log(this) 
}
test.call(obj) // {name: 'yd'}

最后的call调用,输出的this,为什么不是window呢?

其实以上的调用方式都隐含的传递了一个变量:this,这个this指向基本就是谁调用就指向谁,而Call与Apply的第一个参数就是用来改变这个this的,如果不传,默认为全局对象。

关于Call函数的作用我们简单打个比方

Call就像现实生活中的打电话,首先打电话前要拨号,这个号码就相当于this,必须有号码才有可能拨通电话,而拨打不同的号码,即Call(null)的参数值不同,接电话的人(作用域)也不同。而给接电话方传递的信息可以通过Call(null, param, param, param....)的其他不必须参数传递,接电话的人也可以通过函数的return回复消息!

这是个人的一个理解方法,它不棒嘛?(被打。。。)

Call 用法

// 例子1
var obj = {
  name: 'yd'
}
function say() {
  console.log(this.name);
}
say.call(obj); // yd

// 例子2
var str = 'abcde';
console.log(str.substr(1)); // bcde
console.log(''.substr.call(str, 1)); // bcde

// 例子3
var arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1)); // [2, 3, 4, 5]
var obj = {
  0: 1,
  1: 2,
  2: 3,
  3: 4,
  4: 5,
  length: 5
};
console.log([].slice.call(obj, 1)); // [2, 3, 4, 5]

Apply 用法

var obj = {
  color:'red'; 
};
window.color = 'blue';
function test(){
  console.log(this.color);
}
test();//blue
test.apply(this);//blue
test.apply(window);//blue
test.apply(null);//blue
test.apply(undefined);//blue
test.apply(obj);//red
test.apply('');//undefined

// 继承
function Dog(name){
  this.name = name;
  console.log(arguments); // ['小米', '第二个参数']
  this.sayName = function(){
    console.log(this.name);
  }
}
function Cat(name){
  // Dog.call(this,name);
  Dog.apply(this, [name, '第二个参数']);
}
var cat = new Cat('小米');
cat.sayName(); // 小米

区别:两者接收的参数不一样。(call参数为一个一个明确的值;apply参数为一个数组(argument),数组会解析成一个一个参数)

Call简单分析之手写实现

基本使用

var obj = {
  name: 'yd'
}
function test(){
  console.log(this.name) 
}
test.call(obj) // yd
  • 实现一 :相当我要重写Function.prototype.call方法。
/* 首先我们先做个假设如果我们将obj变成这样子:
var obj = {
  name: 'yd'
  test: function(){ 
          console.log(this.name) 
        }
}
obj.test()
这样是不是很简单的实现了,当然,还没完呢,这时obj无缘无故多了个属性,这是不好的,所以呢我们还要用delete删除
*/
// 完整代码如下:
Function.prototype.myCall = function(context){
  context.fn = this // this: 能得到test()函数[谁调就是谁]; fn: 叫什么名字其实都无所谓,反正要删除的
  context.fn()
  delete context.fn
}
var obj = {
  name: 'yd'
}
function test(){
  console.log(this.name) // yd
}
test.myCall(obj) 

Call能传递参数

var obj = {
  name: 'yd'
}
function test(nickname, age){
  console.log(this.name, nickname, age) // yd YDYDYDQ 20
}
test.call(obj, 'YDYDYDQ', 20) 
  • 实现二 :1.在myCall中接收到全部参数。2.将参数依次注入test()中使用。
/* 首先我们要想方法获取这些参数,因为参数数量是不确定的,所以呢我们只能通过arguments.
每个函数都有一个arguments,在myCall()中arguments如下:
arguments = {
  0: obj,
  1: 'YDYDYDQ',
  2: 20,
  length: 3
}
我们要取第二个到最后一个.
*/
// 完整代码如下(第一步):
Function.prototype.myCall = function(context){
  context.fn = this
  var args = []
  for(var i = 1;i < arguments.length;i++){ // 因为arguments是类数组对象,所以可以进行迭代
    args.push(arguments[i])
  }
  console.log(args) // ["YDYDYDQ", 20]
  context.fn()
  delete context.fn
}
var obj = {
  name: 'yd'
}
function test(){
  console.log(this.name) // yd
}
test.myCall(obj, 'YDYDYDQ', 20);

/* 那么第二步我们要想办法将ages变成一个一个参数,依次注入到test()函数中了.即我们要将参数放入fn()中.
  这里我们要借用eval(),不懂的可以先自行百度一下哦.
*/
// 完整代码如下(第二步):
Function.prototype.myCall = function(context){
  context.fn = this
  var args = []
  for(var i = 1;i < arguments.length;i++){ 
    args.push('arguments[' + i + ']') //因为eval()会解析args每个值,所以将数组值改成arguments[i]字符
  }
  eval('context.fn(' + args +')') // args数组会自动调用toString(),如eval('[1, 2]')  => [1, 2]
  // 本质变成这样执行: context.fn(arguments[1], arguments[2])
  delete context.fn
}
var obj = {
  name: 'yd'
}
function test(nickname, age){
  console.log(this.name, nickname, age) // yd YDYDYDQ 20
}
test.myCall(obj, 'YDYDYDQ', 20)
  • 现实三 :处理边界情况,其实就是处理一下this为null与其处理函数返回值,直接上代码吧。
Function.prototype.myCall = function(context){
  context = context || window 
  context.fn = this
  var args = []
  for(var i = 1;i < arguments.length;i++){ 
    args.push('arguments[' + i + ']') //因为eval()会解析args每个值,所以将数组值改成arguments[i]字符
  }
  var result = eval('context.fn(' + args +')') // args数组会自动调用toString()
  // 本质变成这样执行: context.fn(arguments[1], arguments[2])
  delete context.fn
  return result
}
var obj = {
  name: 'yd'
}
function test(nickname, age){
  console.log(this.name, nickname, age) // yd YDYDYDQ 20
  return '我是函数返回值'
}
console.log(test.myCall(obj, 'YDYDYDQ', 20))
// obj不传默认指向this

至此,Call函数的基本原理也算弄清楚了吧?(不清楚也不要问我....哈哈)

Apply函数手写实现

废话一下:Apply方法和Call方法几乎差不多,就参数列表不同而已,它只接收一个数组参数,废话不多说,直接上代码。

Function.prototype.myApply = function(context, arr){
  context = context || window 
  context.fn = this
  
  var result
  if(!arr){
     result = context.fn()
  }else{
    var args = [];
    for(var i = 0, len = arr.length;i < len; i++) {
        args.push('arr[' + i + ']');
    }
    result = eval('context.fn(' + args + ')')
  }
  delete context.fn
  return result
}
var obj = {
  name: 'yd'
}
function test(nickname, age){
  console.log(this.name, nickname, age) // yd YDYDYDQ 20
  return '我是函数返回值'
}
console.log(test.myApply(obj, ['YDYDYDQ', 20]))

完结,撒花撒花。^ .. ^