专栏首页前端进阶学习学习Javascript之模拟实现call,apply
原创

学习Javascript之模拟实现call,apply

前言

本文1630字,阅读大约需要8分钟。

总括: 本文从零开始通过提出问题然后解决问题的方式模拟实现了比较完善的call和apply方法

每一个不曾起舞的日子,都是对生命的辜负。

正文

call,apply简介

首先介绍下call和apply两个方法,这两个方法都是挂载在函数的原型上的,所以所有的函数都可以调用这两个方法。

注意:call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组

例子:

function foo(b = 0) {
	console.log(this.a + b);
}
const obj1 = {
	a: 1
};
const obj2 = {
	a: 2
};
foo.call(obj1, 1); // 2
foo.call(obj2, 2); // 4
foo.apply(obj1, [1]); // 2
foo.apply(obj2, [2]); // 4

对于this不熟悉的同学可以先异步:理解Javascript的this。总结起来一句话:Javascript函数的this指向调用方,谁调用this就指向谁,如果没人谁调用这个函数,严格模式下指向undefined,非严格模式指向window

所以本质上call和apply就是用来更改被调用函数的this值的。如上,call和apply只有参数的不同,模拟实现了call,那么apply就只是参数处理上的区别。也就是说,call和apply干了两件事:

  1. 改变被调用函数的this值;
  2. 传参调用;

###更改this

现在模拟实现call和apply的问题转移到另一个问题上,即如何去更改一个函数的this值,很简单:

function foo(b = 0) {
	console.log(this.a + b);
}
const obj1 = {
	a: 1,
  foo: foo
};
const obj2 = {
	a: 2,
  foo: foo
};
obj1.foo(1);
obj2.foo(2);

也就是说我们把这个方法赋值给对象,然后对象调用这个函数就可以了。改变一个函数的this步骤很简单,首先将这个函数赋值给this要指向的对象,然后对象调用这个函数,执行完从对象上删除掉这个函数就好了。步骤如下:

obj.foo = foo;
obj.foo();
delete obj.foo;

有了思路我们实现第一版call方法

Function.prototype.call2 = function(context) {
  context = context || {};
  context[this.name] = this;
  context[this.name]();
  delete context[this.name];
}

this.name是函数声明的名称,但其实是没必要一定对应函数名称的,我们随便用一个key都可以:

Function.prototype.call2 = function(context) {
  context = context || {};
  context.func = this;
  context.func();
  delete context.func;
}

使用新的call调用上面的函数:

foo.call2(obj1); // 1
foo.call2(obj2); // 2

OK,this的问题解决了,接下来就是传参的问题:

传参

函数中的参数保存在一个类数组对象arguments中。因此我们可以从arguments里面去拿从传到call2里面的参数:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  context.func = this;
  context.func();
  delete context.func;
}

此时问题来了,如何把参数params传递到func中呢?比较容易想到的办法是利用ES6的扩展运算符

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  context.func = this;
  context.func(...params);
  delete context.func;
}

看下我们的例子:

foo.call2(obj1, 1); // 2
foo.call2(obj2, 2); // 4

还有一个实现,是利用不常用的eval函数,即我们把参数拼接成一个字符串,传给eval函数去执行,

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

看下我们的第二版实现:

Function.prototype.call2 = function(context) {
  context = context || {};
  var params = [];
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // 注意,此处的this是指的被调用的函数
  context.func = this;
  eval('context.func(' + params.join(",") + ')');
  delete context.func;
}

其它

callapply还有另外两个重要的特性,可以正常返回函数执行结果,接受nullundefined为参数的时候将this指向window,然后我们来实现下这两个特性,然后加上必要的判断提示,这是我们的第三版实现

Function.prototype.call2 = function(context) {
  context = context || window;
  var params = [];
  // 此处将i初始化为1,是为了跳过context参数
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // 注意,此处的this是指的被调用的函数
  context.func = this;
  var res = eval('context.func(' + params.join(",") + ')');
  delete context.func;
  return res;
}

然后我们调用测试下:

foo.call2(obj1, 1); // 2

foo.call(2, 1); // NaN
foo.call2(2, 1); // context.func is not a function

如上我们发现将对象改成数字2后原始call返回了NaN,我们的call2却报错了,说明一个问题,我们直接context = context || window是有问题的。内部还有一个类型判断,解决这个问题后,我们的第四版实现如下:

Function.prototype.call2 = function(context) {  
  if (context === null || context === undefined) {
		context = window;
  } else {
		context = Object(context) || context;
  }
  var params = [];
  // 此处将i初始化为1,是为了跳过context参数
 	for (var i = 1; i < arguments.length; i++) {
    params[i - 1] = arguments[i];
	}
  // 注意,此处的this是指的被调用的函数
  context.func = this;
  var res = eval('context.func(' + params.join(",") + ')');
  delete context.func;
  return res;
}

这就是我们的最终代码,这个代码可以从ES3一直兼容到ES6,此时:

foo.call(2, 1); // NaN
foo.call2(2, 1); // NaN

模拟实现apply

apply和call只是参数上的区别,将call2改写就好了:

Function.prototype.apply2 = function(context, arr) {
  if (context === null || context === undefined) {
		context = window;
  } else {
		context = Object(context) || context;
  }
  // 注意,此处的this是指的被调用的函数
  context.func = this;
  arr =  arr || [];
  var res = eval('context.func(' + arr.join(",") + ')');
  delete context.func;
  return res;
}

以上就是我们最终的实现,目前还有一个问题就是context.func的问题,这样一来我们传进来的context就不能使用func字符串作为方法名了。

结论

我们实现过程都解决了以下问题:

  1. 更改被调用函数的this
  2. 将参数传递给被调用函数;
  3. 将被调用函数结果返回,第一个参数为nullundefined的时候被调用函数的this指向window;
  4. 解决类型判断的问题;

以上。


能力有限,水平一般,欢迎勘误,不胜感激。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 学习Javascript之尾调用

    总括: 本文介绍了尾调用,尾递归的概念,结合实例解释了什么是尾调用优化,并阐述了尾调用优化如今的现状。

    Damonare
  • HTML5之Canvas

    document.createElement("canvas").getContext("2d");

    疯狂的技术宅
  • HTML5动态时钟

    felix
  • canvas荧光表源码分享

    Youngxj
  • Canvas绘图——2d表

    初学JavaScript,用Canvas画一个表。主要用到昨天学的间歇调用(setInterval)。 方法和属性介绍 context.beginPath()、...

    刘开心_1266679
  • canvas 实现自定义钟表

    参考博客:https://www.cnblogs.com/liugang-vip/p/3557983.html

    acoolgiser
  • Canvas 基本绘制(下)

    HTML5学堂:在前一篇文章《Canvas 基本绘制(上)》当中,我们为大家介绍了Canvas的基本知识——什么是Canvas、如何使用Canvas进行图像的绘...

    HTML5学堂
  • 06. Web大前端时代之:HTML5+CSS3入门系列~HTML5 画布(下)

    矩 阵 变 化 其实像 translate(移动),scale(缩放),rotate(旋转)都是特殊的矩阵变换 transform(m11,m12,m21,m2...

    逸鹏
  • 06. Web大前端时代之:HTML5+CSS3入门系列~HTML5 画布(上)

    Web大前端时代之:HTML5+CSS3入门系列:http://www.cnblogs.com/dunitian/p/5121725.html 点击原文,查看笔...

    逸鹏
  • sjtuLib爬虫(一)

    前段时间想看一本《天才在左,疯子在右》的书,到图书馆网站一看,发现被预约了十次之多,只能说当时我就醉了。

    钱塘小甲子

扫码关注云+社区

领取腾讯云代金券