学习阮一峰老师ES6系列(函数的扩展)

59 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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 

⚠️注意点:

  1. 函数的参数是默认声明的,不能在函数体内用let或者const再次声明 如下所示: function foo(x = 5) { let x = 1; //不可以再次声明

    const x = 2; //不可以再次声明 }

  2. 声明函数参数的默认值时,不能有同名的参数

  3. 函数参数的默认值有表达式时,每次调用函数都会重新计算默认值 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 namefunction() {}).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