这是我在公众号(高级前端进阶)看到的文章,现在做笔记 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}