本文内容
默认参数
ES 6 之前想为函数添加默认参数,一般就是函数内部对参数进行检测,不存在时将其赋值为默认参数,但是这些检测代码会扰乱函数逻辑,而且想写出完全没有缺陷的检测代码还有点困难,所以 ES 6 提供了对默认参数的原生支持:
function makeRequest(url, timeout = 2000, callback = function () { }) {
// 函数其余部分
}
这个示例中,第二个和第三个参数都是可选参数,都有一个默认值。
我们也可以在中间使用默认参数,后续继续使用非默认的参数
function makeRequest(url, timeout = 2000, callback) {
// 函数其余部分
}
只有中间的参数有默认值,这种情况下,只有第二个参数没有传值或者主动传入 undefined,才会使用参数的默认值。(传入 null 不会使用默认值)
默认参数对 arguments 的影响
ES 6 中,函数参数与 arguments 会进行分离,而在 ES 5 非严格模式下,函数参数与 arguments 是同步更新的,也就是说
function f(first, second) {
console.log(first === arguments[0]); // true
console.log(second === arguments[1]); // true
first = 'a';
second = 'b';
console.log(first === arguments[0]); // true
console.log(second === arguments[1]); // true
}
f(1, 2);
严格模式下
function f(first, second) {
'use strict';
console.log(first === arguments[0]); // true
console.log(second === arguments[1]); // true
first = 'a';
second = 'b';
console.log(first === arguments[0]); // false
console.log(second === arguments[1]); // false
}
f(1, 2);
在 ES 6 中
function f(first, second = 3) {
console.log(first === arguments[0]); // true
console.log(second === arguments[1]); // false
first = 'a';
second = 'b';
console.log(first === arguments[0]); // false
console.log(second === arguments[1]); // false
}
f(1);
可以这么理解:
- 默认参数使用类似于 ES 5 的严格模式来执行
- 当我们使用默认值时,相当于:原值是 undefined,被修改成了那个默认值
- 同时由于 arguments 没有进行同步更改,因此
second !== arguments[1] - 后面的 2 个 false 则是与 ES 5 的严格模式一致
默认参数表达式
let value = 5;
function getValue() {
return value++;
}
function f(a, b = getValue()) {
console.log(a);
console.log(b);
}
f(2); // 2 5
f(2); // 2 6
f(2); // 2 7
显然,可以在默认参数的值那里嵌入一个表达式,表达式的值是调用时计算的
function f(a, b = a) {
console.log(a);
console.log(b);
}
f(2); // 2 2
f(3); // 3 3
f(4); // 4 4
这种方式,第二个参数引用第一个参数的值是允许的,这里有个限制:只能后定义的参数引用先定义的参数,先定义的参数引用后定义的参数将会报错,例如
function f(a = b, b) {
console.log(a);
console.log(b);
}
f(undefined,2); // 抛出错误
这个是由于默认参数的临时死区导致的
默认参数的临时死区
查看如下示例
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
实际上 first 和 second 的定义相当于如下
// add(1,1) 时的参数等价定义
let first = 1;
let second = 1;
// add(1) 时的参数等价定义
let first = 1;
let second = getValue(first);
现在,我们重写 add 方法
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误
这个方式里 first 和 second 的定义相当于如下
// add(1,1) 时的参数等价定义
let first = 1;
let second = 1;
// add(undefined,1) 时的参数等价定义
let first = second;
let second = 1;
当调用 add(undefined,1) 时,参数定义 first 时,second 尚处于临时死区,所以会抛出错误
变长参数
实际上,JavaScript 函数中,都有一个 arguments 参数,可以用来表示函数接收到的所有参数,从而实现变长参数的特性。但是 ES 6 提供了更方便的特性来直接支持这一点,使用方式和 Java 的变长参数类似。
在函数的命名参数前添加三个点 ... 就表示这是一个变长参数,这个参数在函数体内是一个数组,包含着自它之后传入的所有参数。例如:
function add(first, ...others) {
console.log(first + others.reduce((pre, next) => {
return pre + next;
}));
}
add(1, 2, 3, 4); // 10
我们使用 others 接收了 2 3 4,这三个参数组成了一个数组,也就是说在函数内部 others = [2,3,4]
函数的 length 属性只计算命名参数的数量,也就是说,add(first, ...others) 函数中,只有 first 才计入 length 数量,而 others 不计算在 length 中
使用限制:
每个函数最多只有一个变长参数(很合理,基本我见过有这种特性的语言都是这样的)
变长参数一定要放在参数末尾(也很合理和直观)
定长参数不能用于对象的 setter 方法中,例如
let object = { set name(...value) { // 执行一些逻辑 } };将会报错。因为对象字面量的 setter 的参数有且只能有一个,只能执行 object.name = xxx,只能传入一个参数,所以不允许使用变长参数来定义 setter
无论是否使用变长参数,
arguments总是表示函数接收到的所有参数
展开运算符
展开运算符可以让我们把一个数组展平,当做多个参数传入到函数中,例如
console.log(Math.max(1, 2, 3, 4)); // 4
console.log(Math.max([1, 2, 3, 4])); // NaN
第一行正确输出,第二行输出 NaN。因为第二行传入的是一个数组,Math.max 将其当成一个参数接收了,发现不是一个数字,于是输出 NaN。
而展开运算符则可以处理这种情况,例如
const array = [1, 2, 3, 4];
console.log(Math.max(...array)); // 4
我们在参数前面加上 ... 就可以将数组展平,Math.max(...array) 等价于执行了 Math.max(1,2,3,4),Math.max(...array, 5) 等价于执行了 Math.max(1,2,3,4,5)
函数名称
ES 6 为所有函数都添加了 name 属性。例如
function doSomething() {
// 空函数
}
let doAnotherThing = function () {
// 空函数
};
console.log(doSomething.name); // doSomething
console.log(doAnotherThing.name); // doAnotherThing
明确函数的多重用途
在 ES 5 以及之前,函数可以直接调用,也可以通过 new 来调用。通过 new 调用的方式是首先生成一个空对象,然后将函数的 this 指向这个空对象,最后将新对象的__proto__ 属性指向函数对象的 prototype。
函数的两个内部方法
每个函数通常有两个内部方法:[[Call]] 和 [[Construct]],当通过 new 调用的时候,指向的是内部的 [[Construct]],普通调用执行的是内部的 [[Call]]。具有 [[Construct]] 的函数被称为构造函数。
不是所有的函数都有
[[Construct]],例如箭头函数就没有[[Construct]],因此不能通过 new 来调用箭头函数
元属性 new.target
ES 6 中,函数内添加了一个元属性,new.target,当调用函数的 [[constructor]] 方法时,new.target 被赋值为 new 操作符的目标的构造函数,例如:
function Person() {
console.log(new.target === this.constructor);
}
new Person();
上述结果将会是 true,表示 new.target 是新对象的构造器
同时有
new.target=this.constructor=Person=Person.prototype.constructor
我们通过这个元属性可以知道,函数是不是被通过 new 来调用的
块级函数
ES 6 之前,在块级作用域内部声明函数是错误的语法。ES 6 之后添加了块级函数(严格模式下)
"use strict";
if (true) {
console.log(typeof dosomething); // function
function dosomething() {
}
dosomething();
}
console.log(typeof dosomething); // undefined
代码块内,函数的声明提升,所以第一次打印了 function,代码块外,块级作用域结束,因此 dosomething 不存在,打印 undefined
非严格模式下,ES 6 可以在代码块内声明函数,块级函数的声明不是被提升到代码块顶部,而是外围函数或者全局作用域的顶部
if (true) {
console.log(typeof dosomething); // function
function dosomething() {
}
dosomething();
}
console.log(typeof dosomething); // function
箭头函数
箭头函数有以下特性:
- 没有 this、super、arguments、new.target 属性。箭头函数中的这些值都由外围最近一层的非箭头函数决定
- 不能通过 new 关键字调用。箭头函数没有
[[constructor]]方法,不能被用作构造函数 - 没有原型。箭头函数不存在
prototype属性 - 不可以改变 this 绑定。函数内部的 this 值不可以改变,在函数体内始终保持一致
- 不支持 arguments 对象。所以必须要通过命名参数和变长参数进行访问
- 不支持重复的命名参数。(非严格模式下普通函数可以有重复的参数)
语法
let reflect = value => value;
// 实际上相当于
let reflect = function (value) {
return value;
}
当箭头函数只有一个参数,可以省略参数的括号,右侧的表达式求值后被当做返回值返回
let sum = (num1, num2) => num1 + num2;
// 实际上相当于
let sum = function (num1, num2) {
return num1 + num2;
}
传入多个参数时,参数周围需要有括号
let sum = () => 3;
// 实际上相当于
let sum = function () {
return 3;
}
没有参数时,也需要带有括号
也就是说,只有在有且仅有一个参数时,参数可以不带括号
let sum = (num1, num2) => {
return num1 + num2;
};
// 实际上相当于
let sum = function (num1, num2) {
return num1 + num2;
};
这种方式类似于传统的函数体
let donothing = () => {
};
// 实际上相当于
let donothing = function () {
};
定义一个空的箭头函数
let getObj = () => ({id: 1, name: 'czp'});
// 实际上相当于
let getObj = function () {
return {id: 1, name: 'czp'}
};
当我们想返回一个对象字面量的时候,要在外面添加一层小括号,这是防止与函数体混淆,不加括号会报错
箭头函数的this
箭头函数的 this 可以当成闭包来理解
let obj = {
init: function () {
console.log(this);
return () => console.log(this);
}
};
let func = obj.init();
func();
输出如下
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
{ init: [Function: init] }
{ init: [Function: init] }
分析:
- 箭头函数内的 this 类似于一个闭包,捕获了外围函数的 this
obj.init()调用后,init()内的 this 指向obj- 因此箭头函数中的
this指向obj
这种行为十分类似于闭包,所以按照闭包的思路来理解挺合适的。
与闭包中的变量捕获不同的是,箭头函数内的 this 是不可以被更改的,即使通过 call、apply、bind 方法来修改 this 值,也不会生效
箭头函数的arguments
这个跟箭头函数的 this 行为几乎一致,也是通过闭包的变量捕获的机制来完成的,就不再多讲解了
尾递归优化
ES 6 添加了尾递归优化的功能,基本用不上。
最好别写递归,尾递归优化最主要的限制是递归调用必须在结尾,而且直接作为值返回。