这是我在公众号(高级前端进阶)看到的文章,现在做笔记  https://github.com/yygmind/blog/issues/22

call() 和 apply()

call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。

call() 和 apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

举个例子:

var func = function(arg1, arg2) {
...
}; func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组

常用用法

下面列举一些常用用法:

1、合并两个数组
var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot']; // 将第二个数组融合进第一个数组
// 相当于 vegetables.push('celery', 'beetroot');
Array.prototype.push.apply(vegetables, moreVegs);
// vegetables;
// ['parsnip', 'potato', 'celery', 'beetroot']

当第二个数组(如示例中的 moreVegs )太大时不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。不同的引擎有不同的限制,JS核心限制在 65535,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。

如何解决呢?方法就是将参数数组切块后循环传入目标方法

function concatOfArray(arr1, arr2) {
var QUANTUM = ;
for (var i = , len = arr2.length; i < len; i += QUANTUM) {
Array.prototype.push.apply(
arr1,
arr2.slice(i, Math.min(i + QUANTUM, len) )
);
}
return arr1;
} // 验证代码
var arr1 = [-, -, -];
var arr2 = [];
for(var i = ; i < ; i++) {
arr2.push(i);
} Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
2、获取数组中的最大值和最小值
var numbers = [,  ,  , - ];
Math.max.apply(Math, numbers); //458
Math.max.call(Math, , , , -); //458 // ES6
Math.max.call(Math, ...numbers); //

为什么要这么用呢,因为数组 numbers 本身没有 max 方法,但是 Math 有呀,所以这里就是借助 call / apply 使用 Math.max 方法。

3、验证是否是数组
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([, , ]);
// true // 直接使用 toString()
[, , ].toString(); // "1,2,3"
"".toString(); // "123"
.toString(); // SyntaxError: Invalid or unexpected token
Number().toString(); // "123"
Object().toString(); // "123"

可以通过toString() 来获取每个对象的类型,但是不同对象的 toString()有不同的实现,所以通过 Object.prototype.toString() 来检测,需要以 call() / apply() 的形式来调用,传递要检查的对象作为第一个参数。

另一个验证是否是数组的方法

var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){
return toStr(obj) === '[object Array]';
}
isArray([, , ]);
// true // 使用改造后的 toStr
toStr([, , ]); // "[object Array]"
toStr(""); // "[object String]"
toStr(); // "[object Number]"
toStr(Object()); // "[object Number]"

上面方法首先使用 Function.prototype.call函数指定一个 this 值,然后 .bind 返回一个新的函数,始终将 Object.prototype.toString 设置为传入参数。其实等价于 Object.prototype.toString.call() 。

这里有一个前提是toString()方法没有被覆盖

Object.prototype.toString = function() {
return '';
}
isArray([, , ]);
// false
4、类数组对象(Array-like Object)使用数组方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 不同环境下数据不同
// (505) ["h1", html.gr__hujiang_com, head, meta, ...]

类数组对象有下面两个特性

  • 1、具有:指向对象元素的数字索引下标和 length 属性
  • 2、不具有:比如 push 、shift、 forEach 以及 indexOf 等数组对象具有的方法

要说明的是,类数组对象是一个对象。JS中存在一种名为类数组的对象结构,比如 arguments 对象,还有DOM API 返回的 NodeList 对象都属于类数组对象,类数组对象不能使用 push/pop/shift/unshift 等数组方法,通过 Array.prototype.slice.call 转换成真正的数组,就可以使用 Array下所有方法。

类数组对象转数组的其他方法:

// 上面代码等同于
var arr = [].slice.call(arguments); ES6:
let arr = Array.from(arguments);
let arr = [...arguments];

Array.from() 可以将两类对象转为真正的数组:类数组对象和可遍历(iterable)对象(包括ES6新增的数据结构 Set 和 Map)。

PS扩展一:为什么通过 Array.prototype.slice.call() 就可以把类数组对象转换成数组?

其实很简单,slice 将 Array-like 对象通过下标操作放进了新的 Array 里面。

下面代码是 MDN 关于 slice 的Polyfill,链接 Array.prototype.slice()

Array.prototype.slice = function(begin, end) {
end = (typeof end !== 'undefined') ? end : this.length; // For array like object we handle it ourselves.
var i, cloned = [],
size, len = this.length; // Handle negative value for "begin"
var start = begin || ;
start = (start >= ) ? start : Math.max(, len + start); // Handle negative value for "end"
var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
if (end < ) {
upTo = len + end;
} // Actual expected size of the slice
size = upTo - start; if (size > ) {
cloned = new Array(size);
if (this.charAt) {
for (i = ; i < size; i++) {
cloned[i] = this.charAt(start + i);
}
} else {
for (i = ; i < size; i++) {
cloned[i] = this[start + i];
}
}
} return cloned;
};
}

PS扩展二:通过 Array.prototype.slice.call() 就足够了吗?存在什么问题?

在低版本IE下不支持通过Array.prototype.slice.call(args)将类数组对象转换成数组,因为低版本IE(IE < 9)下的DOM对象是以 com 对象的形式实现的,js对象与 com 对象不能进行转换。

兼容写法如下:

unction toArray(nodes){
try {
// works in every browser except IE
return Array.prototype.slice.call(nodes);
} catch(err) {
// Fails in IE < 9
var arr = [],
length = nodes.length;
for(var i = ; i < length; i++){
// arr.push(nodes[i]); // 两种都可以
arr[i] = nodes[i];
}
return arr;
}
}

PS 扩展三:为什么要有类数组对象呢?或者说类数组对象是为什么解决什么问题才出现的?

JavaScript类型化数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。 Array存储的对象能动态增多和减少,并且可以存储任何JavaScript值。JavaScript引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着Web应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问WebSockets的原始数据等,很明显有些时候如果使用JavaScript代码可以快速方便地通过类型化数组来操作原始的二进制数据,这将会非常有帮助。

一句话就是,可以更快的操作复杂数据。

5、调用父构造函数实现继承
function  SuperType(){
this.color=["red", "green", "blue"];
}
function SubType(){
// 核心代码,继承自SuperType
SuperType.call(this);
} var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"] var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]

在子构造函数中,通过调用父构造函数的call方法来实现继承,于是SubType的每个实例都会将SuperType中的属性复制一份。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

更多继承方案查看我之前的文章。JavaScript常用八种继承方案

call的模拟实现

以下内容参考自 JavaScript深入之call和apply的模拟实现

先看下面一个简单的例子:

var value = ;
var foo = {
value:
}; function bar() {
console.log(this.value);
} bar.call(foo); //
模拟实现第一步

如果在调用call()的时候把函数 bar()添加到foo()对象中,即如下

var foo = {
value: ,
bar: function() {
console.log(this.value);
}
}; foo.bar(); //

这个改动就可以实现:改变了this的指向并且执行了函数bar

但是这样写是有副作用的,即给foo额外添加了一个属性,怎么解决呢?

解决方法很简单,用 delete 删掉就好了。

所以只要实现下面3步就可以模拟实现了。

  • 1、将函数设置为对象的属性:foo.fn = bar
  • 2、执行函数:foo.fn()
  • 3、删除函数:delete foo.fn

代码实现如下:

// 第一版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this; // foo.fn = bar
context.fn(); // foo.fn()
delete context.fn; // delete foo.fn
} // 测试一下
var foo = {
value:
}; function bar() {
console.log(this.value);
} bar.call2(foo); //

完美!

模拟实现第二步

第一版有一个问题,那就是函数 bar 不能接收参数,所以我们可以从 arguments中获取参数,取出第二个到最后一个参数放到数组中,为什么要抛弃第一个参数呢,因为第一个参数是 this

类数组对象转成数组的方法上面已经介绍过了,但是这边使用ES3的方案来做。

var args = [];
for(var i = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

参数数组搞定了,接下来要做的就是执行函数 context.fn()

context.fn( args.join(',') ); // 这样不行

上面直接调用肯定不行,args.join(',')会返回一个字符串,并不会执行。

这边采用 eval方法来实现,拼成一个函数。

eval('context.fn(' + args +')')

上面代码中args 会自动调用 args.toString() 方法,因为'context.fn(' + args +')'本质上是字符串拼接,会自动调用toString()方法,如下代码:

var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"] console.log(args.toString());
// a1,b2,c3 console.log("" + args);
// a1,b2,c3

所以说第二个版本就实现了,代码如下:

// 第二版
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
} // 测试一下
var foo = {
value:
}; function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
} bar.call2(foo, 'kevin', );
// kevin
// 18
//

完美!!

模拟实现第三步

还有2个细节需要注意:

  • 1、this 参数可以传 null 或者 undefined,此时 this 指向 window
  • 2、函数是可以有返回值的

实现上面的两点很简单,代码如下:

// 第三版
Function.prototype.call2 = function (context) {
context = context || window; // 实现细节 1
context.fn = this; var args = [];
for(var i = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
} var result = eval('context.fn(' + args +')'); delete context.fn
return result; // 实现细节 2
} // 测试一下
var value = ; var obj = {
value:
} function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
} bar.call2(null); // console.log(bar.call2(obj, 'kevin', ));
// 1
// {
// value: 1,
// name: 'kevin',
// age: 18
// }

完美!!!

call和apply模拟实现汇总

call的模拟实现

ES3:

Function.prototype.call = function (context) {
context = context || window;
context.fn = this; var args = [];
for(var i = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')'); delete context.fn
return result;
}

ES6:

Function.prototype.call = function (context) {
context = context || window;
context.fn = this; let args = [...arguments].slice();
let result = context.fn(...args); delete context.fn
return result;
}
apply的模拟实现

ES3:

Function.prototype.apply = function (context, arr) {
context = context || window;
context.fn = this; var result;
// 判断是否存在第二个参数
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = , len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')');
} delete context.fn
return result;
}

ES6:

Function.prototype.apply = function (context, arr) {
context = context || window;
context.fn = this; let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
} delete context.fn
return result;
}

实际上上面的代码还是有问题的,上面的代码里,我们假设context 对象本身没有 fn 属性,这是不行的。必须保证fn属性是唯一的

ES3下模拟实现

解决方法也很简单,首先判断 context中是否存在属性 fn,如果存在那就随机生成一个属性fnxx,然后循环查询 context 对象中是否存在属性 fnxx。如果不存在则返回最终值。

一种循环方案实现代码如下:

function fnFactory(context) {
var unique_fn = "fn";
while (context.hasOwnProperty(unique_fn)) {
unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
} return unique_fn;
}

一种递归方案实现代码如下:

function fnFactory(context) {
var unique_fn = "fn" + Math.random();
if(context.hasOwnProperty(unique_fn)) {
// return arguments.callee(context); ES5 开始禁止使用
return fnFactory(context); // 必须 return
} else {
return unique_fn;
}
}

模拟实现完整代码如下:

function fnFactory(context) {
var unique_fn = "fn";
while (context.hasOwnProperty(unique_fn)) {
unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
} return unique_fn;
} Function.prototype.call = function (context) {
context = context || window;
var fn = fnFactory(context); // added
context[fn] = this; // changed var args = [];
for(var i = , len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context[fn](' + args +')'); // changed delete context[fn]; // changed
return result;
} // 测试用例在下面
ES6下模拟实现

ES6有一个新的基本类型Symbol,表示独一无二的值,用法如下。

const symbol1 = Symbol();
const symbol2 = Symbol();
const symbol3 = Symbol('foo'); console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

不能使用 new 命令,因为这是基本类型的值,不然会报错。

new Symbol();
// TypeError: Symbol is not a constructor

模拟实现完整代码如下:

Function.prototype.call = function (context) {
context = context || window;
var fn = Symbol(); // added
context[fn] = this; // changed let args = [...arguments].slice();
let result = context[fn](...args); // changed delete context[fn]; // changed
return result;
}
// 测试用例在下面

测试用例在这里:

// 测试用例
var value = ;
var obj = {
value: ,
fn:
} function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
} bar.call(null);
// console.log(bar.call(obj, 'kevin', ));
// 1
// {value: 1, name: "kevin", age: 18} console.log(obj);
// {value: 1, fn: 123}

【进阶3-3期】深度广度解析 call 和 apply 原理、使用场景及实现(转)的更多相关文章

  1. 深度解析 Vue 响应式原理

    深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...

  2. 基于DPI(深度报文解析)的应用识别

    一.概述 1.DPI(Deep packet inspection,深度报文解析) 所谓“深度”是和普通的报文分析层次相比较而言的,“普通报文检测”仅分析IP包4 层以下的内容,包括源地址.目的地址. ...

  3. 二叉树 遍历 先序 中序 后序 深度 广度 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  4. JPA实体关系映射:@ManyToMany多对多关系、@OneToMany@ManyToOne一对多多对一关系和@OneToOne的深度实例解析

    JPA实体关系映射:@ManyToMany多对多关系.@OneToMany@ManyToOne一对多多对一关系和@OneToOne的深度实例解析 今天程序中遇到的错误一 org.hibernate.A ...

  5. C++ 进阶5 拷贝构造 深度复制 运算符重载

    C++ 进阶5 拷贝构造 深度复制 运算符重载 20131026 例子: 运行环境是G++ 编译, /* * main.cpp * *  Created on: 2013年10月26日 *      ...

  6. SSH深度历险(十) AOP原理及相关概念学习+AspectJ注解方式配置spring AOP

    AOP(Aspect Oriented Programming),是面向切面编程的技术.AOP基于IoC基础,是对OOP的有益补充. AOP之所以能得到广泛应用,主要是因为它将应用系统拆分分了2个部分 ...

  7. NET/ASP.NET Routing路由(深入解析路由系统架构原理)(转载)

    NET/ASP.NET Routing路由(深入解析路由系统架构原理) 阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模 ...

  8. java基础解析系列(七)---ThreadLocal原理分析

    java基础解析系列(七)---ThreadLocal原理分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)-- ...

  9. SSH深度历险(十一) AOP原理及相关概念学习+xml配置实例(对比注解方式的优缺点)

    接上一篇 SSH深度历险(十) AOP原理及相关概念学习+AspectJ注解方式配置spring AOP,本篇我们主要是来学习使用配置XML实现AOP 本文采用强制的CGLB代理方式 Security ...

随机推荐

  1. org.apache.jasper.JasperException: Unable to convert string

    最佳实践 不要使用idea生成的模板,头文件宁愿不要省事,除非知道有什么副作用. <!--<!DOCTYPE web-app PUBLIC--> <!--"-//Su ...

  2. leetcode 90. subsets

    解题思路: 要生成子集,对于vector 中的每个数,对于每个子集有两种情况,加入或不加入. 因此代码: class Solution { public: void subsetG(vector< ...

  3. Docker(五)如何构建Dockerfile

    摘自 https://mp.weixin.qq.com/s/_hq9dPe6390htN8BTkoQeQ 一.Dockerfile的指令集 由于Dockerfile中所有的命令都是以下格式:INSTR ...

  4. c++ 回调函数封装

    std::function<void(int a,int b)> ha; //函数封装  当成参数用callback  std::bind(&fun1,this,std::plac ...

  5. js对象数组去重

    <script> var array = [{ greeting: "Hello", nickName: "Aziz" }, { greeting: ...

  6. MySql cmd下的学习笔记 —— 有关修饰器的知识(trigger)

    关于触发器的理解: 进行数据库应用软件的开发时,有时我们碰到表的某些数据的改变时,希望同时 引起其他相关数据改变的需求,利用触发器就能满足这样的需求. 触发器能在表中的某些特定数据变化时自动完成某些查 ...

  7. TPU使用说明

    1 TPU分类和收费标准 1.1 分类和计费说明 地区 抢占式TPU Cloud TPU 美国 $1.35/hour $4.5/hour 欧洲 $1.485/hour $4.95/hour 亚太区地区 ...

  8. Struts2学习(二)

    1.Struts2的Servlet的API的访问 1.1   完全解耦合的方式 ActionContext context = ActionContext.getContext( ); 通过conte ...

  9. sonar排除实体类配置

    sonar覆盖率检查可以将一些实体类排除,maven项目可以在pom.xml文件中添加如下配置 <properties> <sonar.exclusions> src/main ...

  10. Wasserstein CNN: Learning Invariant Features for NIR-VIS Face Recognition

    承接上上篇博客,在其基础上,加入了Wasserstein distance和correlation prior .其他相关工作.网络细节(maxout operator).训练方式和数据处理等基本和前 ...