二、函数
1.js中函数(也是一个对象)如果没有return语句,函数执行完成后会返回undefined
,有两种定义函数的方法,第二种是将函数名abs
视为指向abs
函数的变量,function (x)
是一个匿名函数,“这个匿名函数赋值给了变量abs
,所以通过变量abs
就可以调用该函数”,示例代码:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
2.js函数允许传入任意个参数而不影响调用(不会报错),会出现两种特殊情况:(1)传入的参数比定义的参数多,函数执行没有问题;(2)传入的参数比定义的参数少,未接收到具体的值的参数将收到undefined
。注意js的不等于符号不是!=
而是!==
。
3.“js的关键字arguments
只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数(不管是多于还是少于定义的参数个数),arguments
类似Array
但又不是一个Array
,常用于判断传入参数的个数:arguments.length
”。
4.ES6标准引入了参数rest
,多于函数定义参数个数的参数以数组的形式赋给rest
,如果没传入足够的参数,rest
的值是空数组[]
,而不是undefined
,并且rest
参数以...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 []
5.练习用rest参数编写一个sum()
函数,接收任意个参数(数值)并返回它们的和(接收任意个参数意味着sum函数只需要定义rest参数):
'use strict';
function sum(...rest) {
var x = 0;
for (var i = 0; i < rest.length; ++i) {
if (typeof rest[i] === 'number') {
x = x + rest[i];
}
}
return x;
}
// 测试:
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('测试通过!');
}
6.注意正确的return多行语句写法需要括号括起来。
7.练习1,定义一个计算圆面积的函数area_of_circle()
,它有两个参数:r: 表示圆的半径;pi: 表示π的值,如果不传,则默认3.14:
'use strict';
function area_of_circle(r, pi) {
var s = 0;
if (arguments.length === 1) {
s = 3.14 * r * r;
}else if (arguments.length === 2) {
s = pi * r * r;
}
return s;
}
// 测试:
if (area_of_circle(2) === 12.56 && area_of_circle(2, 3.1416) === 12.5664) {
console.log('测试通过');
} else {
console.log('测试失败');
}
练习2,小明是一个JavaScript新手,他写了一个max()
函数,返回两个数中较大的那个,但无论传入什么数,max()
函数总是返回undefined
,问题在于return多行语句处理时,return后面自动补上了分号;
,修改后的代码:
'use strict';
function max(a, b) {
if (a > b) {
return a;
} else {
return b;
}
}
console.log(max(15, 20));
8.“js的函数可以互相嵌套,内部函数可以访问(也可以改变)外部函数定义的变量,反过来则不行”,示例代码:
'use strict';
function foo() {
var x = 1;
function bar() {
var y = x++; // bar可以访问foo的变量x
console.log('内部改变了外部的x:' + x);
}
bar();
console.log('外部的x:' + x); // 外部函数的x值也改变了
}
foo();
结果:
内部改变了外部的x:2
外部的x:2
此外,“如果内部函数定义了与外部函数重名的变量,则内部函数的变量将屏蔽外部函数的变量”。
9.注意:“在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则,最常见的做法是用一个var
申明函数内部用到的所有变量”,示例代码:
'use strict';
function foo() {
var
x = 1, // x初始化为1
y = x + 1, // y初始化为2
z, i; // z和i为undefined
// 其他语句
console.log('x:' + x + ', y:' + y + ', z:' + z + ', i:' + i);
}
foo();
结果:
x:1, y:2, z:undefined, i:undefined
10.“不在任何函数内定义的变量就具有全局作用域,JS默认有一个全局对象window
,全局作用域的变量实际上被绑定成为window
的一个属性”,示例代码:
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
“以变量方式var foo = function () {}
定义的函数实际上也是一个全局变量,因此顶层函数的定义也被视为一个全局变量,并绑定到window
对象”,示例代码:
'use strict';
function foo() {
alert('foo');
}
foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用
11.“把自己的代码全部放入唯一的名字空间中(把自己的所有全局变量和顶层函数全部绑定给一个全局对象),会大大减少全局变量冲突的可能。”
12.“在for
循环等语句块中是无法定义具有局部作用域的变量的”,函数内以for (var i = 0;...)
形式定义的变量i作用域其实是整个函数内部,不是局限于for循环内部,“用let
替代var
可以申明一个块级作用域(在函数内部的括号{}
是一个块级作用域,如for循环)的变量”。
13.解构赋值可以简化代码(但需要在支持ES6解构赋值特性的浏览器中运行),其可以将数组或对象中的元素或属性赋值给多个变量,但要注意赋值时结构要一致,并且可以忽略一些元素,但需要注意数组进行解构赋值时的情况,示例代码:
let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'
对象进行解构赋值且忽略一些元素时不用特殊处理,示例代码:
'use strict';
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
var {name, age, passport} = person;
// name, age, passport分别被赋值为对应属性:
console.log('name = ' + name + ', age = ' + age + ', passport = ' + passport);
结果:
name = 小明, age = 20, passport = G-12345678
注意如何将嵌套的对象属性赋值给变量的情况(重点学习),以及要使用的变量名和属性名不一致的情况,示例代码:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
// 把passport属性赋值给变量id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined
为了避免不存在的属性返回undefined,解构赋值可以使用默认值。注意最好在对象变量解构赋值时同时声明变量且赋值,如果先声明变量再赋值会出现问题,此时用小括号()
括起来即可。
// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =
解决:
({x, y} = { name: '小明', x: 100, y: 200});
14.绑定给对象的函数成为对象的方法,绑定给对象的函数中,this
指向该对象,但如果在该函数内部再定义函数,在内部函数中this
指向undefined
(在strict
模式下)或window
变量(不在strict
模式下)注意:(1)要保证this
指向正确,必须用obj.xxx()
的形式调用;(2)为了可以在方法内部定义其他的函数,可以参考以下代码捕获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 fun = xiaoming.age;
fun();
15.要指定函数的this
指向哪个对象,可以用函数本身的apply
方法或者call
方法,普通函数(将this
绑定为null
)调用示例如下:
// 调用Math.max(3, 5, 4)
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
注意:“利用apply
方法还可以动态改变函数的行为”,方法部分涉及的this
关键字和apply
、call
方法需要重点理解学习,最好的做法是写代码的时候明确this
的指向。
16.在js中高阶函数可以接收另一个函数作为参数。map方法定义在数组中,通过数组变量.map(函数名),可以将map方法的函数参数作用于数组的每个元素,返回处理后的新数组;“Array的reduce()
把一个函数作用在这个Array
的[x1, x2, x3...]
上,这个函数必须接收两个参数,reduce方法把结果继续和序列的下一个元素做累积计算”,效果是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
// 用reduce对数组进行求和
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
17.“想办法把一个字符串13579
先变成Array
——[1, 3, 5, 7, 9]
,再利用reduce()
就可以写出一个把字符串转换为Number的函数”,练习不要使用JavaScript内置的parseInt()
函数,利用map和reduce操作实现一个string2int()
函数:
'use strict';
function string2int(s) {
var str = [];
// 把字符串中的字符放入数组中
// 方法一:用var...of遍历字符串
for (var c of s) {
str.push(c);
}
// 方法二:常规操作
/* for (var i = 0; i < s.length; ++i) {
str[i] = s[i];
}*/
var str2array = str.map(function (x) {
return x - '0';
});
var result = str2array.reduce(function (x, y) {
return 10 * x + y;
});
return result;
}
// 测试:
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']
,代码如下:
'use strict';
function normalize(arr) {
var result = arr.map(function (s) {
str = s[0].toUpperCase() + s.substring(1).toLowerCase();
return str;
});
return result;
}
// 测试:
if (normalize(['adam', 'LISA', 'barT']).toString() === ['Adam', 'Lisa', 'Bart'].toString()) {
console.log('测试通过!');
}
else {
console.log('测试失败!');
}
注意:“由于map()
接收的回调函数可以有3个参数:callback(currentValue, index, array)
,通常我们仅需要第一个参数,而忽略了传入的后面两个参数,在以下代码中parseInt(string, radix)
没有忽略第二个参数,导致实际执行的函数分别是:parseInt(‘1’, 0); // 1, 按十进制转换、parseInt(‘2’, 1); // NaN, 没有一进制、parseInt(‘3’, 2); // NaN, 按二进制转换不允许出现3。”:
'use strict';
var arr = ['1', '2', '3'];
var r;
r = arr.map(parseInt);
console.log(r);
结果:
1,NaN,NaN
可以将parseInt函数换成Number函数(仅接受一个参数)解决问题。
18.“数组的filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素”,filter
函数和map
函数一样,其回调函数可以接收多个参数,但一般只用第一个参数(代表数组元素)
19.练习用filter函数筛选出100以内的素数,因为任何一个数都不可能分解成两个大于其平方根的数的乘积,只能分解为一个大于或等于其平方根的质因子,另一个小于或等于其平方根的质因子,即一个合数一定有小于它平方根的质因子,因此只需要判断到一个数的平方根即可,代码如下:
'use strict';
function get_primes(arr) {
var result = arr.filter(function (x) {
if (x === 0 || x === 1) {
return false;
}
if (x === 2) {
return true;
}
for (var i = 2; i <= Math.sqrt(x); ++i) {
if(x % i === 0) {
return false;
}
}
return true;
});
return result;
}
// 测试:
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());
}
20.注意“Array
的sort()
方法默认把所有元素先转换为String再排序(字符串默认根据ASCII
码进行排序)”,但sort
函数也可以接收一个比较函数(有两个参数)来实现自定义的排序,sort
方法直接改变数组。
21.“数组的every
方法可以判断数组的所有元素是否满足测试条件;find
方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
;findIndex()
和find()
类似,也是查找符合条件的第一个元素,不同之处在于findIndex()
会返回这个元素的索引,如果没有找到,返回-1
;forEach
方法简化代码示例如下:”。
'use strict';
var arr = ['Apple', 'pear', 'orange'];
arr.forEach(console.log); // 依次打印数组每个元素
22.注意“返回闭包时,返回的函数不要引用任何循环变量,或者后续会发生变化的变量”,重点学习一定要引用循环变量的情况,示例代码:
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
闭包还可以封装私有变量,“闭包就是携带状态的的函数,且其状态可以完全对外隐藏”;“闭包还可以把多参数的函数变成单参数的函数”。
23.“脑洞大开”部分一开始没咋看明白,翻了下评论,果然人才辈出,以下是大佬原回复(像俄罗斯套娃,手动狗头):
zero(f)(x) = x;
one(f)(x) = f(x);
two(f)(x) = one(f)(one(f)(x)) = one(f)(f(x)) = f(f(x));
three(f)(x) = two(f)(one(f)(x)) = two(f)(f(x)) = f(f(f(x)));
...
five(f)(x) = f(f(f(f(f(x)))));
// 尝试定义four,当然不止这一种
// 计算数字4 = 2 + 2
var four = add(two, two);
// 推理four
four(f)(x) = two(f)(two(f)(x)) = two(f)(f(f(x))) = one(f)(one(f)(f(f(x)))) = one(f)(f(f(f(x)))) = f(f(f(f(x))));
24.箭头函数相当于匿名函数(重点学习),箭头左边是参数,右边是返回的值,注意返回对象时需要加括号:
x => { foo: x } // 报错
x => ({ foo: x }) // 没问题
25.在箭头函数中this
永远指向词法作用域,即外层调用者,注意“用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数(对象)被忽略”。
26.练习使用箭头函数简化排序时传入的函数:
'use strict'
var arr = [10, 20, 1, 2];
// 方法一
arr.sort((x, y) => {
if (x < y) return -1;
if (x > y) return 1;
return 0;
});
// 方法二
arr.sort((x, y) => {
return x - y;
});
console.log(arr); // [1, 2, 10, 20]
27.“生成器generator
由function*
定义,除了return
语句,还可以用yield
返回多次”,可以理解其为可以返回多次的函数,有两种调用generator
对象的方法(重点学习),(1)调用生成器的next方法,一直执行到return语句结束;(2)类似for (var x of f(3))
循环迭代生成器对象f(3)
时,可以理解x是生成器返回的多个返回值之一,但这种方法不会返回return语句定义的值,不管是undefined还是其他。generator
有两个重要用处:(1)“可以实现需要用面向对象才能实现的功能”;(2)“把异步回调代码变成“同步”代码”,到ajax部分重点学习。
28.练习用generator
生成一个自增的ID,有点没太明白,翻了下评论区的代码,理解了一点:yield
语句只在生成器调用next
方法时才会执行,并且存储当前状态(也就是自增的变量ID)、跳出函数,下一次调用next
方法时会跟在上一次yield
部分后继续执行,(但仍然是一脸蒙比,可能学到后面实践用的时候会明白一点?待学习)参考代码如下:
'use strict';
// 第一种
function* next_id() {
var id = 1;
while (true) {
yield id++;
}
return;
}
// 第二种,x就是后面代码里的for循环自增的x
function* next_id() {
while (true) yield x;
}
// 测试:
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('测试通过!');
}