(五)函数的扩展
1、函数参数的默认值
1)ES5怎么制定默认值
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。
if (typeof y === 'undefined') {
y = 'World';
}
2)ES6怎么制定默认值
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
3)ES6中参数变量是默认声明,所以不能用 let 或 const 再次声明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
// 为什么变量不能重命名
4)使用参数默认值时,函数不能有同名参数
// 不报错
function foo(x, x, y) {
// ...
}
// 报错
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
5)默认值是惰性求值的,运行时计算
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100
2、解构赋值默认值结合使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
?为什么会出现上述情况,为什么需要双重默认
// 写法一
function m1({x = 0, y = 0} = {}) {
console.log(x,y);
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
console.log(x,y);
return [x, y];
}
m1({y:1});
m2({y:1});
1)切记只有 undefined 的情况下才能触发默认值,包括 null,'',NaN,false 等等都不能。
3、函数的 length 属性
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
1) 函数的length其实就是参数的长度。
2) length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数
3) 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了
4) 函数的length和arguments.length一样吗
console.log((function (a, b, c = 5) {
console.log(arguments.length);
}).length) // 2
const abc = function (a, b, c = 5) {
console.log(arguments.length);
}
abc(1,2,3);
4、作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
请某位小伙伴解释?
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里面,并没有定义变量foo,所以foo指向外层的全局变量foo,因此输出outer。
如果写成下面这样,就会报错。
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar() // ReferenceError: foo is not defined
5、应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
可以将参数默认值设为undefined,表明这个参数是可以省略的。
function foo(optional = undefined) { ··· }
6、rest 参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
1) rest 参数代替arguments变量的例子。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
1) rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错
// 报错
function f(a, ...b, c) {
// ...
}
2) 函数的length属性,不包括 rest 参数
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
6、name 属性
自己看。
7、箭头函数
JavaScript 中,函数可以用箭头语法(”=>”)定义,有时候也叫“lambda表达式”。
这种语法主要意图是定义轻量级的内联回调函数
// Arrow function:
[5, 8, 9].map(item => item + 1); // -> [6, 9, 10]
// Classic function equivalent:
[5, 8, 9].map(function(item) {
return item + 1;
}); // -> [6, 9, 10]
1) 箭头函数的演变
var f = () => { return 5; }
// 等同于
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
const abc = (a) => (b) =>a+b;
console.log(abc(3)(4));
// 田田解释上面语句
2) 箭头函数的返回值
// 有返回值
var f = () => { return 5; }
// 等同于
var f = () => 5;
// 无返回值
var sum = (num1, num2) => void (num1 + num2);
console.log(sum(3,4));
// 两种方法无区别
// 无返回值
var sum = (num1, num2) => {
num1 + num2;
};
console.log(sum(3,4));
3) 箭头函数的简写
// 省略return
var f = () => { return 5; }
// 等同于
var f = () => 5;
// 省略圆括号
var f = i =>console.log(i);
// 省略void
var f = i =>void(console.log(i));
var f = i =>void(console.log(i));
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
4) 箭头函数的常用方法
箭头函数的一个用处是简化回调函数。
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
另一个例子是
// 正常函数写法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);
下面是 rest 参数与箭头函数结合的例子。
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
5) 箭头函数的辨识方法
尽管箭头函数和传统函数的语法不同。
var abc = (a,b)=>a+b;
console.log(typeof abc)
console.log(abc instanceof Function) //true
箭头函数与其他函数并无二致。
箭头函数也可以使用call(),apply和bind()方法 ,但与其他函数不同的是,箭头函数的this值不会受这些方法的影响。
var sum = (a,b)=>a+b;
console.log(sum.call(null,1,2));
console.log(sum.apply(null,[1,2]));
const bindfun =sum.bind(null,1,2);
console.log(bindfun());
6) 箭头函数的总结
箭头函数的来源
箭头函数的渊源可以追溯到上古时期一个叫lambda演算的东西。lambda演算是数学家提出来的,有些数学家跟我们程序员一样也很懒,数学定理那么多,今天要证三角定律,明天要证勾股定律,累不累!那能不能将所有的证明问题用一个统一的体系进行形式化描述,然后由机器来完成自动推导呢?lambda演算就是干这个的,图灵也搞了一套体系叫图灵机,两者是等价的。
关于lambda演算说了这么多,好像跟今天要讲的箭头函数没什么关系?其实是有关系的,lambda演算深刻影响了箭头函数的设计。数学家们喜欢用 纯函数 式编程语言,纯函数的特点是没有副作用,给予特定的输入,总是产生确定的输出,甚至有些情况下通过输出能够反推输入。要实现纯函数,必须使函数的执行过程不依赖于任何外部状态,整个函数就像一个数学公式,给定一套输入参数,不管是在地球上还是火星上执行都是同一个结果。
6.1)没有this,super,arguments,和new.target绑定
箭头函数中的this、super、arguments及new.target这些值由外围最近一层非箭头函数决定。
6.2)不能通过new关键字调用
箭头函数 箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果能通过new关键字条用箭头函数,程序会抛出错误
6.3)没有原型
由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性
6.4)不可以改变this的绑定
函数内部的this值不可被改变,在函数的生命周期内时钟保持一致。
6.5)不支持arguments对象
箭头函数没有arguments绑定,所以你必须通过命名参数和不定参数这两种形式访问函数的参数
6.6)不支持重复的命名参数
无论严格模式还是非严格模式
7) 尾调用优化
尾调用
尾调用指的是函数作为另一个函数的最后一条语句被条用。
function doSomething(){
return doSomethingOther(); //尾调用
}
为什么要尾条用
ES5引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧,将其推入调用栈来表示函数调用。也即是说,在循环调用中,每一个未用完的栈帧都会被保存在内存中,当调用栈变得过大时会造成程序问题。(栈溢出或内存溢出)
1)怎么做才是尾调用优化
1.1)尾调用不访问当前帧栈的变量(也就是说函数不是一个闭包)
1.2)在函数内部,尾调用是最后一条语句
1.3)尾调用的结果作为函数值返回
满足上述三个条件,可以被javascript引擎自动优化
"use strict"
function doSomething() {
return doSomethingOther(); //尾调用
}
示例
//不能优化
"use strict"
function doSomething() {
doSomethingOther();
}
//不能优化
"use strict"
function doSomething() {
return 1+doSomethingOther();
}
//不能优化
"use strict"
function doSomething() {
var result = doSomethingOther();
return result;
}
//不能优化
"use strict"
function doSomething() {
var num = 1,func=()=>num;
// 无法优化,该函数是一个闭包
return func();
}
递归函数是其最主要应用场景
阶乘函数实例
function factorial(n) {
if(n<=1){
return 1;
} else {
// 不能优化
return n* factorial(n-1);
}
}
//能优化
function factorial(n, p =1) {
if(n<=1){
return 1*p;
} else {
const result = n* p;
return factorial(n-1, result);
}
}
console.log(factorial(10));
8) 项目实战
8.1) rest运算符实战
const params = { ...values };
params.coverImgStorageId = coverImgObj.storageId;
params.shareImgStorageId = shareImgObj.storageId;
const content = JSON.stringify(editorList);
params.content = content;
delete params.editorList;
delete params.coverImg;
delete params.shareImg;
//优化后代码
const {editorList, coverImg, shareImg, ...params} = values;
params.coverImgStorageId = coverImgObj.storageId;
params.shareImgStorageId = shareImgObj.storageId;
const content = JSON.stringify(editorList);
params.content = content;
8.2)斐波那契数列尾递归调用优化
作业