唐抉的个人博客

前端三件套之JavaScript(二)

字数统计: 7.2k阅读时长: 33 min
2022/10/31

函数

函数定义和调用

定义函数

在JavaScript中,定义函数的方式如下:

1
2
3
4
5
6
7
function abs(x){
if(x>=0){
return x;
}else{
return -x;
}
}

函数内的语句执行到return语句时,函数便执行完毕了。若没有return语句,函数执行完毕后会返回undefined。

由于函数也是一个对象,因此上述定义的abs()函数实际上是一个函数对象,因此,第二种定义函数的方式如下:

1
2
3
4
5
6
7
var abs=function(x){
if(x>=0){
return x;
}else{
return -x;
}
};

这两种定义完全等价,注意第二种方式函数体末尾要加上一个分号;,表示赋值语句结束。

调用函数

调用函数时,需要按顺序传入参数:

1
2
abs(18);
abs(-231);

由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也不会报错:

1
2
abs(12,'sdada');//返回12
abs(-123,'asd','asd',null);//返回123

传入的参数比定义的少也不会报错:

1
abs();//abs()收到参数undefined,返回NaN

若要避免收到undefined,可以对参数进行检查:

1
2
3
4
5
6
7
8
9
10
function abs(x){
if(typeof x!=='number'){
throw 'Not a number';
}
if(x>=0){
return x;
}else{
return -x;
}
}

arguments

arguments只在函数内部起作用,并且永远指向当前函数的调用者所传入的所有参数。arguments类似于Array但不是一个Array。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(x){
console.log('x='+x);
for(var i=0;i<arguments.length;i++){
console.log('arg'+i+'='+arguments[i]);
}
}
foo(10,20,30);
/*运行结果如下:
x=10
arg0=10
arg1=20
arg2=30
*/

利用arguments可以获得调用者传入的所有参数,即使函数不定义任何参数值,可还是能拿到参数的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function abs(){
if(arguments.length===0){
return 0;
}
var x=arguments[0];
return x>=0?x:-x;
}
console.log(abs());
console.log(abs(12));
console.log(abs(-12));
/*运行结果如下:
0
12
12
*/

arguments常用于判断传入参数的个数:

1
2
3
4
5
6
7
//foo(a,[,b],c)接收2~3个参数,b是可选参数。若只传入2个参数,b默认为bull
function foo(a,b,c){
if(arguments.length===2){
c=b;
b=null;
}
}

rest参数

ES6标准引入了rest参数以帮助获得除了已定义参数之外的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(a,b,...rest){
console.log('a='+a);
console.log('b='+b);
console.log(rest);
}
foo(1,2,3,4,5);
foo(1);
/*运行结果如下:
a=1
b=2
(3) [3, 4, 5]
a=1
b=undefined
(0) []
*/

rest参数只能写在最后,前面用...标识。若传入的参数少于定义参数,rest参数会接收一个空数组。

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

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function sum(...rest){
var sum=0;
if(rest.length===0){
return 0;
}
for (var i=0;i<rest.length;i++){
sum=sum+rest[i];
}
return sum;
}
// 测试:
var i, args = [];
for (i=1; i<=100; i++) {
args.push(i);
}
if (sum() !== 0) {
console.log('测试失败: sum() = ' + sum());
} else if (sum(1) !== 1) {
console.log('测试失败: sum(1) = ' + sum(1));
} else if (sum(2, 3) !== 5) {
console.log('测试失败: sum(2, 3) = ' + sum(2, 3));
} else if (sum.apply(null, args) !== 5050) {
console.log('测试失败: sum(1, 2, 3, ..., 100) = ' + sum.apply(null, args));
} else {
console.log('测试通过!');
}
/*运行结果如下:
测试通过!
*/

JavaScript引擎有一个再行末自动添加分号的机制,因此要注意return语句不要轻易换行。若要换行写,可以写成以下形式:

1
2
3
4
5
function foo(){
return{
name:'foo'
};
}

练习题

定义一个计算圆面积的函数area_of_circle(),它有两个参数:

  • r: 表示圆的半径;
  • pi: 表示π的值,如果不传,则默认3.14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'use strict';

function area_of_circle(r, pi) {
if (arguments.length===1){
pi=3.14;
}
return pi*r*r;
}
// 测试:
if (area_of_circle(2) === 12.56 && area_of_circle(2, 3.1416) === 12.5664) {
console.log('测试通过');
} else {
console.log('测试失败');
}
/*
测试通过
*/

小明是一个JavaScript新手,他写了一个max()函数,返回两个数中较大的那个。但是小明抱怨他的浏览器出问题了,无论传入什么数,max()函数总是返回undefined。请帮他指出问题并修复:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

function max(a, b) {
if (a > b) {
return
a;
} else {
return
b;
}
}
console.log(max(15, 20));

修复后的代码如下:

1
2
3
4
5
6
7
8
9
10
'use strict';

function max(a, b) {
if (a > b) {
return a;
} else {
return b;
}
}
console.log(max(15, 20));

变量作用域与解构赋值

若一个变量再函数体内部声明,则该变量的作用域为整个函数体,在函数体外不可引用该变量。

若两个不同的函数各自声明了同一个变量,则该变量只在各自的函数体内其作用。

由于JavaScript的函数可以嵌套,此时内部函数可以访问外部函数定义的变量,而外部函数无法访问内部函数定义的变量。

若内部函数和外部函数的变量名重名时,内部函数的变量将忽略外部函数的变量。

变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有声明的变量“提升”到函数顶部。只提升变量的声明,不会提升变量的赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(){
var x='Hello,'+y;
console.log(x);
var y='Bob';
}
foo();
//等同于
function foo(){
var y;
var x='Hello,'+y;
console.log(x);
var y='Bob';
}
/*运行结果如下:
Hello,undefined
*/

因此,为了防止不必要的错误,在函数内部定义变量时,首先声明所有变量。

全局作用域

不在任何函数内的变量便处于全局作用域中。JavaScript默认有一个全局对象window,全局作用域的百年来实际上被绑定到window属性中。

1
2
3
var a='asd';
alert(a);//弹出asd弹窗
alert(window.a);//弹出asd弹窗

由于函数定义有两种方式,以变量形式定义的函数var foo=function(){}也是一个全局变量。此时函数的定义也被视为一个全局变量,并绑定到window对象中。alert函数也是window的一个变量。

名字空间

全局变量会绑定到window上,不同的JavaScript文件若使用了相同的全局变量,或定义了相同名字的顶层函数,就会造成命名冲突,且这种错误很难被发现。

减少冲突的方法就是把自己的所有变量和函数全部绑定到一个全局变量中,即把代码全部放入唯一的名字空间中MYAPP

1
2
3
4
5
6
7
8
9
//唯一的全局变量
var MYAPP={};
//其他变量
MYAPP.name='myapp';
MYAPP.version=1.0;
//其他函数
MYAPP.foo=function(){
return 'foo';
}

局部作用域

由于JavaScript的变量作用域实际上是函数内部,故for循环等语句块是无法定义具有局部作用域的变量的。

1
2
3
4
5
6
function foo(){
for(var i=0;i<100;i++){
//
}
i+=100//仍然可以引用变量i
}

ES6引入了let关键字,用let替代var可以声明一个块级作用域的变量:

1
2
3
4
5
6
7
function foo(){
var sum=0;
for(let i=0;i<100;i++){
sum=sum+i;
}
i+=100//报错ReferenceError: i is not defined
}

常量

由于varlet声明的是变量,在ES6后,若要声明一个常量,可以通过关键字const来定义,常量名要全部大写。constlet都具有块级作用域:

1
2
3
const PI=3.14;
PI=3;//报错TypeError: Assignment to constant variable.,某些浏览器不报错,但是无效果
console.log(PI);

解构赋值

在ES6中,可以使用解构赋值来直接对多个变量同时赋值:

1
2
3
4
5
var[x,y,z]=['hello','JavaScript','ES6'];
console.log('x='+x+', y='+y+', z= '+z);
/*运行结果如下:
x=hello, y=JavaScript, z= ES6
*/

对数组元素进行结构赋值时,多个变量要用[]括起来。

若数组本身还有嵌套,也可以通过下面的形式进行解构赋值,嵌套层次和位置要保持一致:

1
2
3
4
5
let [x,[y,z]]=['hello',['JavaScript','ES6']];
console.log('x='+x+', y='+y+', z= '+z);
/*运行结果如下:
x=hello, y=JavaScript, z= ES6
*/

解构赋值还可以忽略某些元素:

1
2
3
4
5
let [,,z]=['hello','JavaScript','ES6'];
console.log('z='+z);
/*运行结果如下:
z=ES6
*/

若要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:

1
2
3
4
5
6
7
8
9
10
11
12
var person={
name:'zhangsan',
age:13,
gender:'male',
passport:'G-12345678',
school:'No.1 middle school'
};
var{name,age,passport}=person;
console.log('name='+name+', age='+age+', passport'+passport);
/*运行结果如下:
name=zhangsan, age=13, passportG-12345678
*/

对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,主要要保证对应的层次一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var person={
name:'zhangsan',
age:13,
gender:'male',
passport:'G-12345678',
school:'No.1 middle school',
address:{
city:'Beijing',
street:'No.1 Road',
zipcode:'100001'
}
};
var{name,address:{city,zip,zipcode}}=person;//person中没有zip属性,故其值为undefined
console.log('name='+name+', city='+city+', zip='+zip+', zipcode='+zipcode);
/*运行结果如下:
name=zhangsan, city=Beijing, zip=undefined, zipcode=100001
*/

使用解构赋值对对象属性进行赋值时,若对应的属性不存在,变量将被赋值为undefined,若要使用的变量名和属性名不一致,可以用下面的语法获取:

1
2
3
4
5
6
7
8
9
10
11
12
var person={
name:'zhangsan',
age:13,
gender:'male',
passport:'G-12345678',
school:'No.1 middle school',
};
let{name,passport:id}=person;
console.log('name='+name+', id='+id);
/*运行结果如下:
name=zhangsan, id=G-12345678
*/

解构赋值还可以使用默认值,这样避免了不存在的属性返回undefined值:

1
2
3
4
5
6
7
8
9
10
11
12
var person={
name:'zhangsan',
age:13,
gender:'male',
passport:'G-12345678',
school:'No.1 middle school',
};
var{name,single=true}=person;//若person对象没有single属性,则默认返回true
console.log('name='+name+', single='+single);
/*运行结果如下:
name=zhangsan, single=true
*/

若变量已经被声明了,再次赋值时,正确的写法也会报语法错误:

1
2
3
4
var x,y;
{x,y}={name:'zhangsan',x:10,y:200};//报错SyntaxError: Unexpected token '='
// ({x,y}={name:'zhangsan',x:10,y:200});//返回x=10, y=200
console.log('x='+x+', y='+y);

使用场景

解构赋值在很多时候可以简化代码,如:无需临时变量交换两个变量的值:

1
2
var x=1,y=2;
[x,y]=[y,x];

快速获取当前页面的域名和路径:

1
var{hostname:domain,pathname:path}=location;

若一个函数接收一个对象作为参数,那么使用解构直接把对象的属性绑定到变量中,如快速创建一个Date对象:

1
2
3
function buildDate({year,month,dat,hour=0,minute=0,second=0}){
return new Date(year+'-'+month+'-'+day+' '+hour+':'+minute+':'+second);
}

目前支持解构赋值的浏览器包括Chrome、Firefox、Edge等。

方法

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

1
2
3
4
5
6
7
8
9
10
11
var person={
name:'zhangsan',
birth:1999,
age:function(){
var y=new Date().getFullYear();
return y-this.birth;
}
};
console.log(person.age());
/*运行结果如下:
23*/

这里的age()函数便是一个方法,在方法内部,this是一个特殊变量,它始终指向当前对象。若是单独调用函数,在函数内调用this,this指向全局变量。要保证this指向正确,必须用obj.xxx()的形式调用。

在strict模式下,函数的this指向undefined,在非strict模式下,函数的this指向全局对象window。

若要将方法重构,为了避免出错可先用that变量捕获this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person={
name:'zhangsan',
birth:1999,
age:function(){
var that=this;
function getAgeFromBirth(){
var y=new Date().getFullYear();
return y-that.birth;
}
return getAgeFromBirth();
}
}
console.log(person.age());
/*运行结果如下:
23*/

apply

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getAge(){
var y=new Date().getFullYear();
return y-this.birth;
}
var person={
name:'zhangsan',
birth:1999,
age:getAge
};

console.log(person.age());
console.log(getAge());//单独调用getAge,this指向window
console.log(getAge.apply(person,[]));//apply将this指向person
/*运行结果如下:
23
NaN
23*/

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

  • apply()把参数打包成Array后再传入
  • call()把参数按顺序传入

如要调用Math.max(3,5,4):

1
2
3
4
5
console.log(Math.max.apply(null,[3,5,4]));
console.log(Math.max.call(null,3,5,4));
/*运行结果如下:
5
5*/

装饰器

利用apply()可以动态改变函数的行为。即使是JavaScript的内置函数,也可以重新指向新的函数。如要统计代码调用了多少次parseInt(),可以将系统默认的parseInt()替换为新的可以统计多少次调用的parseInt()

1
2
3
4
5
6
7
8
9
10
11
12
13
var count=0;
var oldParseInt=parseInt;//保存原函数
window.parseInt=function(){
count+=1;
return oldParseInt.apply(null,arguments);//调用原函数
}
// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count);
/*在浏览器中的运行结果如下:
count = 3*/

高阶函数

接收另一个函数作为参数的函数,称为高阶函数。

map/reduce

map

若要将函数 \[ f(x)=x^2 \] 作用在数组[1,2,3,4,5,6,7,8,9]上,可用map实现:

1
2
3
4
5
6
7
8
function pow(x){
return x*x;
}
var arr=[1,2,3,4,5,6,7,8,9];
var results=arr.map(pow);
console.log(results);
/*运行结果如下:
(9) [1, 4, 9, 16, 25, 36, 49, 64, 81]*/

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

用map,只需两行代码便可以把Array的所有数组转换为字符串:

1
2
3
4
var arr=[1,2,3,4,5,6,7,8,9];
console.log(arr.map(String))
/*运行结果如下:
(9) ['1', '2', '3', '4', '5', '6', '7', '8', '9']*/

reduce

Array的reduce()把函数作用在Array的[x1,x2,x3...]上,reduce()接收两个参数,reduce()把结果继续和序列的下一个元素做累计运算,即[x1,x2,x3].reduce(f)=f(f(x1,x2),x3)

对于一个Array求和,可以用reduce实现:

1
2
3
4
5
6
var arr=[1,3,5,7,9];
console.log(arr.reduce(function(x,y){
return x+y;
}));
/*运行结果如下:
25*/

利用reduce()也可以把[1,3,5,7,9]变换成整数13579:

1
2
3
4
5
6
var arr=[1,3,5,7,9];
console.log(arr.reduce(function(x,y){
return x*10+y;
}));
/*运行结果如下:
13579*/

练习题

利用reduce()求积:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function product(arr) {
return arr.reduce(function(x,y){
return x*y;
});
}
// 测试:
if (product([1, 2, 3, 4]) === 24 && product([0, 1, 2]) === 0 && product([99, 88, 77, 66]) === 44274384) {
console.log('测试通过!');
}
else {
console.log('测试失败!');
}
/*运行结果如下:
测试通过!*/

不要使用JavaScript内置的parseInt()函数,利用map和reduce操作实现一个string2int()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function string2int(s) {
var arr=[];
for (var i of s){
arr.push(i);
}
return arr.map(function(x){
return x*1;
}).reduce(function(x,y){
return x*10+y;
});
}
// 测试:
if (string2int('0') === 0 && string2int('12345') === 12345 && string2int('12300') === 12300) {
if (string2int.toString().indexOf('parseInt') !== -1) {
console.log('请勿使用parseInt()!');
} else if (string2int.toString().indexOf('Number') !== -1) {
console.log('请勿使用Number()!');
} else {
console.log('测试通过!');
}
}
else {
console.log('测试失败!');
}
/*运行结果如下:
测试通过!*/

请把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT'],输出:['Adam', 'Lisa', 'Bart']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function normalize(arr) {
return arr.map(function(x){
return x.slice(0,1).toUpperCase()+x.slice(1).toLowerCase();
})
}
// 测试:
if (normalize(['adam', 'LISA', 'barT']).toString() === ['Adam', 'Lisa', 'Bart'].toString()) {
console.log('测试通过!');
}
else {
console.log(normalize(['adam', 'LISA', 'barT']));
console.log('测试失败!');
}
/*运行结果如下:
测试通过!*/

小明希望利用map()把字符串变成整数,他写的代码很简洁:

1
2
3
4
var arr = ['1', '2', '3'];
var r;
r = arr.map(parseInt);
console.log(r);

结果竟然是1, NaN, NaN,小明百思不得其解,请帮他找到原因并修正代码。

修正代码如下:

1
2
3
4
5
6
7
8
var arr = ['1', '2', '3'];
var r=[];
r = arr.map(function(x){
return parseInt(x);
});
console.log(r);
/*运行结果如下:
(3)[1, 2, 3]*/

filter

filter用于过滤Array中的某些元素,返回剩下的元素,filter也只接收一个函数,filter把传入的函数依次作用于每个元素,然后根据返回值True or False来决定保留还是丢弃该元素。

如在一个Array中,删掉偶数,只保留奇数:

1
2
3
4
5
6
7
var arr=[1,2,4,5,6,9,10,15];
var r=arr.filter(function(x){
return x%2!=0;
});
console.log(r);
/*运行结果如下:
(4) [1, 5, 9, 15]*/

把一个Array中的空字符串删掉:

1
2
3
4
5
6
7
var arr=['A','','B',null,undefined,'C',' '];
var r=arr.filter(function(s){
return s&&s.trim();
});
console.log(r);
/*运行结果如下:
(3) ['A', 'B', 'C']*/

回调函数

filter()接收的回调函数,可以有多个参数。通常仅使用第一个参数来表示Array的某个元素。回调函数还可以接收另外两个参数来表示元素的位置和数组本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);//打印变量arr
return true;
})
/*运行结果如下:
A
0
(3) ['A', 'B', 'C']
B
1
(3) ['A', 'B', 'C']
C
2
(3) ['A', 'B', 'C']*/

利用filter()可以去除Array的重复元素:

1
2
3
4
5
6
7
8
var r;
var arr=['apple','strawberry','banana','pear','apple','orange','orange','strawberry'];
r=arr.filter(function(element,index,self){
return self.indexOf(element)===index;
});
console.log(r.toString());
/*运行结果如下:
apple,strawberry,banana,pear,orange*/

由于indexof总是返回元素第一次出现的位置,因此后续的重复元素位置与indexof不相等就被过滤掉了。

练习题

请尝试用filter()筛选出素数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function get_primes(arr) {
return arr.filter(function(x){
if(x===1){
return false;
}else if(x===2||x===3||x===5||x===7){
return true;
}else if(x%2===0||x%3===0||x%5===0||x%7===0){
return false;
}else{
return true;
}
});
}

// 测试:
var
x,
r,
arr = [];
for (x = 1; x < 100; x++) {
arr.push(x);
}
r = get_primes(arr);
if (r.toString() === [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97].toString()) {
console.log('测试通过!');
} else {
console.log('测试失败: ' + r.toString());
}

/*运行结果如下:
测试通过!*/

sort

JavaScript中的sort()默认把所有元素转换为String再进行排序,用于排序字符串时,根据ASCII码进行排序。因此,直接使用sort()对数字进行排序,得到的结果往往不如人意。

比较两个元素大小时,通常规定,对于x<y,则返回-1;对于x==y,则返回0;对于x>y,则返回1

若要按数字大小进行排序,可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//从小到大排序
var arr=[10,20,1,2];
arr.sort(function(x,y){
if(x<y){
return -1;
}
if(x>y){
return 1;
}
return 0
});
console.log(arr);
/*运行结果如下:
(4) [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
});
console.log(arr);
/*运行结果如下:
(4) [20, 10, 2, 1]*/

忽略字母大小写,按照字母序排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr=['Google','apple','Microsoft'];
console.log(arr.sort(function(s1,s2){
x1=s1.toUpperCase();
x2=s2.toUpperCase();
if(x1<x2){
return -1;
}
if(x1>x2){
return 1;
}
return 0;
}));
/*运行结果如下:
(3) ['apple', 'Google', 'Microsoft']*/

sort()方法会直接对Array进行修改,原Array返回的结果是修改后的Array:

1
2
3
4
5
6
7
8
9
var a1=['A','C','B'];
var a2=a1.sort();
console.log(a1);
console.log(a2);
console.log(a1===a2);
/*运行结果如下:
(3) ['A', 'B', 'C']
(3) ['A', 'B', 'C']
true*/

Array

对于数组,除了map()reduce()filter()sort()这些方法外,Array对象还提供了很多实用的高阶函数。

every

every()方法可以判断数组的所有元素是否满足测试条件:

1
2
3
4
5
6
7
8
9
10
var arr=['Apple','pear','orange'];
console.log(arr.every(function(s){
return s.length>0;
}));
console.log(arr.every(function(s){
return s.toLowerCase()===s;//判断每个元素是否都是小写
}));
/*运行结果如下:
true
false*/

find

find()方法用于查找符合条件的第一个元素,若找到了,返回这个元素,否则返回undefined:

1
2
3
4
5
6
7
8
9
10
var arr=['Apple','pear','orange'];
console.log(arr.find(function(s){
return s.toLowerCase()===s;//返回全是小写的第一个元素
}));
console.log(arr.find(function(s){
return s.toUpperCase()===s;//返回全是小写的第一个元素
}));
/*运行结果如下:
pear
undefined*/

findIndex

findIndex()find()类似,也是查找符合条件的第一个元素,不同之处在于findIndex()会返回这个元素的索引,若没有找到,则返回-1:

1
2
3
4
5
6
7
8
9
10
var arr=['Apple','pear','orange'];
console.log(arr.findIndex(function(s){
return s.toLowerCase()===s;//返回全是小写的第一个元素
}));
console.log(arr.findIndex(function(s){
return s.toUpperCase()===s;//返回全是小写的第一个元素
}));
/*运行结果如下:
1
-1*/

forEach

forEach和map()类似,也把每个元素依次作用于传入的函数,但不会返回新的数组,常用于遍历数组。

1
2
3
4
5
6
var arr=['Apple','pear','orange'];
arr.forEach(console.log);
/*运行结果如下:
Apple 0 (3) ['Apple', 'pear', 'orange']
pear 1 (3) ['Apple', 'pear', 'orange']
orange 2 (3) ['Apple', 'pear', 'orange']*/

闭包

函数作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function lazy_sum(arr){
var sum=function(){
return arr.reduce(function(x,y){
return x+y;
});
}
return sum;
}
var f1=lazy_sum([1,2,3,4,5]);//f=sum(),调用lazy_sum时返回的不是求和结果,而是求和函数
var f2=lazy_sum([1,2,3,4,5]);//每次调用都会返回一个新函数
console.log(f1());
console.log(f1===f2);
/*运行结果如下:
15
false*/

闭包

当一个函数返回一个新函数后,其内部的局部变量还被新函数引用。返回的函数并没有立刻执行,而是知道调用f()后才执行。

与pyhton中的闭包相同,由于返回的函数非立刻执行,而是等到返回函数调用完成时才执行,因此会发生以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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];
console.log(f1(),f2(),f3());
/*运行结果如下:
16 16 16*/

因此返回闭包时,返回函数不要引用任何循环遍历或是会发生变化的量。

若一定要引用循环变量,则要再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环遍历如何更改,已绑定到函数参数的值都是不变的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
console.log(f1(),f2(),f3());
/*运行结果如下:
1 4 9*/

这里用到了一个创建匿名函数并立刻执行的语法:

1
2
3
(function(x){
return x*x;
})(3);

正常创建匿名函数并立刻执行写法如下:

1
function(x){return x*x}(3)

但再JavaScript中这么些会报错,因此需要用括号把函数定义括起来。

JavaScript里没有class机制,只有函数。借助闭包,同样可以封装一个私有变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function create_counter(initial){
var x=initial||0;
return {
inc:function(){
x+=1;
return x;
}
}
}
var c1=create_counter();
console.log(c1.inc());
console.log(c1.inc());
console.log(c1.inc());

var c2=create_counter(10);
console.log(c2.inc());
console.log(c2.inc());
console.log(c2.inc());
/*运行结果如下:
1
2
3
11
12
13*/

在返回的对象中实现了一个闭包,该闭包携带了局部变量x,且外部的代码无法访问到变量x。

闭包还可以把多参数的函数变成单参数的函数。如计算x^y可以用Math.pow(x,y)函数,可以利用闭包创建新的函数pow2和pow3:

1
2
3
4
5
6
7
8
9
10
11
12
function make_pow(n){
return function(x){
return Math.pow(x,n);
}
}
var pow2=make_pow(2);
var pow3=make_pow(3);
console.log(pow2(5));
console.log(pow3(7));
/*运行结果如下:
25
343*/

箭头函数

ES6标准新增了一种新的函数:箭头函数。它的定义用的就是一个箭头:

1
2
3
4
5
x=>x*x
//等价于
funtion(x){
return x*x;
}

箭头函数相当于是匿名函数,并且简化了函数定义。箭头函数有两种格式,一种只包含一个表达式:

1
x=>x*x

还有一种可以包含多条语句:

1
2
3
4
5
6
7
x=>{
if(x>0){
return x*x;
}else{
return -x*x;
}
}

如果参数不是一个,就需要用括号()括起来:

1
2
3
4
5
6
7
8
9
(x,y)=>x*x+y*y//两个参数
()=>3.14//无参数
(x,y,...rest)=>{
var i,sum=x+y;
for(i=0;i<rest.lenth;i++){
sum+=rest[i];
}
return sum;
}

如果要返回一个对象,就要写成这样:

1
x=>({foo:x})

this

箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

箭头函数完全修复了this的指向,this总是指向词法作用域,即外层调用者obj,以前的hack写法var that=this就不再需要了:

1
2
3
4
5
6
7
8
9
10
11
var obj={
birth:1999,
getAge:function(){
var b=this.birth;
var fn=()=>new Date().getFullYear()-this.birth;
return fn()
}
};
console.log(obj.getAge());
/*运行结果如下:
23*/

由于this在箭头函数中已经按照词法作用域绑定了,因此用call()apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:

1
2
3
4
5
6
7
8
9
10
11
var obj={
birth:1999,
getAge:function(year){
var b=this.birth;
var fn=(y)=>y-this.birth;
return fn.call({birth:2000},year)
}
};
console.log(obj.getAge(2015));
/*运行结果如下:
16*/

练习题

请使用箭头函数简化排序时传入的函数:

1
2
3
4
5
6
7
var arr = [10, 20, 1, 2];
arr.sort((x, y) => {
return x-y;
});
console.log(arr); // [1, 2, 10, 20]
/*运行结果如下:
(4) [1, 2, 10, 20]*/

generator

生成器generator是ES6标准引入一个数据类型,其概念和语法与python的generator相似。

generator的定义如下:

1
2
3
4
5
function* foo(x){
yield x+1;
yield x+2;
return x+3;
}

generator和函数不同的是,generator由function*定义,除了用return语句,还可以用yield返回多次。

用generator写一个斐波那契数列如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function* fib(max){
var t,a=0,b=1,n=0;
while (n<max){
yield a;
[a,b]=[b,a+b];
n++;
}
return;
}
var f=fib(5);
console.log(f.next());//通过调用generator对象的next()方法来调用生成器
console.log(f.next());//value的值就是yield的返回值,done表示这个生成器已经执行结束了
console.log(f.next());
console.log(f.next());
console.log(f.next());
console.log(f.next());//若done为true,value就是return的返回值,这个生成器对象已经全部执行完毕

//用for...of循环迭代生成器对象
for(var x of fib(10)){
console.log(x);
}
/*运行结果如下:
{value: 0, done: false}
{value: 1, done: false}
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
{value: undefined, done: true}
0
1
1
2
3
5
8
13
21
34*/

由于generator可以在执行过程中多次返回,因此它看上去像是可以记住执行状态的函数。因此可以利用它来实现保存状态的功能。

练习题

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

1
2
3
4
5
6
var current_id = 0;

function next_id() {
current_id ++;
return current_id;
}

由于函数无法保存状态,故需要一个全局变量current_id来保存数字。

不用闭包,试用generator改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function* next_id(){
var current_id=1,n=0;
while(n<100){
yield current_id;
current_id++;
n++;
}
return ;
}

// 测试:
var
x,
pass = true,
g = next_id();
for (x = 1; x < 100; x ++) {
if (g.next().value !== x) {
pass = false;
console.log('测试失败!');
break;
}
}
if (pass) {
console.log('测试通过!');
}
CATALOG
  1. 1. 函数
    1. 1.1. 函数定义和调用
      1. 1.1.1. 定义函数
      2. 1.1.2. 调用函数
      3. 1.1.3. arguments
      4. 1.1.4. rest参数
      5. 1.1.5. 练习题
    2. 1.2. 变量作用域与解构赋值
      1. 1.2.1. 变量提升
      2. 1.2.2. 全局作用域
      3. 1.2.3. 名字空间
      4. 1.2.4. 局部作用域
      5. 1.2.5. 常量
      6. 1.2.6. 解构赋值
        1. 1.2.6.1. 使用场景
    3. 1.3. 方法
      1. 1.3.1. apply
      2. 1.3.2. 装饰器
    4. 1.4. 高阶函数
      1. 1.4.1. map/reduce
        1. 1.4.1.1. map
        2. 1.4.1.2. reduce
        3. 1.4.1.3. 练习题
      2. 1.4.2. filter
        1. 1.4.2.1. 回调函数
        2. 1.4.2.2. 练习题
      3. 1.4.3. sort
      4. 1.4.4. Array
        1. 1.4.4.1. every
        2. 1.4.4.2. find
        3. 1.4.4.3. findIndex
        4. 1.4.4.4. forEach
    5. 1.5. 闭包
      1. 1.5.1. 函数作为返回值
      2. 1.5.2. 闭包
      3. 1.5.3. 箭头函数
        1. 1.5.3.1. this
        2. 1.5.3.2. 练习题
      4. 1.5.4. generator
        1. 1.5.4.1. 练习题