深入理解ES6 - 函数
ECMAScript6 在 ECMAScript5 的基础上对函数进行了大量的改进,使之更灵活。
函数形参的默认值
js 函数无论在定义中申明了多少形参,都可以传入任意数量的参数。ES6 以前,可以用如下方式为参数赋默认值:
function(a,b){
a = a || 20; //风险:当 a 传入0时,即使合法,a 也会被赋予20
b = (typeof b !=="undefined") ? b :20; //更安全,但需要额外编码
}
ES6 中的默认参数值
ES6 中设置默认参数值比较简单。注意:在 ES6 中,参数的默认值并不会改变 arguements 对象中的值,这与 ES5 严格模式是一致的。
function demo(url, timeout = 6000, parameter = {a:1}) {
//调用时如果传入三个参数,则不使用默认值
}
//在已指定默认值的参数后可以继续声明无默认值的参数
function demo2(url, timeout = 6000, parameter) {
//只有第二个参数不传值,或传入 undefined 时,才会用默认参数
}
demo2('index.action'); //使用 timeout 的默认值,parameter 为 undefined
demo2('index.action', undefined, {a:1}); //使用 timeout 的默认值
demo2('index.action', null, {a:1}); //不使用 timeout 的默认值
默认参数表达式
可以通过函数执行得到默认参数的值。
function add(a, b = getValue()) {
return a+ b
}
add(1); //假设 getValue() 返回5,则 add(1) 结果是6。
上面的代码,js 引擎在解析函数申明时,不会执行 getValue() 方法,只有当调用 add() 函数且不传第二个参数时才会调用。正因为默认参数是在函数调用时求值,故可以使用先定义的参数作为后定义的参数的默认值,反之不成立。
//也可以将 a 作为参数运算后赋值给 b,如 b = getValue(a)。
function add(a, b = a) {
return a + b;
}
add(1,2); //3
add(1); //2
function add2(a = b, b) {
return a + b;
}
add2(1,2); //3,不会报错,因为 a 传入了合法值,没有运算 a = b
add2(undefined,2); //报错,因为运算 a = b 时,b 未定义。(b 在“临时死区”中)
函数参数有自己的作用域和临时死区,与函数体的作用域是各自独立的,也就是说,参数的默认值不可以访问函数体内申明的变量。
无名参数
JavaScript 提供 arguments 对象来检查函数的所有参数,所以不是每一用到的参数都需要定义。ES6中的不定参数和展开运算符就是很好的解决方案。
不定参数
申明函数时,参数前加三个点,表明这是个不定参数。该参数是一个数组,包含这自它之后传入的所有参数。关于不定参数的注意事项:
- 函数最多只能有一个不定参数,且要放在参数末尾;
- 不定参数不能用于对象字面量 setter 中,因为 setter 有且只能有一个参数;
- 不定参数不会影响函数的 length 属性,该属性统计的是函数命名参数的数量,不包含不定参数;
- 无论是否使用不定参数,arguments 对象总是包含所有传入的参数。
function Demo3(name, ...args){
console.log(args.length); //2
console.log(arguments.length); //3
console.log(args[0],arguments[0]); //a demo
console.log(args[1],arguments[1]); //b a
}
Demo3('demo','a','b');
console.log(Demo3.length); //1,仅包含 name,不包含不定参数
展开运算符
展开运算符与不定参数很相似,它可以简化使用数组给参数传参的编码。它可以通过指定一个数组,然后将数组的每个元素作为独立的参数传入函数。
let a = 10, b = 20;
console.log(Math.max(a,b)); //20
//如何获取数组中的最大元素呢
let arr = [10, 30, 6];
//ES5 的可以通过 apply() 实现
console.log(Math.max.apply(Math, arr)); //30
//ES6 的展开运算符可以简化上述操作
console.log(Math.max(...arr)); //30 等价于 Math.max(10, 30, 6)
console.log(Math.max(...arr, 666)); //666 限制最小返回值为666
函数的其它属性
name属性
ES6 为所有函数新增了 name 属性用于标记函数,且每个函数的 name 属性都有一个合适的值。不过请注意:函数 name 属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用 name 属性的值来获取对函数的引用。
function doThing(){};
var doSomethimg = function(){};
var doAnother = function doAnotherElse(){};
console.log(doThing.name); //doThing 对应声明时的函数名
console.log(doSomethimg.name); //doSomethimg 匿名函数表达式对应被赋值的变量名称
console.log(doAnother.name); //doAnotherElse 函数申明时的函数名比被赋值的变量名权重高
var something = function(){};
console.log(something.bind().name); //bound something 通过 bind() 创建的函数带有前缀 bound
console.log((new Function()).name); //anonymous 通过 Function 构造函数创建的函数,带有前缀 anonymous
元属性 new.target
元属性是指非对象的属性。
ES6 中引入了 new.target 这个元属性,可以判断函数是否是通过 new 关键字调用。通过 new 关键字调用函数时,new.target 被赋值为 new 操作符的目标,否则 new.target 的值为 undefined。注意:在函数外使用 new.target 是一个语法错误。
function Demo(name){
if(typeof new.target !== 'undefined'){
this.name = name;
}else{
throw new Error('请通过 new 关键字来调用 Demo');
}
}
var demo1 = new Demo('demo1');
Demo.call(demo1, 'demo2'); //报错
可以将 typeof new.target !== 'undefined' 替换成 typeof new.target !== Demo 来检查函数是否被某个特定的构造函数所调用。
块级函数
JavaScript 早期版本中,在代码块中声明块级函数是一个语法错误,ES5 严格模式中会给出错误提示,但是 ES6 对其提供了支持。在严格模式下,块级函数会被提升至作用域顶部,出作用域则函数被销毁。非严格模式下,则会提升至外围函数或全局作用域的顶部。
"use strict"
if(true){
console.log(typeof demo1); //function 块级函数提升至作用域顶部,出if块则销毁
console.log(typeof demo2); //报错,let 定义的函数表达式不会被提升
function demo1(){}
let demo2 = function(){};
}
console.log(typeof demo1); //严格模式下:undefined,非严格模式下:function
console.log(typeof demo2); //undefined
箭头函数
箭头函数(=>)是 ES6 中重要的新特性。与传统函数有些区别,区别的原因主要是为了减少错误来源、简化代码,使引擎可以更好的执行箭头函数。
- 箭头函数中的 this、super、arguments、new.target 由外围最近一层非箭头函数决定。
- 不能通过 new 关键字调用,没有 construct 方法,没有 prototype 属性。
- 函数 this 值不可被改变。
- 没有 arguments 对象,所以只能通过命名参数或不定参数访问函数的参数。但无论函数在哪个上下文执行,箭头函数都可以访问外围函数的 arguments 对象。
箭头函数语法多变,但所有变种都由参数、箭头、函数体组成。
//一个参数的情况
let demo = arg => arg;
//实际上等价于
let demo = function(arg){
return arg;
};
//两个参数的情况
let demo1 = (arg1, arg2) => arg1 + arg2;
//没有参数的情况
let demo2 = () => 'hello';
//若箭头函数想要从函数体内向外返回一个对象字面量,就必须将该字面量包裹在圆括号内
//将对象字面量包裹在括号内,标示了括号内是一个字面量而不是函数体
var demo3 = id => ({ id: id, name: "demo3" });
//如果有多行代码,需要显示返回
var demo4 = () => {
//todo sth.
return true;
};
//如下情况用箭头函数就很方便
let arr = [22,3,11,66];
let arr1 = arr.sort(function(a, b){ return a- b; }); //[3,11,22,66]
let arr2 = arr.sort((a, b) => a-b); //[3,11,22,66]
箭头函数设计的初衷是“即用即弃”,个人理解与匿名函数类似。
尾调调用优化
尾调用指的是函数作为最后一条语句被返回。ES6 之前尾调用的实现与其他函数类似:创建一个新的栈帧,将其推入调用栈帧来表示函数调用。ES6 缩减了严格模式下调用栈的大小(非严格模式不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧:
- 尾调用不访问当前栈的变量。
- 在函数内部,尾调用是最后一条语句。
- 尾调用的结果作为函数值返
"use strict"
//引擎自动优化
function demo1(){
return doSomething();
}
//无法优化,因为没有返回
function demo2(){
doSomething();
}
//无法优化,因为尾调函数又参与了运算,不是作为结果返回
function demo3(){
return 1 + doSomething();
}
//无法优化,调用不在尾部
function demo4(){
let result = doSomething();
return result;
}
//无法优化,该函数是个闭包,即引用当前函数变量
function demo5(){
let num = 1;
let func = () => num;
return func();
}
尾调优化的主要应用场景是递归函数。