开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天
函数的扩展
1. 函数参数的默认值
基本用法
ES6之前,不能为函数的参数指定默认值,只能采用以下方法:
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello'); //output: Hello World
log('Hello', 'China'); //output: Hello China
log('Hello', ''); //output: Hello World
如上述代码所示,可以使用变通的方法为函数的参数指定默认值,但当参数转换类型后对应的布尔值为false(比如’’, 0)时,就有问题。
为了避免这种问题,可以增加一个判断条件
y = (typeof y !== unfefined) ? y : 1
ES6以后,允许为函数的参数设置默认值,直接写在参数定义后面,当函数没有参数或者参数为undefined时使用默认值。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello'); //output: Hello World
log('Hello', 'China'); //output: Hello China
log('Hello', '') //output: Hello
⚠️注意点:
-
函数的参数是默认声明的,不能在函数体内用let或者const再次声明 如下所示: function foo(x = 5) { let x = 1; //不可以再次声明
const x = 2; //不可以再次声明 }
-
声明函数参数的默认值时,不能有同名的参数
-
函数参数的默认值有表达式时,每次调用函数都会重新计算默认值 let x = 99; function foo(p = x + 1) { console.log(p); } foo(); //output: 100;
x = 100; foo(); //output: 101
与解构赋值默认值结合使用
函数参数的默认值可以 与解构赋值的默认值结合起来使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}); //output: undefined 5
foo({x: 1}); //output: 1 5
foo({x: 1, y: 2}); //output: 1 2
foo(); //eror
只有当foo的参数是一个对象时,变量x和y才会通过解构赋值生成,如果调用函数时没有提供参数,变量不会生成,从而报错。这个时候需要给函数的参数提供默认值
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo(); //output: undefined 5
上面的代码表示,如果调用foo时没有提供参数,函数foo的参数默认为一个空对象
下面是另外一个解构赋值默认值的例子
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('juejin.com', {}); //output: GET
fetch('juejin.com') //Uncaught TypeError: Cannot read properties of undefined (reading 'body')
函数fetch的第二个参数时一个对象,这时候调用函数fetch不能省略第二个参数,如果结合函数默认值,就可以省略第二个参数。这种也叫双重默认值
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('juejin.com'); //output: GET
在上述代码中,当调用fetch时,因为没有第二个参数,设置的函数参数默认值就会生效,然后解构赋值的默认值生效,mtthod取到默认值GET
参数默认值的位置
通常定义默认值的参数应该是函数的尾参数,这样可以容易看出来到底省略了哪些参数。如果设置的默认值是非尾部的参数,则参数无法省略,如下所示:
function func(x = 1, y) {
return [x, y];
}
func(); //output: 1,undefined
func(2); //output: 2, undefined
func( , 2); //error
func(undefined, 1); //output: [1, 1]
function foo(x, y = 5, z) {
return [x, y, z];
}
foo() //output: [undefined, 5, undefined]
foo(1) //output: [1, 5, undefined]
foo(1, , 2); // Uncaught SyntaxError: Unexpected token ','
foo(1, undefined, 2); // [1, 5, 2]
上面的代码中,有默认值的参数都不是尾参数,这时,无法只省略该参数,而不省略它后面的参数,除非显式地输入undefined,会触发参数的默认值,如下所示:
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null); //output: 5 null
函数的length属性
在指定了函数的length属性后,将返回没有指定默认值的参数的个数
(function(a) { }).length //output: 1
(function(a = 5) { }).length //output: 0
(function(a, b, c = 5) { } ).length // output: 2
作用域
在设置了函数参数的默认值后,函数进行初始化时,参数会形成一个单独的作用域(context),等到初始化结束后,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1; //全局的变量x
function foo(x, y = x) {
console.log(y);
}
foo(2); //output: 2
在foo函数中,参数y的默认值等于变量x,调用函数foo时,参数形成一个单独的作用域,在这个作用域中,默认值变量x指向第一个参数x而不是全局的变量x,所以输出是2
再来看下面这个函数
var x = 1; //全局变量x
function foo(y = x) { //默认值变量x
let x = 2; //局部变量x
console.log(y);
}
foo(). //output:1
在上面的代码中,函数foo调用时,参数y=x形成一个单独的作用域,在这个作用域中,默认值变量x没有被声明,所以向外层寻找一个全局变量x,如果此时全局变量x不存在,就会报错,如下面代码所示。在函数调用时,函数体内的声明的局部变量x不会影响默认值变量x。
function foo(y = x) {
let x = 2;
console.log(x);
}
f() //Uncaught ReferenceError: x is not defined
再来看一个例子
var x = 1;
function foo(x = x) {
console.log(x);
}
foo(); //Uncaught ReferenceError: Cannot access 'x' before initialization
上面的代码中,参数x=x形成一个单独的作用域,实际上执行的是 let x = x,由于存在暂时性死区(即在let 声明变量前,变量是不可用的),代码会报错
如果参数的默认值是一个函数,该函数的作用域也遵循这个原则
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // output: outer
在上面的代码中,函数bar的参数func的默认值是一个匿名函数,它返回值为变量foo。在函数参数形成的作用域中,没有定义变量foo,所以在外层的全局变量中寻找foo。
下面是一个更复杂的例子
var x = 1;
function foo(x, y = function() { x = 2; } ) {
var x = 3;
y();
console.log(x);
}
foo(); //output : 3
x // output: 1
在上面的代码中,函数foo的参数形成一个单独的作用域,在这个作用域里首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。在这个匿名函数内部的x指向同一个作用域下的foo的参数x。在foo函数内部又声明了一个内部变量x,该变量和第一个参数x不是在一个作用域下,互相不会影响。
如果将代码改变如下:
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo(); //output: 2
x // 1
在函数foo内部的变量x指向第一个参数x,与匿名函数内部的x是一个值,所以输出为2,而在最外层的全局变量x依然不受影响。
应用
利用参数默认值,可以指定某一个参数不得省略,省略则抛出一个错误
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeprovided = throwIfMissing()) {
return mustBeProvided;
}
foo(); //output: Missing parameter
上述代码的foo函数被调用时如果没有参数,会调用参数的默认值throwIfMissing函数抛出一个错误。另外如果将参数的默认值设置为undefined,则表明这个参数是可以省略的。
2. rest参数(剩余操作符)
ES6引入rest参数(形式为…变量名),用于获取函数的多余参数,这样就可以不使用arguments对象。rest参数搭配的变量是一个数组,将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var value of values) {
sum += value;
}
return sum;
}
add(2, 5, 3); //output: 10
rest 参数之后不能再有其他参数,否则会报错。
4. name属性
函数的name属性,返回该函数的函数名
function foo() { };
foo.name // output: foo
如果将一个匿名函数赋值给一个变量,ES5的name属性会返回空字符串,ES6的name属性会返回实际的函数名
var f = function() {};
//ES5
f.name // ""
//ES6
f.name // "f"
上面代码中,变量f相当于一个匿名函数。所以返回的值也不一样
如果给这个函数加一个名字,则不管是ES5还是ES6的name属性都将返回这个函数的名字。
Function构造函数返回的函数实例,name属性的值为anonymous(匿名的)
bind返回的函数,name属性会加上bound后缀
function foo() {};
foo.bind({}).name //output: bound name
(function() {}).bind({}).name // output: bound
5. 箭头函数
基本用法
ES6允许使用箭头 ⇒ 定义函数
vat f = v => v;
//等同于
var f = function(v) {
return v;
};
如果箭头函数不需要参数或者需要多个参数时,用一个圆括号代表参数部分
var f = () => 5;
//等同于
var f = function() { return 5 };
var sum = (num1, num2) => num1 + num2;
//等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多余一条语句,就要使用大括号将它们括起来,并使用return语句返回。
let sum = (a, b) => {
let res = a + b;
return res;
};
如果箭头函数直接返回一个对象,必须在对象外面加上圆括号,否则会报错
//报错
let getTempItem = id => { id: id, name: "Temp" };
//不报错
let getTempItem = id => ({ id: id, name: "Temp" });
箭头函数的用处:简化回调函数
[1, 2, 3].map(function(x) {
return x * x;
});
[1, 2, 3].map(x => x * x);
⚠️:箭头函数的特点
1、没有自己的this对象
2、不可以当作构造函数,即不可以对箭头函数使用new命令
3、不可以使用arguments对象
4、不可以使用yiedld命令,不能用作Generator函数
普通函数的this指向函数运行时所在的对象,但是对于箭头函数来说,this是指向定义时上层作用域中的this。如下所示:
function foo() {
setTimeout(() => {
console.log('id', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 }); //output: 42
上述代码setTimeout()的参数是一个箭头函数,这个箭头函数定义生效是在foo函数生成时,但是它真正执行在100毫秒以后。如果是普通函数,执行时的this应该指向全局对象window,但是箭头函数导致this总是指向函数定义生效时所在的对象(本例子中是{id: 42}),所以会输出42。
请看下面的代码,对比内部的this指向
function Timer() {
this.s1 = 0;
this.s2 = 0;
setInterval(() => this.s1++ ,1000);
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1', timer.s1), 3100); //output: 3
setTimeout(() => console.log('s2', timer.s2), 3100); //output: 0
上面的代码中,在Timer函数内部设置了两个定时器函数,第一个定时器的this指向函数定义时所在的作用域,即Timer函数,第二个定时器的this指向执行时所在的作用域(即全局对象),所以3100毫秒以后,s1更新了3次,s2没有被更新。
箭头函数可以让this指向固定化,绑定this使得它不在可变,这种特性常常用在封装回调函数。如下所示:
var header = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling' + type + 'for' + this.id);
}
};
在init方法中,使用了箭头函数,这会使得箭头函数的this总是指向handler对象。如果回调函数是普通对象,那么运行this.doSomething这一行时会报错,因为此时this指向的是document对象。
总之箭头函数没有自己的this,导致箭头函数的this就是外层代码块的this,正是因为它没有this,所以也就不能用作构造函数。
下面是Babel转箭头函数产生的ES5代码,可以清楚地说明this的指向
//ES6代码
function foo() {
setTimeout(() => {
console.log('id', this.id);
}, 100);
}
//转换为ES5代码
function foo() {
var _this = this;
setTimeout(() => {
console.log('id', _this.id);
}, 100);
}
上面的代码清楚的说明了箭头函数没有自己的this,而是引用外层的this