您的位置:首页 > Web前端 > JavaScript

JavaScript基础系列之二 函数

2017-08-23 18:11 537 查看

JavaScript基础系列之二 函数


函数

我们知道圆的面积计算公式为:

S = πr2

当我们知道半径
r
的值时,就可以根据公式计算出面积。假设我们需要计算3个不同大小的圆的面积:
var r1 = 12.34;
var r2 = 9.08;
var r3 = 73.1;
var s1 = 3.14 * r1 * r1;
var s2 = 3.14 * r2 * r2;
var s3 = 3.14 * r3 * r3;


当代码出现有规律的重复的时候,你就需要当心了,每次写
3.14 * x * x
不仅很麻烦,而且,如果要把
3.14
改成
3.14159265359
的时候,得全部替换。

有了函数,我们就不再每次写
s = 3.14 * x * x
,而是写成更有意义的函数调用
s
= area_of_circle(x)
,而函数
area_of_circle
本身只需要写一次,就可以多次调用。

基本上所有的高级语言都支持函数,JavaScript也不例外。JavaScript的函数不但是“头等公民”,而且可以像变量一样使用,具有非常强大的抽象能力。


抽象

抽象是数学中非常常见的概念。举个例子:

计算数列的和,比如:
1 + 2 + 3 + ... + 100
,写起来十分不方便,于是数学家发明了求和符号∑,可以把
1
+ 2 + 3 + ... + 100
记作:

100

∑n

n=1

这种抽象记法非常强大,因为我们看到 ∑ 就可以理解成求和,而不是还原成低级的加法运算。

而且,这种抽象记法是可扩展的,比如:

100

∑(n2+1)

n=1

还原成加法运算就变成了:

(1 x 1 + 1) + (2 x 2 + 1) + (3 x 3 + 1) + ... + (100 x 100 + 1)

可见,借助抽象,我们才能不关心底层的具体计算过程,而直接在更高的层次上思考问题。

写计算机程序也是一样,函数就是最基本的一种代码抽象的方式。


函数定义和调用


定义函数

在JavaScript中,定义函数的方式如下:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}


上述
abs()
函数的定义如下:

function
指出这是一个函数定义;
abs
是函数的名称;
(x)
括号内列出函数的参数,多个参数以
,
分隔;
{ ... }
之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。

请注意,函数体内部的语句在执行时,一旦执行到
return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

如果没有
return
语句,函数执行完毕后也会返回结果,只是结果为
undefined


由于JavaScript的函数也是一个对象,上述定义的
abs()
函数实际上是一个函数对象,而函数名
abs
可以视为指向该函数的变量。

因此,第二种定义函数的方式如下:
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};


在这种方式下,
function (x) { ... }
是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量
abs
,所以,通过变量
abs
就可以调用该函数。

上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个
;
,表示赋值语句结束。


调用函数

调用函数时,按顺序传入参数即可:
abs(10); // 返回10
abs(-9); // 返回9


由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9


传入的参数比定义的少也没有问题:
abs(); // 返回NaN


此时
abs(x)
函数的参数
x
将收到
undefined
,计算结果为
NaN


要避免收到
undefined
,可以对参数进行检查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
if (x >= 0) {
return x;
} else {
return -x;
}
}


arguments

JavaScript还有一个免费赠送的关键字
arguments
,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。
arguments
类似
Array
但它不是一个
Array

function foo(x) {
alert(x); // 10
for (var i=0; i<arguments.length; i++) {
alert(arguments[i]); // 10, 20, 30
}
}
foo(10, 20, 30);


利用
arguments
,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:
function abs() {
if (arguments.length === 0) {
return 0;
}
var x = arguments[0];
return x >= 0 ? x : -x;
}

abs(); // 0
abs(10); // 10
abs(-9); // 9


实际上
arguments
最常用于判断传入参数的个数。你可能会看到这样的写法:
// foo(a[, b], c)
// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
function foo(a, b, c) {
if (arguments.length === 2) {
// 实际拿到的参数是a和b,c为undefined
c = b; // 把b赋给c
b = null; // b变为默认值
}
// ...
}


要把中间的参数
b
变为“可选”参数,就只能通过
arguments
判断,然后重新调整参数并赋值。


rest参数

由于JavaScript函数允许接收任意个参数,于是我们就不得不用
arguments
来获取所有参数:
function foo(a, b) {
var i, rest = [];
if (arguments.length > 2) {
for (i = 2; i<arguments.length; i++) {
rest.push(arguments[i]);
}
}
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}


为了获取除了已定义参数
a
b
之外的参数,我们不得不用
arguments
,并且循环要从索引
2
开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的
rest
参数,有没有更好的方法?

ES6标准引入了rest参数,上面的函数可以改写为:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []


rest参数只能写在最后,前面用
...
标识,从运行结果可知,传入的参数先绑定
a
b
,多余的参数以数组形式交给变量
rest
,所以,不再需要
arguments
我们就获取了全部参数。

如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是
undefined
)。

因为rest参数是ES6新标准,所以你需要测试一下浏览器是否支持。请用rest参数编写一个
sum()
函数,接收任意个参数并返回它们的和:

'use strict';

// 测试:
var i, args = [];
for (i=1; i<=100; i++) {
args.push(i);
}
if (sum() !== 0) {
alert('测试失败: sum() = ' + sum());
} else if (sum(1) !== 1) {
alert('测试失败: sum(1) = ' + sum(1));
} else if (sum(2, 3) !== 5) {
alert('测试失败: sum(2, 3) = ' + sum(2, 3));
} else if (sum.apply(null, args) !== 5050) {
alert('测试失败: sum(1, 2, 3, ..., 100) = ' + sum.apply(null, args));
} else {
alert('测试通过!');
}

小心你的return语句

前面我们讲到了JavaScript引擎有一个在行末自动添加分号的机制,这可能让你栽到return语句的一个大坑:
function foo() {
return { name: 'foo' };
}

foo(); // { name: 'foo' }
如果把return语句拆成两行:
function foo() {
return
{ name: 'foo' };
}

foo(); // undefined
要小心了,由于JavaScript引擎在行末自动添加分号的机制,上面的代码实际上变成了:
function foo() {
return; // 自动添加了分号,相当于return undefined;
{ name: 'foo' }; // 这行语句已经没法执行到了
}
所以正确的多行写法是:
function foo() {
return { // 这里不会自动加分号,因为{表示语句尚未结束
name: 'foo'
};
}

变量作用域

在JavaScript中,用
var
申明的变量实际上是有作用域的。如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:
'use strict';function foo() {
var x = 1;
x = x + 1;
}

x = x + 2; // ReferenceError! 无法在函数体外引用变量x
如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:
'use strict';function foo() {
var x = 1;
x = x + 1;
}

function bar() {
var x = 'A';
x = x + 'B';
}
由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行:
'use strict';function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以访问foo的变量x!
}
var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}
如果内部函数和外部函数的变量名重名怎么办?
'use strict';function foo() {
var x = 1;
function bar() {
var x = 'A';
alert('x in bar() = ' + x); // 'A'
}
alert('x in foo() = ' + x); // 1
bar();
}
这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。

变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
'use strict';function foo() {
var x = 'Hello, ' + y;
alert(x);
var y = 'Bob';
}

foo();
虽然是strict模式,但语句
var x = 'Hello, ' + y;
并不报错,原因是变量
y
在稍后申明了。但是
alert
显示
Hello, undefined
,说明变量
y
的值为
undefined
。这正是因为JavaScript引擎自动提升了变量
y
的声明,但不会提升变量
y
的赋值。对于上述
foo()
函数,JavaScript引擎看到的代码相当于:
function foo() {
var y; // 提升变量y的申明
var x = 'Hello, ' + y;
alert(x);
y = 'Bob';
}
由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个
var
申明函数内部用到的所有变量:
function foo() {
var
x = 1, // x初始化为1
y = x + 1, // y初始化为2
z, i; // z和i为undefined
// 其他语句:
for (i=0; i<100; i++) {
...
}
}

全局作用域

不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象
window
,全局作用域的变量实际上被绑定到
window
的一个属性:
'use strict';var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
因此,直接访问全局变量
course
和访问
window.course
是完全一样的。你可能猜到了,由于函数定义有两种方式,以变量方式
var foo = function () {}
定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到
window
对象:
'use strict';function foo() {
alert('foo');
}

foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用
进一步大胆地猜测,我们每次直接调用的
alert()
函数其实也是
window
的一个变量:
'use strict';window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {}

// 恢复alert:
window.alert = old_alert;
alert('又可以用alert()了!');


这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。


名字空间

全局变量会绑定到
window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP:
var MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
return 'foo';
};


把自己的代码全部放入唯一的名字空间
MYAPP
中,会大大减少全局变量冲突的可能。

许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。


局部作用域

由于JavaScript的变量作用域实际上是函数内部,我们在
for
循环等语句块中是无法定义具有局部作用域的变量的:
'use strict';function foo() {
for (var i=0; i<100; i++) {
//
}
i += 100; // 仍然可以引用变量i
}


为了解决块级作用域,ES6引入了新的关键字
let
,用
let
替代
var
可以申明一个块级作用域的变量:
'use strict';function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
i += 1; // SyntaxError
}


常量

由于
var
let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:
var PI = 3.14;


ES6标准引入了新的关键字
const
来定义常量,
const
let
都具有块级作用域:
'use strict';const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14




方法

在一个对象中绑定函数,称为这个对象的方法。

在JavaScript中,对象的定义是这样的:
var xiaoming = {
name: '小明',
birth: 1990
};


但是,如果我们给
xiaoming
绑定一个函数,就可以做更多的事情。比如,写个
age()
方法,返回
xiaoming
的年龄:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};

xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了


绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个
this
关键字,这个东东是什么?

在一个方法内部,
this
是一个特殊变量,它始终指向当前对象,也就是
xiaoming
这个变量。所以,
this.birth
可以拿到
xiaoming
birth
属性。

让我们拆开写:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}

var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};

xiaoming.age(); // 25, 正常结果
getAge(); // NaN


单独调用函数
getAge()
怎么返回了
NaN
?请注意,我们已经进入到了JavaScript的一个大坑里。

JavaScript的函数内部如果调用了
this
,那么这个
this
到底指向谁?

答案是,视情况而定!

如果以对象的方法形式调用,比如
xiaoming.age()
,该函数的
this
指向被调用的对象,也就是
xiaoming
,这是符合我们预期的。

如果单独调用函数,比如
getAge()
,此时,该函数的
this
指向全局对象,也就是
window


坑爹啊!

更坑爹的是,如果这么写:
var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN


也是不行的!要保证
this
指向正确,必须用
obj.xxx()
的形式调用!

由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的
this
指向
undefined
,因此,在strict模式下,你会得到一个错误:
'use strict';var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};

var fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined


这个决定只是让错误及时暴露出来,并没有解决
this
应该指向的正确位置。

有些时候,喜欢重构的你把方法重构了一下:
'use strict';var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};

xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined


结果又报错了!原因是
this
指针只在
age
方法的函数内指向
xiaoming
,在函数内部定义的函数,
this
又指向
undefined
了!(在非strict模式下,它重新指向全局对象
window
!)

修复的办法也不是没有,我们用一个
that
变量首先捕获
this

'use strict';var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};

xiaoming.age(); // 25


var that = this;
,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。


apply

虽然在一个独立的函数调用中,根据是否是strict模式,
this
指向
undefined
window
,不过,我们还是可以控制
this
的指向的!

要指定函数的
this
指向哪个对象,可以用函数本身的
apply
方法,它接收两个参数,第一个参数就是需要绑定的
this
变量,第二个参数是
Array
,表示函数本身的参数。

apply
修复
getAge()
调用:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}

var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};

xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空


另一个与
apply()
类似的方法是
call()
,唯一区别是:

apply()
把参数打包成
Array
再传入;

call()
把参数按顺序传入。

比如调用
Math.max(3, 5, 4)
,分别用
apply()
call()
实现如下:
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5


对普通函数调用,我们通常把
this
绑定为
null


装饰器

利用
apply()
,我们还可以动态改变函数的行为。

JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。

现在假定我们想统计一下代码一共调用了多少次
parseInt()
,可以把所有的调用都找出来,然后手动加上
count
+= 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的
parseInt()

var count = 0;
var oldParseInt = parseInt; // 保存原函数

window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};

// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3




高阶函数

高阶函数英文叫Higher-order function。那么什么是高阶函数?

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

一个最简单的高阶函数:
function add(x, y, f) {
return f(x) + f(y);
}


当我们调用
add(-5, 6, Math.abs)
时,参数
x
y
f
分别接收
-5
6
和函数
Math.abs
,根据函数定义,我们可以推导计算过程为:
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;


用代码验证一下:
add(-5, 6, Math.abs); // 11


编写高阶函数,就是让函数的参数能够接收别的函数


map/reduce

如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data
Processing on Large Clusters”,你就能大概明白map/reduce的概念。


map

举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个数组
[1,
2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用
map
实现如下:



由于
map()
方法定义在JavaScript的
Array
中,我们调用
Array
map()
方法,传入我们自己的函数,就得到了一个新的
Array
作为结果:
function pow(x) {
return x * x;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]


map()
传入的参数是
pow
,即函数对象本身。

你可能会想,不需要
map()
,写一个循环,也可以计算出结果:
var f = function (x) {
return x * x;
};

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = [];
for (var i=0; i<arr.length; i++) {
result.push(f(arr[i]));
}


的确可以,但是,从上面的循环代码,我们无法一眼看明白“把f(x)作用在Array的每一个元素并把结果生成一个新的Array”。

所以,
map()
作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把
Array
的所有数字转为字符串:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']


只需要一行代码。


reduce

再看reduce的用法。Array的
reduce()
把一个函数作用在这个
Array
[x1,
x2, x3...]
上,这个函数必须接收两个参数,
reduce()
把结果继续和序列的下一个元素做累积计算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)


比方说对一个
Array
求和,就可以用
reduce
实现:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25


要把
[1, 3, 5, 7, 9]
变换成整数13579,
reduce()
也能派上用场:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x * 10 + y;
}); // 13579


如果我们继续改进这个例子,想办法把一个字符串
13579
先变成
Array
——
[1,
3, 5, 7, 9]
,再利用
reduce()
就可以写出一个把字符串转换为Number的函数。


filter

filter也是一个常用的操作,它用于把
Array
的某些元素过滤掉,然后返回剩下的元素。

map()
类似,
Array
filter()
也接收一个函数。和
map()
不同的是,
filter()
把传入的函数依次作用于每个元素,然后根据返回值是
true
还是
false
决定保留还是丢弃该元素。

例如,在一个
Array
中,删掉偶数,只保留奇数,可以这么写:
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]


把一个
Array
中的空字符串删掉,可以这么写:
var arr = ['A', '', 'B', null, undefined, 'C', '  '];
var r = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
});
r; // ['A', 'B', 'C']


可见用
filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。


回调函数

filter()
接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示
Array
的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身:
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
return true;
});


利用
filter
,可以巧妙地去除
Array
的重复元素:

'use strict';var
r,
arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];

alert(r.toString());


去除重复元素依靠的是
indexOf
总是返回第一个元素的位置,后续的重复元素位置与
indexOf
返回的位置不相等,因此被
filter
滤掉了。


sort


排序算法

排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个对象呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。通常规定,对于两个元素
x
y
,如果认为
x
< y
,则返回
-1
,如果认为
x
== y
,则返回
0
,如果认为
x
> y
,则返回
1
,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。

JavaScript的
Array
sort()
方法就是用于排序的,但是排序结果可能让你大吃一惊:
// 看上去正常的结果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];

// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]


第二个排序把
apple
排在了最后,是因为字符串根据ASCII码进行排序,而小写字母
a
的ASCII码在大写字母之后。

第三个排序结果是什么鬼?简单的数字排序都能错?

这是因为
Array
sort()
方法默认把所有元素先转换为String再排序,结果
'10'
排在了
'2'
的前面,因为字符
'1'
比字符
'2'
的ASCII码小。



如果不知道
sort()
方法的默认排序规则,直接对数字排序,绝对栽进坑里!

幸运的是,
sort()
方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。

要按数字大小排序,我们可以这么写:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
}); // [1, 2, 10, 20]


如果要倒序排序,我们可以把大的数放前面:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
}); // [20, 10, 2, 1]


默认情况下,对字符串排序,是按照ASCII的大小比较的,现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能定义出忽略大小写的比较算法就可以:
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']


忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。

从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

最后友情提示,
sort()
方法会直接对
Array
进行修改,它返回的结果仍是当前
Array

var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一对象




闭包


函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

我们来实现一个对
Array
的求和。通常情况下,求和的函数是这样定义的:
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}

sum([1, 2, 3, 4, 5]); // 15


但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数!
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}


当我们调用
lazy_sum()
时,返回的并不是求和结果,而是求和函数:
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()


调用函数
f
时,才真正计算求和的结果:
f(); // 15


在这个例子中,我们在函数
lazy_sum
中又定义了函数
sum
,并且,内部函数
sum
可以引用外部函数
lazy_sum
的参数和局部变量,当
lazy_sum
返回函数
sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

请再注意一点,当我们调用
lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false


f1()
f2()
的调用结果互不影响。


闭包

注意到返回的函数在其定义内部引用了局部变量
arr
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了
f()
才执行。我们来看一个例子:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];


在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都添加到一个
Array
中返回了。

你可能认为调用
f1()
f2()
f3()
结果应该是
1
4
9
,但实际结果是:
f1(); // 16
f2(); // 16
f3(); // 16


全部都是
16
!原因就在于返回的函数引用了变量
i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量
i
已经变成了
4
,因此最终结果为
16


返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1(); // 1
f2(); // 4
f3(); // 9


注意这里用了一个“创建一个匿名函数并立刻执行”的语法:
(function (x) {
return x * x;
})(3); // 9


理论上讲,创建一个匿名函数并立刻执行可以这么写:
function (x) { return x * x } (3);


但是由于JavaScript语法解析的问题,会报SyntaxError错误,因此需要用括号把整个函数定义括起来:
(function (x) { return x * x }) (3);


通常,一个立即执行的匿名函数可以把函数体拆开,一般这么写:
(function (x) {
return x * x;
})(3);


说了这么多,难道闭包就是为了返回一个函数然后延迟执行吗?

当然不是!闭包有非常强大的功能。举个栗子:

在面向对象的程序设计语言里,比如Java和C++,要在对象内部封装一个私有变量,可以用
private
修饰一个成员变量。

在没有
class
机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。我们用JavaScript创建一个计数器:
'use strict';function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}


它用起来像这样:
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3

var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13


在返回的对象中,实现了一个闭包,该闭包携带了局部变量
x
,并且,从外部代码根本无法访问到变量
x
。换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。

闭包还可以把多参数的函数变成单参数的函数。例如,要计算xy可以用
Math.pow(x,
y)
函数,不过考虑到经常计算x2或x3,我们可以利用闭包创建新的函数
pow2
pow3

function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}

// 创建两个新函数:
var pow2 = make_pow(2);
var pow3 = make_pow(3);

pow2(5); // 25
pow3(7); // 343


脑洞大开

很久很久以前,有个叫阿隆佐·邱奇的帅哥,发现只需要用函数,就可以用计算机实现运算,而不需要
0
1
2
3
这些数字和
+
-
*
/
这些符号。

JavaScript支持函数,所以可以用JavaScript用函数来写这些计算。来试试:

'use strict';// 定义数字0:
var zero = function (f) {
return function (x) {
return x;
}
};

// 定义数字1:
var one = function (f) {
return function (x) {
return f(x);
}
};

// 定义加法:
function add(n, m) {
return function (f) {
return function (x) {
return m(f)(n(f)(x));
}
}
}



箭头函数

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。

为什么叫Arrow Function?因为它的定义用的就是一个箭头:
x => x * x


上面的箭头函数相当于:
function (x) {
return x * x;
}


在继续学习箭头函数之前,请测试你的浏览器是否支持ES6的Arrow Function:

'use strict';

alert('你的浏览器支持ES6的Arrow Function!');
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连
{ ... }
return
都省略掉了。还有一种可以包含多条语句,这时候就不能省略
{ ... }
return
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}
如果参数不是一个,就需要用括号
()
括起来:
// 两个参数:
(x, y) => x * x + y * y

// 无参数:
() => 3.14

// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
// SyntaxError:
x => { foo: x }
因为和函数体的
{ ... }
有语法冲突,所以要改为:
// ok:
x => ({ foo: x })

this

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的
this
是词法作用域,由上下文确定。回顾前面的例子,由于JavaScript函数对
this
绑定的错误处理,下面的例子无法得到预期结果:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
现在,箭头函数完全修复了
this
的指向,
this
总是指向词法作用域,也就是外层调用者
obj
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25
如果使用箭头函数,以前的那种hack写法:
var that = this;
就不再需要了。由于
this
在箭头函数中已经按照词法作用域绑定了,所以,用
call()
或者
apply()
调用箭头函数时,无法对
this
进行绑定,即传入的第一个参数被忽略:
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
obj.getAge(2015); // 25

generator

generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。ES6定义generator标准的哥们借鉴了Python的generator的概念和语法,如果你对Python的generator很熟悉,那么ES6的generator就是小菜一碟了。如果你对Python还不熟,赶快恶补Python教程!。我们先复习函数的概念。一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果:
function foo(x) {
return x + x;
}

var r = foo(1); // 调用foo函数
函数在执行过程中,如果没有遇到
return
语句(函数末尾如果没有
return
,就是隐含的
return undefined;
),控制权无法交回被调用的代码。generator跟函数很像,定义如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
generator和函数不同的是,generator由
function*
定义(注意多出的
*
号),并且,除了
return
语句,还可以用
yield
返回多次。大多数同学立刻就晕了,generator就是能够返回多次的“函数”?返回多次有啥用?还是举个栗子吧。我们以一个著名的斐波那契数列为例,它由
0
1
开头:
0 1 1 2 3 5 8 13 21 34 ...
要编写一个产生斐波那契数列的函数,可以这么写:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
t = a + b;
a = b;
b = t;
arr.push(t);
}
return arr;
}

// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函数只能返回一次,所以必须返回一个
Array
。但是,如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 1;
while (n < max) {
yield a;
t = a + b;
a = b;
b = t;
n ++;
}
return a;
}
直接调用试试:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接调用一个generator和调用函数不一样,
fib(5)
仅仅是创建了一个generator对象,还没有去执行它。调用generator对象有两个方法,一是不断地调用generator对象的
next()
方法:
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: true}
next()
方法会执行generator的代码,然后,每次遇到
yield x;
就返回一个对象
{value: x, done: true/false}
,然后“暂停”。返回的
value
就是
yield
的返回值,
done
表示这个generator是否已经执行结束了。如果
done
true
,则
value
就是
return
的返回值。当执行到
done
true
时,这个generator对象就已经全部执行完毕,不要再继续调用
next()
了。第二个方法是直接用
for ... of
循环迭代generator对象,这种方式不需要我们自己判断
done
for (var x of fib(5)) {
console.log(x); // 依次输出0, 1, 1, 2, 3
}
generator和普通函数相比,有什么用?因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。例如,用一个对象来保存状态,得这么写:
var fib = {
a: 0,
b: 1,
n: 0,
max: 5,
next: function () {
var
r = this.a,
t = this.a + this.b;
this.a = this.b;
this.b = t;
if (this.n < this.max) {
this.n ++;
return r;
} else {
return undefined;
}
}
};
用对象的属性来保存状态,相当繁琐。generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个好处要等到后面学了AJAX以后才能体会到。没有generator之前的黑暗时代,用AJAX时需要这么写代码:
ajax('http://url-1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
回调越多,代码越难看。有了generator的美好时代,用AJAX时可以这么写:
try {
r1 = yield ajax('http://url-1', data1);
r2 = yield ajax('http://url-2', data2);
r3 = yield ajax('http://url-3', data3);
success(r3);
}
catch (err) {
handle(err);
}
看上去是同步的代码,实际执行是异步的。

练习

要生成一个自增的ID,可以编写一个
next_id()
函数:
var current_id = 0;

function next_id() {
current_id ++;
return current_id;
}
由于函数无法保存状态,故需要一个全局变量
current_id
来保存数字。不用闭包,试用generator改写:
'use strict';function* next_id() {

}

// 测试:
var
x,
pass = true,
g = next_id();
for (x = 1; x < 100; x ++) {
if (g.next().value !== x) {
pass = false;
alert('测试失败!');
break;
}
}
if (pass) {
alert('测试通过!');
}


转自:https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/00143450083887673122b45a4414333ac366c3c935125e7000

--- end ---
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: