利用apply提高编程效率的方法总结

合理的使用apply/call函数能够帮助我们极大地简化代码,高效地解决问题。本文尝试总结apply方法的几种用法,并发现规律,以便在需要时能够想到该方法。因apply和call方法的用法几乎相同,差别仅在于参数的传入方法,故本文仅以apply方法为例。

方法介绍

apply是函数对象原型的一个方法(Function.prototype.apply),它能够改变函数在运行时的this指向,即,能够改变函数运行时的执行环境。该函数最多接受两个参数,第一个参数指定函数运行时的this指向,决定了执行环境,第二个参数为,将要传入函数的参数组成的数组或者类数组对象(如果是call函数,则参数直接传入)。

下面是一个常见的例子:

var dog = {
    sound: 'wang',
    makeSound: function() {
        console.log(this.sound);
    }
}

var cat = {
    sound: 'miao',
}

dog.makeSound.apply(cat);复制代码

例子中分别定义了一个dog对象和一个cat对象,它们都有sound变量,但是dog对象有一个makeSound方法,而cat没有,如果这里要求cat也要能够发声(makeSound),有两种直观的解决方案:

第一,给cat直接赋予一个makeSound方法

var cat = {
    sound: 'miao',
    makeSound: function() {
        console.log(this.sound);
    }
}复制代码

第二,利用原型继承

var Pets = function(sound) { this.sound = sound; }
Pets.prototype.makeSound = function() { console.log(this.sound); }

var dog = new Pets('wang');
var cat = new Pets('miao');
cat.makeSound(); // miao复制代码

但是,有了apply,一切都变得很简单:

dog.makeSound.apply(cat);// miao

cat既不需要有自己的makeSound方法,也不需要和dog继承自同一个父类,只要makeSound自己能用,那就可以拿来用。
以下的所有用法,其实都是针对apply方法两个方面的特性而来的:

  1. 可以传入this,改变函数运行时的执行环境。
  2. apply的第二个参数是函数参数组成的数组或者类数组,且被借用函数是以散列形式传参。

几种用途

1. 类数组借用数组方法

类数组虽然能和数组一样使用下标索引,但是它不具有数组拥有的内置方法,无法直接使用这些方法,幸运的是,类数组的特性决定了数组的方法也能在其对象上使用,这就给apply方法发挥的空间:

// 错误例子
var addOne = function() {
    return argument.map(function(a) {return a+1;});
}
var arr = addOne(1,2,3,4) // Uncaught TypeError: arguments.map is not a function

// 正确的例子
var addOne = function(){
    var arr = Array.prototype.slice.apply(arguments);
    return arr.map(function(a){return a+1;});
}
addOne(1,2,3,4); // [2,3,4,5]
// 这里使用call更加简单
var addOne = function() {
    return Array.prototype.map.call(arguments, function(a){ return a+1; })
}复制代码

上面的addOne函数将所有传入的参数分别加1,然后组成数组返回,函数的arguments就是由参数构成的一个类数组,我们无需取出这些参数再一个个加1,再push进数组,而是将类数组先转化为数组,然后使用数组的map方法。第一个错误的示例表明了类数组不具有数组方法,所以报错。

在DOM操作中,如document.getElementsByClassName, document.querySelectAll等方法拿到的对象都是类数组,一般来讲,只要转化成数组类型就会极大地方便我们的操作。

2. 求数组的最大最小值

求数组中的最大最小值操作是很常见的,但是,数组并没有为我们实现这样的操作,最直观的方法就是遍历数组,查找最大最小值,无疑,这种方法不仅笨拙低效,而且性能极差。但是,如果给你的不是一个数组,而就是一些数字呢?你可能会立即想到Math.max方法。这两种形式参数的关系恰好就符合我们之前提到的两个特性之二:函数要求以散列形式传参,apply又要求以数组或类数组传参。

Math.max.apply(null, [1,6,5,3,5]) // 6
Math.min.apply(null, [1,6,5,3,5] ) // 1复制代码
3. 准确判断对象类型

判断对象类型,我们有typeof函数可用,但是它的判断并不可靠,比如,对数组进行typeof操作,返回的却是"object"。而在Object的原型对象上,有一个toString方法,它作用在不同类型的对象上,返回特定的字符串,根据返回值可以准确地判断对象类型。

Object.prototype.toString.apply([]) // "[object, Array]"
Object.prototype.toString.apply({}) // "[object, Object]"
Object.prototype.toString.apply(undefined) // "[object, Undefined]"
Object.prototype.toString.apply(function(){}) // "[object, Function]"
Object.prototype.toString.apply(document.getElementsByClassName(div)) // "[object, NodeList ]"复制代码

该判断方法可以支持如下类型的判断:NodeList,Window, Object, String, Infinity, Number(NaN), Function, HTMLDocument, Undefined, Boolean。需要特别注意的是Number类型的判断,NaNInfinity 也会被识别为Number类型(可用如下规则判断:1/0 === Infinity, 1/-0 === -Infinity, NaN != NaN)。这里因为不涉及第二个参数的问题,所以使用call也完全是可以的。

4. 二维数组的扁平化

先来看看数组的concat方法的用法:

var a = [1,2,3];
var b = [4,5,6];
var c = a.concat(7,8,9) // [1,2,3,7,8,9]
var d = a.concat(b) // [1, 2, 3, 4, 5, 6]
var f = a.concat(7,8,b,9) //[1, 2, 3, 7, 8, 4, 5, 6, 9]
a // [1,2,3]
b // [4,5,6]复制代码

可以发现,concat既可以接受数组也可以接受散列参数,而且最终生成的结果都是一样的,都是一个一维的数组,同时,不改变原来的数组。如果将计算f的参数组成一个数组,那么就是一个二维数组,再结合apply接受数组作为第二个参数的特性,就可以实现一个二维数组的扁平化功能了:

var twoDemArr = [[1,2,3], [4,5,6], 7,8,9]
var arr = Array.prototype.concat.apply([],twoDemArr);
arr // [1,2,3,4,5,6,7,8,9]复制代码

进一步,我们看看那些接受散列参数的数组方法,如果结合apply会有什么样的作用:

push也接受散列参数,它将参数推入数组,并且改变了原数组,那么它就可以实现,将一个数组的元素推入另外一个数组,并且改变被推入数组:

var a = [1,2,3]
var b = [4,5,6]
Array.prototype.push.apply(a, b);
a // [1,2,3,4,5,6]复制代码

unshiftpush一样,不过是将元素加在数组前面:

var a = [1,2,3]
var b = [4,5,6]
Array.prototype.unshift.apply(a, b);
a // [4,5,6,1,2,3]复制代码
5. 修正内部函数的this指向

在函数内部定义的函数,如果直接调用,则该内部函数的this并不指向外层函数的this,而是指向全局执行环境,所以调用内部函数必须指明其this的指向

// 问题代码示例
document.getElementById('div1').onclick = function() {
    alert(this.id) // div1
    var func = function() {
        alert(this.id);
    }
    func(); // undefined
}
// 正确代码示例
document.getElementById('div1').onclick = function() {
    alert(this.id) // div1
    var func = function() {
        alert(this.id);
    }
    func.apply(this); // div1
}复制代码

当然,在外层保存this变量,然后在内部函数定义中直接使用保存的变量也可以达到相同的效果,但是使用apply的方法相对而言,保持了内部函数的独立性。

6. 给既有方法打补丁

看下面一段代码:

// 保存原函数
var originalfoo = someobject.foo;
someobject.foo = function() {
    // 在这里添加需要在原函数调用前执行的操作
    console.log(arguments);
    // 调用原函数
    originalfoo.apply(this, arguments);
    // 在这里添加需要在原函数调用后执行的操作
}复制代码

上面的例子,在调用原来的方法之前或者之后,执行了新的操作,补充增强了原来的方法,而且不改变原来的操作。这也是设计模式中装饰者模式的实现思路

举一个应用场景:假如你从上一位开发者手中接过了一个项目,你需要在不改动原来功能的基础上开发一个新功能,你找到了这个功能的函数位置,但是因为代码组织很糟糕,你几乎看不懂这段代码做了什么,所以也不敢轻易改动。怎么办?也许上面利用apply打补丁的方法值得一试。只需在调用前后添加新操作,然后按照其调用方法调用,完全不用关心原函数的实现细节如何。

结语

善用applycall方法,可以大幅提高我们编程的效率,提升程序性能。这里仅仅总结了一部分apply的用法,apply的强大之处肯定远远不止如此,更多的用法还有待我们进一步的发现和总结。

你有什么好的用法,欢迎留言,我继续补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值