函数
《深入理解ES6》—— Nicholas C. Zakas
在ES5函数之上,进行增量改进。
1. 参数默认值
参数默认值 让函数在接收到的参数数量不足时行为更清晰。
1.1. JS函数的特点
调用时可接受任意数量的参数,而无视函数声明处的参数数量。
1.2. ES5中模拟
利用逻辑或运算符(||
),当左侧的值为假时,返回右侧的操作数。
function makeRequest( url, timeout, calback ) {
// 方式一
timeout = timeout || 3000;
// 方式二
timeout = ( typeof timeout !== "undefined" ) ?
timeout : 3000;
}
方式一:如果传给 timeout 的值是 0,则会出错。
方式二:公共模式。
1.3. ES6中的参数默认值
未给参数传值,才会使用默认值。
给参数传值了(就算是 null
或 undefined
),则不使用默认值。
function makeRequest( url, timeout = 3000, calback ) {
}
参数默认值是一个表达式,这意味着,可以是一个函数调用
function getValue() {
return Math.random();
}
function add( first, second = getValue() ) {
return first + second;
}
2. 剩余参数
说明:
解决传入的参数过多的情况,替换 arguments
。
语法:
三个点(...
)与一个紧跟着的具名参数组成,是数组类型。
示例:
function foo( name, ...args ) {
// ...
}
限制:
- 一个函数只能有一个剩余参数,且必须放在最后
- 不能在 setter 中使用
3. 扩展运算符
语法:
在数组前面加 ...
。
说明:
避免使用apply()
混淆代码的真实意图。
JS引擎会将数组分割为独立参数传递进去。
示例:
let values = [1, 2, 3, 5, 4];
// 以前
Math.max.apply( Math, values ); //=> 5
// 现在
Math.max(...values); //=> 5
Math.max(...values, 10); //=> 10
4. 函数的名称属性
ES6给所有函数添加了 name
属性,以解决各种函数定义方式造成调试困难的问题。
示例:
function fnA() {};
fnA.name; //=> "fnA"
let fnB = function () {};
fnB.name; //=> "fnB"
// setter函数 会有 "set" 前缀
// bind()创建的函数 会有 "bound" 前缀
// Function 构造器创建的函数 会有 "anonymous" 前缀
5. 明确函数的用途
用途:
- 普通函数,用于调用
- 构造函数,创建对象
new
:
函数根据是否使用 new
来调用,而有双重用途。
当用new
来调用时,函数内部的this
是一个新对象,并作为函数的返回值。
区分
通过首字母是否大写来区分是否用new
来调用,如 Person
、getPersonName
。
机制
JS为函数提供了两个不同的内部方法:
[[call]]
:当函数未使用new
调用时,[[call]]
会被执行,运行的是函数体里的代码。[[constructor]]
:当使用new
调用时,[[constructor]]
会被执行,负责创建一个新对象,并将新最新作为this
去执行函数体
ES6
新增 new.target
从而消除函数调用的不确定性。
function Person( name ) {
if ( typeof new.target !== "undefined" ) {
this.name = name; // 使用 new 调用
}
}
6. 块级函数
在ES3中,在代码块中声明函数(即块级函数)严格来说是一个语法错误,
但所有的浏览器却都支持该语法。
但支持该语法的浏览器都有轻微的行为差异,所以最佳实践就是不要在代码块中声明函数(而是使用函数表达式)。
ES6标准化这种行为来消除不兼容性。
if( true ) {
typeof doSomething; //=> "function"
function doSomething() {
}
doSomething();
}
ES6 会将 doSomething()
函数视为块级声明,
并允许它在定义所在的代码块内部被访问。
块级函数会被提升到定义所在代码块的顶部,
一旦if
执行完毕,doSomething()
就会被移除。
使用 let
的函数表达式不会造成提升。
7. 箭头函数
7.1. 与传统JS函数的比较
- 没有
this
、super
、arguments
、new.target
绑定 - 不能使用
new
来调用:没有[[Construct]]
方法,不能作为构造函数 - 没有原型:不能使用
new
,就不需要原型了 - 不能更改
this
:在函数的整个生命周期内,其值保持不变 - 没有
arguments
对象:依赖具名参数和剩余参数来访问函数的参数
在JS编程中,this
绑定是发生错误的常见根源之一,
在嵌套函数中会因为调用方式的不同,而导致丢失对外层 this
值的追踪,
就可能导致预期外的程序行为。
其次,箭头函数使用单一的 this
值来执行代码,
使得JS引擎可以更容易对代码的操作进行优化;
而常规函数可能会作为构造函数使用(导致this
易变而不利于优化)。
其余差异也是聚集在减少箭头函数内部的错误与不确定性,
这样JS引擎也能更好地优化箭头函数的运行。
7.2. 语法
参数 => 函数体
7.2.1. 参数
当只有一个参数时,包裹参数列表的括号()
可以省略。
// 无参数
() => 函数体
// 一个参数
(arg1) => 函数体
arg1 => 函数体
// 多个参数
(arg1, arg2) => 函数体
7.2.2. 函数体
只有一个表达式,且以该表达式作为返回值是,函数体不需要{}
包裹。
arg1 => arg1 * 2;
(arg1, arg2) => arg1 + arg2;
(arg1, arg2) => (arg1 + arg2);
// 返回对象字面量
() => ( {name: 1, gender: 0} );
arg1 => {
return arg1 * 3;
}
7.3. 没有 this 绑定
JS最常见的错误领域之一就是函数内的this
绑定。
一个函数内部的this
值可以被改变,这取决于调用该函数时的上下文。
箭头函数没有 this
绑定,
意味着箭头函数内部的this
值只能通过查找作用域链来确定。
如果箭头函数在一个非箭头函数的函数体内,
那么 this
值就会与该函数的相等。
箭头函数的 this
值由包含它的函数决定。
7.4. 没有 arguments 绑定
尽管箭头函数没有自己的 arguments
对象,
但仍然能访问包含它的函数的 arguments
对象。
7.5. 识别
let f = () => {};
typeof f; //=> "function"
f instanceof Function; //=> true
8. 尾调用优化
通常用于递归,会带来显著的性能提升。
但闭包不能被优化,这也是为什么避免使用闭包的原因。
9. 总结
函数在ES6中并未经历巨大变化,但一系列增量改进使得函数更易于使用。
在特定参数未被传入时,函数的默认参数允许你更容易指定需要使用的值。
剩余参数允许你将余下的所有参数放入指定数组。
使用真正的数组并让你指定哪些参数需要被包含,
使得剩余参数成为比 arguments
更为灵活的解决方案。
扩展运算符是剩余参数的好伙伴,允许在调用函数时将数组解构为分离的参数。
新增的 name
属性能帮你在调试与执行方面更容易得识别函数。
函数的行为被[[Call]]
和[[Construct]]
方法所定义,
前者对应普通的函数执行,二者对应使用了new
的调用。
new.target
元属性能在函数体内判断该函数调用时是否使用了new
。
ES6函数的最大变化就是增加了箭头函数。
箭头函数被设计用于替代匿名函数表达式,
它拥有更简洁的语法、词法级的this
绑定,并且没有 arguments
对象。
不能改变this
绑定的值,因此不能作为构造器。
尾调用优化允许某些函数调用被优化,以保持更小的调用栈、使用更少的内存,
并防止堆栈溢出。