目录
  1. 概述
  2. Function的那些事儿
    1. arguments不是Array,但可以构造出一个Array对象来!
    2. 相同功能,不同调用形式的两方法——apply和call
    3. 去优雅地使用apply和call吧!
    4. 使用bind方法,再造函数!
    5. uncurryThis,你知道吗?
  3. Array的那些事儿
    1. 可以浅度复制数组的slice方法
    2. 不仅仅作用于数组的堆栈操作,四方法:push/pop/shift/unshift
    3. 可删可插入的splice方法
    4. forEach/map/reduce的实现与效率!
    5. 其他:join/concat/sort/reverse/...
  4. 结束

一、概述

在JavaScript中,内置对象Array和Function本身提供了不少方法,有些方法为人所熟知,有些方法则不被注意。而有些方法虽然被人所熟悉,却又有不被重视的使用场景,去实现一些妙用。

本文结合当下自己的使用心得,一方面做个分享,一方面也是个备忘,哈哈!

二、Function的那些事儿

2.1 arguments不是Array!

arguments是函数被执行时,传入的实参集合。他直接在函数里面被类似于一个数组进行访问。

在Chrome浏览器的控制台执行下面代码(本文代码都可以在Chrome浏览器的控制台中运行):

function testFunc() { 
    console.log(arguments);
    
    for(var i=0; i<arguments.length; ++i) {
        console.log(arguments[i]);
    }
        
    for(var idx in arguments) {
        console.log(arguments[idx]);
    }
}

传入参数运行:

testFunc(1,2,3,4)

执行结果:

[1, 2, 3, 4]
1
2
3
4
1
2
3
4

从上面例子中,我们可以访问argumentslength属性,来确定实参的个数,以及通过下标对arguments进行访问,获取我们需要的某个参数。

注意:arguments不是一个Array!

示例代码:

var testFunc = function() { 
    console.log("[] 是一个 Array?"+Array.isArray([]));
    console.log("arguments 是一个Array?"+Array.isArray(arguments));
    console.log("arguments toString:" + arguments.toString());
}

传入参数运行:

testFunc(1,2,3,4)

执行结果:

[] 是一个 Array?true
arguments 是一个Array?false
arguments toString:[object Arguments] 

提示:我们可以基于arguments构造一个数组对象

示例代码:

var testFunc = function() { 
    var args = Array.prototype.slice.apply(arguments);
    console.log("args 是一个 Array?"+Array.isArray(args));
    console.log(args);
}

运行:

testFunc(1,2,3,4)

结果:

args 是一个 Array?true
[1, 2, 3, 4]

当然,你也可以在构造的同时,对实参进行裁剪。参考代码如下:

var testFunc = function() { 
    var args = Array.prototype.slice.call(arguments, 1);
    console.log(args);
}

2.2 相同功能,不同参数形式的两方法——apply和call

这两个方法的功能相同,只是定义参数方式不同: 它们的作用都是将函数绑定到另外一个对象上去运行,两者仅在定义参数方式有所区别:

Function.apply(thisArg,argArray);

Function.call(thisArg[,arg1,arg2…]);

他们的功能,都是将函数绑定到一个指定的对象上去运行,即所有函数内部的this指针都会被赋值为thisArg。

这种功能,可以实现将函数作为特定对象的方法,进行执行的目的。

为什么会有这两种不同的形式呢?是为满足需求,而决定的!

Function.call的形式很自然,调用起来很方便,就像2.1小节最后那个例子那样。在这个例子中,你肯定不愿意使用Function.apply,是不是?

但是,它存在一个缺点:当函数的参数个数是动态的,只能在运行过程中才能确定下来,那么使用Function.call就不合适了,就只能使用Function.apply了。在运行过程中,将动态的参数都放到数组中去,然后把数组作为一个参数,传给Function.apply。这样就完美的解决问题了!

2.3 去优雅地使用apply和call吧

有一个动态数组:

var numbers=[]
for(var i=0; i<100; ++i) {
    numbers[i] = Math.floor(Math.random()*10000);
}

要求:从中找到最大数和最小数。

先看看常规的方法吧:

max = -Infinity, min = +Infinity;

numbers.forEach(function(item) {
  if (item > max) max = item;
  if (item < min) min = item;
})

那么,如何优雅地使用apply去编码呢?

var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);

就执行效率而言,后者也比前者快!

当然,就解决这个题目而言,还有更快的方案,有兴趣地话,可以看看笔者的测试:http://jsperf.com/apply-vs-loop-for-max/2。这个测试的结果,可能会颠覆你的认知,让你惊讶的。^_^

2.4 使用bind方法,定制函数!

先看定义:

fun.bind(thisArg[, arg1[, arg2[, …]]])

如果说,前面的applycall方法是将函数绑定到特定的对象上去执行。那么,bind方法,就是只绑定,不执行。

它生成一个新的方法,它可以给已知的函数fun绑定执行的对象thisArg,还可以绑定执行的参数[, arg1[, arg2[, ...]]],绑定的参数可以是部分,也可以是全部。

所以,就实现的功能而言,bind方法也可以借助apply方法,用下面代码模拟:

Function.prototype.bind = function(ctx) {
    var fn = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        fn.apply(ctx, args.concat(Array.prototype.slice.call(arguments)));
    };
}

那么,bind有哪些作用,适合用在哪些场合呢?

看下面这段代码:

背景设定:

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

调用:

module.getX(); // 81

如你所愿,执行结果是 81.

有时,你可能不经意间,进行赋值:

var getX = module.getX;

再运行:

getX(); // 9, because in this case, "this" refers to the global object

可能,你希望结果是 81,但却是9.

怎么才能让结果仍然是 81呢?

// create a new function with 'this' bound to module
var boundGetX = getX.bind(module);
boundGetX(); // 81

是的,如上使用bind就可以了!

2.5 uncurryThis,你知道吗?

uncurryThis话题,来自于Brendan Eich(JavaScript之父)的一个tweet.

uncurryThis的最重要的用途,就是将对象的方法变为函数去使用

特殊一点,可以将A对象的方法a使用到B对象上去。在前面2.1小节中,我们就用到了这个技巧,在Arguments对象上使用了Array对象的方法slice。很有用,不是吗?

那么怎么实现uncurryThis呢?

  • 扩展Function原型去实现(Brendan Eich写的实现代码):

    Function.prototype.uncurryThis = function () {
          var f = this;
          return function () {
              var a = arguments;
              return f.apply(a[0], [].slice.call(a, 1));
          };
    };
    

    使用:

    var toUpperCase = String.prototype.toUpperCase.uncurryThis();
    [ "foo", "bar", "baz" ].map(toUpperCase)
    

    运行结果:

    [ 'FOO', 'BAR', 'BAZ' ]
    
  • 独立的函数实现:

    var uncurryThis = function(f) {
          var call = Function.call;
          return function() {
              return call.apply(f, arguments);
          };
    };
    

    使用:

    var toUpperCase = uncurryThis(String.prototype.toUpperCase);
    [ "foo", "bar", "baz" ].map(toUpperCase)
    

    运行结果:

    [ 'FOO', 'BAR', 'BAZ' ]
    

    三、Array的那些事儿

Array是个很常用的内置对象,在Javascript规范的逐步完善中,其内置方法在不知不觉中已经提供了很多了。本文仅介绍一些常用的方法。

3.1 可以浅度复制数组的slice方法

定义:

arr.slice(begin[, end])

说明:返回一个新的浅度复制数组,包含从 beginend (不包括该元素)的 arr 中的元素。(原数组内容不发生变化)

beginend表示在原数组上,选取的范围。如果end省略,表示原数组的结尾。 如果begin省略,表示从0位开始。也可以是负数,表示从原数组的尾部开始计算位置。

举例:

var arr = [1,2,3,4,5,6,7,8];
arr.slice(2,4);      // [3, 4]
arr.slice(4);        // [5, 6, 7, 8]
arr.slice(-3);       // [6, 7, 8]
arr.slice(3, -2);    // [4, 5, 6]
arr.slice(-3, -1);   // [6, 7]
arr.slice();         // [1,2,3,4,5,6,7,8]

提示:利用Array.prototype.slice,我们可以将一个类似array对象,转换为array对象

arguments不是数组,但可以转换为数组。

var args = Array.prototype.slice.call(arguments);

我们还可以看一个例子:

function a() { return {length:2, 0:"hello", 1:"world"}}

var t = a();

var z = Array.prototype.slice.call(t);

t instanceof Array;      // false
z instanceof Array       // true
console.log(z);          // ["hello", "world"] 

3.2 不仅仅作用于数组的堆栈操作,四方法:push/pop/shift/unshift

先看定义:

arr.push(element1, …, elementN)

arr.pop()

arr.shift()

arr.unshift(element1, …, elementN)

说明:

  • push将n个元素element1, ..., elementN追加到数组的尾部,其返回值为调用后数组的长度;

    var sports = ["soccer", "baseball"];
    var total = sports.push("football", "swimming");
    console.log(sports); // ["soccer", "baseball", "football", "swimming"]
    console.log(total);  // 4
    
  • pop将数组的最后一个对象删除,其返还值就是这个删除的元素。如果是个空数组,则返回值为undefined;

    var myFish = ["angel", "clown", "mandarin", "surgeon"];
    var popped = myFish.pop();
    console.log(myFish);  // ["angel", "clown", "mandarin"] 
    console.log(popped);  // "surgeon"
    
  • shift 删除数组的第一个数,并且数组内剩余的值,坐标依次前移。返回值为删掉的元素。如果是个空数组,那么返回值为undefined;

    var myFish = ["angel", "clown", "mandarin", "surgeon"];
    var shifted = myFish.shift();
    console.log(myFish);   // ["clown", "mandarin", "surgeon"]
    console.log(shifted);  // "angel"
    
  • unshift将n个元素element1, ..., elementN插入道数组的头部。其返回值为调用后数组的长度。

    var arr = [1, 2];
    arr.unshift(0); // result of call is 3, the new array length
    // arr is [0, 1, 2]
    arr.unshift(-2, -1); // = 5
    // arr is [-2, -1, 0, 1, 2]
    arr.unshift( [-3] );
    // arr is [[-3], -2, -1, 0, 1, 2]
    

通过上面这四个方法,可以对数组进行堆栈操作了。

注意:这四个用作堆栈操作的方法,其实还支持非数组对象。不过,支持的对象需要类似数组。从某种角度而言,这也是支持你实现自定义的具有堆栈功能的对象。

所谓的类似数组,就是有length属性,可以通过0...n下标去访问元素。仅此,即可!

看代码:

var myQueue = function() {return {length:0}};

var tz = new myQueue();

Array.prototype.push.call(tz, "hello", "world");  // 2

console.log(Array.prototype.slice.call(tz));  //["hello", "world"] 

Array.prototype.shift.call(tz);     // "hello"

console.log(Array.prototype.slice.call(tz));  //["world"] 

Array.prototype.unshift.call(tz, "hello");    // 2

console.log(Array.prototype.slice.call(tz));  //["hello", "world"] 

Array.prototype.pop.call(tz);     // "world"

console.log(Array.prototype.slice.call(tz));  //["hello"] 

神奇吧!

3.3 可删可插入的splice方法

先看定义:

array.splice(index , howMany[, element1[, …[, elementN]]])

array.splice(index) // SpiderMonkey/Firefox/Chrome extension

其中, index表示数组中操作所开始的位置。如果大于数组的长度,那就重定位为数组的尾端。如果是负数,那就从数组的尾端向前移。howMany,表示要删除的个数,如果是0,则表示不删除。[, element1[, ...[, elementN]]],表示要插入的个数。 该方法的返回值为:删除的元素数组。

看实例:

var myFish = ["angel", "clown", "mandarin", "surgeon"];

//removes 0 elements from index 2, and inserts "drum"
var removed = myFish.splice(2, 0, "drum");
//myFish is ["angel", "clown", "drum", "mandarin", "surgeon"]
//removed is [], no elements removed

//removes 1 element from index 3
removed = myFish.splice(3, 1);
//myFish is ["angel", "clown", "drum", "surgeon"]
//removed is ["mandarin"]

//removes 1 element from index 2, and inserts "trumpet"
removed = myFish.splice(2, 1, "trumpet");
//myFish is ["angel", "clown", "trumpet", "surgeon"]
//removed is ["drum"]

//removes 2 elements from index 0, and inserts "parrot", "anemone" and "blue"
removed = myFish.splice(0, 2, "parrot", "anemone", "blue");
//myFish is ["parrot", "anemone", "blue", "trumpet", "surgeon"]
//removed is ["angel", "clown"]

//removes 2 elements from index 3
removed = myFish.splice(3, Number.MAX_VALUE);
//myFish is ["parrot", "anemone", "blue"]
//removed is ["trumpet", "surgeon"]

提示:我们可以用splice方法来实现数组的堆栈操作:push/pop/shift/unshift方法

  • 函数方式实现:
var push = function(arr) {
    var args = Array.prototype.slice.call(arguments,1);
    args = [arr.length, 0].concat(args);
    Array.prototype.splice.apply(arr, args);
    return arr.length;
}

var pop = function(arr) {
    if (arr.length==0) return undefined;
    var el = Array.prototype.splice.call(arr, arr.length-1);
    return el[0];
}

var shift = function(arr) {
    if (arr.length==0) return undefined;
    var el = Array.prototype.splice.call(arr, 0, 1);
    return el[0];
}


var unshift = function(arr) {
    var args = Array.prototype.slice.call(arguments,1);
    args = [0, 0].concat(args);
    Array.prototype.splice.apply(arr, args);
    return arr.length;
}

  • 原型方式实现:
Array.prototype.push = function() {
    var self = this;
    var args = Array.prototype.slice.call(arguments);
    args = [self.length, 0].concat(args);
    Array.prototype.splice.apply(self, args);
    return self.length;
}

Array.prototype.pop = function() {
    var self = this;
    if (self.length==0) return undefined;
    var el = Array.prototype.splice.call(self, self.length-1);
    return el[0];
}

Array.prototype.shift = function() {
    var self = this;
    if (self.length==0) return undefined;
    var el = Array.prototype.splice.call(self, 0, 1);
    return el[0];
}


Array.prototype.unshift = function() {
    var self = this;
    var args = Array.prototype.slice.call(arguments);
    args = [0, 0].concat(args);
    Array.prototype.splice.apply(self, args);
    return self.length;
}

由于是使用splice方法来替代实现的,所以,这些方法不能用于类似数组对象。

笔者做了下性能对比测试:原生的最快,原型形式模拟的次之,函数形式模拟的最慢。性能测试见:http://jsperf.com/splice-vs-push-pop-shift-unshift

3.4 forEach/map/reduce的实现与效率!

先看定义:

arr.forEach(callback[, thisArg])

arr.reduce(callback,[initialValue])

arr.map(callback[, thisArg])

给使用示例:

var numbers=[]
for(var i=0; i<100; ++i) {
    numbers[i] = i;
}

var sum = 0;
numbers.forEach(function(item) {
  sum += item;
})

console.log(sum);   //4950

var sum = numbers.reduce(function(sum, item){
    return sum+item;
}, 0);

console.log(sum);  //4950

numbers.slice(1,4).map(function(item){return Math.pow(item,2)});  //[1, 4, 9]

不太想写新手入门教程。写到这里,想说的是注意下面几点:

3.5 其他:join/concat/sort/reverse/every/some…

Array内置了很多方法,下面在给出几个使用较多的方法的定义及使用示例。

定义:

str = arr.join(separator)

arr.concat(value1, value2, …, valueN)

arr.sort([compareFunction])

arr.reverse()

arr.every(callback[, thisArg])

arr.some(callback[, thisArg])

使用示例:

var a = new Array("Wind","Rain","Fire");
var myVar1 = a.join();      // assigns "Wind,Rain,Fire" to myVar1
var myVar2 = a.join(", ");  // assigns "Wind, Rain, Fire" to myVar2
var myVar3 = a.join(" + "); // assigns "Wind + Rain + Fire" to myVar3


var alpha = ['a', 'b', 'c'];
var alphaNumeric = alpha.concat(1, [2, 3]);   //["a", "b", "c", 1, 2, 3] 
alphaNumeric = alpha.concat(1,[2,3],[[4]]);   //["a", "b", "c", 1, 2, 3, [4]]
 

var scores = [1, 2, 10, 21]; 
scores.sort(); // [1, 10, 2, 21]
scores.sort(function(a,b){return a-b}); //[1, 2, 10, 21]


var myArray = ["one", "two", "three"];
myArray.reverse();    // ["three", "two", "one"]



function isBigEnough(element, index, array) {
  return (element >= 10);
}
var passed = [12, 5, 8, 130, 44].every(isBigEnough);   //false
passed = [12, 54, 18, 130, 44].every(isBigEnough);     //true


passed = [2, 5, 8, 1, 4].some(isBigEnough);  //false
passed = [12, 5, 8, 1, 4].some(isBigEnough);  //true

注意concat是浅层复制,从上面例子中也可以看出来。,如果想实现深层复制,可以参考underscore.js框架中的_.flatten,或者参考prototype.js框架中的Array#flatten()

四、结束

非常高兴,你能耐心看完这篇文章,希望能给你带来帮助!欢迎讨论!