调用函数的时候传参了,就用传递的参数;如果没传参,就用默认值
// 之前的默认值实现方式
const multiply = (x, y) => {
if (typeof y === 'undefined') {
y = 3;
}
return x * y;
};
console.log(multiply(2, 2)); // 4
console.log(multiply(2)); // 6
// ES6 默认值实现方式
const multiply = (x, y = 3) => {
return x * y;
};
console.log(multiply(2, 2)); // 4
console.log(multiply(2)); // 6
不传参数,或者明确的传递 undefined 作为参数,只有这两种情况下,默认值才会生效。
注意:null 就是 null,不会使用默认值。
参数默认值可以与解构赋值的默认值,结合起来使用。
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo()
的参数是一个对象时,变量x
和y
才会通过解构赋值生成。如果函数foo()
调用时没提供参数,变量x
和y
就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
上面代码指定,如果没有提供参数,函数foo
的参数默认为一个空对象。
下面是另一个解构赋值默认值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
上面代码中,如果函数fetch()
的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
上面代码中,函数fetch
没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method
才会取到默认值GET
。
注意,函数参数的默认值生效以后,参数解构赋值依然会进行。
function f({ a, b = 'world' } = { a: 'hello' }) {
console.log(b);
}
f() // world
上面示例中,函数f()
调用时没有参数,所以参数默认值{ a: 'hello' }
生效,然后再对这个默认值进行解构赋值,从而触发参数变量b
的默认值生效。
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
上面代码中,x
参数对应undefined
,结果触发了默认值,y
参数等于null
,就没有触发默认值。
接收很多参数的时候
// 普通时候
const logUser = (username = 'zjr', age = 18, sex = 'male') => {
console.log(username, age, sex);
};
// 需要能够记住参数的顺序,如果参数较多那么需要配合文档,使用不方便
logUser('jerry', 18, 'male');
// ------------------------------------------------------------
// 接收一个对象作为参数
// 不需要记住参数的顺序
const logUser = options => {
console.log(options.username, options.age, options.sex);
};
logUser({
username: 'jerry',
age: 18,
sex: 'male'
});
// ------------------------------------------------------------
// 再优化
const logUser = ({username, age, sex}) => {
console.log(username, age, sex);
};
logUser({
username: 'jerry',
age: 18,
sex: 'male'
});
// ------------------------------------------------------------
// 引入默认值
const logUser = ({
username = 'zjr',
age = 18,
sex = 'male'
}) => {
console.log(username, age, sex);
};
// 其实是解构赋值原理
logUser({username: 'jerry'}); // jerry 18 male
logUser({}); // zjr 18 male
logUser(); // 报错,因为这样相当于传了一个 undefined,不符合解构赋值
// ------------------------------------------------------------
// 再优化(函数默认值 + 解构赋值 + 解构赋值默认值)
const logUser = ({
username = 'zjr',
age = 18,
sex = 'male'
} = {}) => {
console.log(username, age, sex);
};
logUser(); // zjr 18 male
/*
解释:
1、options 与 {username = 'zjr', age = 18, sex = 'male'} 互等
2、{username = 'zjr', age = 18, sex = 'male'} = {} 其实就是 options = {}
3、由于 logUser() 的实参为 undefined,所以默认值为 {}
4、再因为 {username = 'zjr', age = 18, sex = 'male'} = {} 是解构赋值
5、由于 {} 内为 undefined,所以解构赋值启用默认值
5、所以真正的形参为 {username = 'zjr', age = 18, sex = 'male'}
注明:这样做的好处是增加函数的健壮性!
*/
某一个参数不得省略,如果省略就抛出一个错误
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
上面代码的foo
函数,如果调用的时候没有参数,就会调用默认值throwIfMissing
函数,从而抛出一个错误。
从上面代码还可以看到,参数mustBeProvided
的默认值等于throwIfMissing
函数的运行结果(注意函数名throwIfMissing
之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
另外,可以将参数默认值设为undefined
,表明这个参数是可以省略的。
function foo(optional = undefined) { ··· }
剩余语法(Rest syntax 也可以叫剩余参数)看起来和展开语法完全相同都是使用 ...
的语法糖,不同之处在于剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来成为一个整体。
在讲解剩余参数前,我们先来看看,剩余参数在函数参数中都解决了哪些问题?为什么会引入剩余参数的概念?
在 ES5 中,函数经常会传入不定参数,在传入不定参数时,ES5 的给出的解决方案是通过 arguments
对象来获取函数调用时传递的参数。 arguments
对象不是一个数组,它是一个类数组对象,所谓类数组对象,就是指可以通过索引属性访问元素并且拥有 length 属性的对象。
一个简单的类数组对象是长这样的:
var arrLike = {
0: 'name',
1: 'age',
2: 'job',
length: 3
}
而它所对应的数组应该是这样子的:
var arr = ['name', 'age', 'job'];
这里我们说类数组对象与数组的性质相似,是因为类数组对象在访问、赋值、获取长度上的操作与数组是一致的,具体内容可查阅相关的类数组使用。
在函数体中定义了 Arguments 对象,其包含函数的参数和其它属性,以 arguments
变量来指代。下面我们看个实例:
function fn() {
console.log(arguments);
}
fn('imooc', 7, 'ES6')
在控制台中打印出上面的代码结果,如下图所示:在定义函数的时候没有给定参数,但是通过 arguments
对象可以拿到传入的参数。可以看到 arguments
中包含了函数传递的参数、length 等属性,length 属性表示的是实参的长度,即调用函数的时候传入的参数个数。这样我们就对 arguments
对象有了一定的了解。
在 ES5 的开发模式下,想要使用传递的参数,则需要按位置把对应的参数取出来。尽管 arguments
是一个类数组且可遍历的变量,但它终究不是数组,它不支持数组方法,因此我们不能调用 arguments.forEeach (…)
等数组的方法。需要使用一些特殊的方法转换成数组使用,如:
function fn() {
var arr = [].slice.call(arguments);
console.log(arr)
}
fn('ES6');
// ["ES6"]
fn('imooc', 7, 'ES6');
// ["imooc", 7, "ES6"]
终于借助 call
方法把 arguments
转化成一个真正的数组了。但是这样无疑是一个繁琐的过程,而且不容易理解。这时 ES6 给出了它的完美解决方案 —— 剩余参数,那剩余参数是如何在函数传参中使用的呢?下面我们来看看实例:
语法:const add = (x, y, z, ...args) => {};
function fn(...args) {
console.log(args)
}
fn('ES6');
// ["ES6"]
fn('imooc', 7, 'ES6');
// ["imooc", 7, "ES6"]
使用方式很简单在函数定义时使用 ...
紧接着跟一个收集的参数,这个收集的参数就是我们所传入不定参数的集合 —— 也就是数组。这样就很简单地摆脱了 arguments 的束缚。另外,还可以指定一个默认的参数,如下示例:
function fn(name, ...args) {
console.log(name); // 基础参数
console.log(args); // 剩下的参数组成的数组
}
fn('ES6');
// 'ES6'
// []
fn('imooc', 7, 'ES6');
// "imooc"
// [7, "ES6"]
上面的代码中给函数第一个参数,声明一个变量 name,剩余的参数会被 ...
收集成一个数组,这就是剩余参数。引入剩余参数就是为了能替代函数内部的 arguments
,由于 arguments
对象不具备数组的方法,所以很多时候在使用之前要先转换成一个数组。而剩余参数本来就是一个数组,避免了这多余的一步,使用起来既优雅又自然。
箭头函数的剩余参数
箭头函数的参数部分即使只有一个剩余参数,也不能省略圆括号。
const add = (...args) => {};
使用剩余参数替代 arguments 获取实际参数
剩余参数的位置
剩余参数只能是最后一个参数,之后不能再有其他参数,否则会报错。
作为数组的应用:
const add = (...args) => {
let sum = 0;
for (let i = 0; i < args.length; i++) {
sum += args[i];
} // 当然此处,arguments 也可以
return sum;
};
console.log(add()); // 0
console.log(add(1, 1)); // 2
console.log(add(1, 2, 3)); // 6
与解构赋值结合使用:
(剩余参数不一定非要作为函数参数使用)
let array = [1, 2, 3, 4, 5];
let [a, b, ...others] = array;
console.log(a); // 1
console.log(b); // 2
console.log(others); // [3,4,5]
const {x, y, ...z} = {a: 3, x: 1, y: 2, b: 4};
console.log(x, y, z);
// 1 2 { a: 3, b: 4 }
// 这里的剩余参数是个对象(准确的应该叫:剩余元素)
const func = ({x, y, ...z}) => {
console.log(x, y, z); // 1 2 { a: 3, b: 4 }
};
func({a: 3, x: 1, y: 2, b: 4});
function fun(...[a, b, c]) {
return a + b + c;
}
fun('1') // NaN (b 和 c 都是 undefined)
fun(1, 2, 3) // 6
fun(1, 2, 3, 4) // 6 多余的参数不会被获取到
上面的代码中,a、b、c 会去解构传入参数,加上有剩余语法的作用,对应的值从数组中的项解构出来,在函数内部直接使用解构出来的参数即可。剩余语法看起来和展开语法完全相同,不同点在于,剩余参数用于解构数组和对象。
本节结合了 ES5 函数中的 arguments
对象引入了为什么 ES6 会引入剩余参数的概念,可以看到剩余参数所带来的好处。本节内容可以总结以下几点:
在编程中使用最多的就是函数,在 ES5 中是用 function
关键字来定义函数的,由于历史原因 function
定义的函数存在一些问题,如 this
的指向、函数参数 arguments
等。
ES6 规定了可以使用 “箭头” =>
来定义一个函数,语法更加简洁。它没有自己的 this
、arguments
、super
或 new.target
,箭头函数表达式更适用于那些本来需要匿名函数的地方,但它不能用作构造函数。
普通函数:
function 函数名() {}
const 变量名 = function () {};
箭头函数:
参数 => 函数体
() => {}
由于箭头函数是匿名函数,所以我们通常把它赋给一个变量
const add = (x, y) => {
return x + y;
};
console.log(add(1, 1)); // 2
const add = (x) => {
return x + 1;
};
// 单个参数可以省略 ()
const add = x => {
return x + 1;
};
// 无参数
const test = () => {
return 1;
};
//或者
const test = _ => {
return 1;
};
const add = (x, y) => {
return x + y;
};
// 单行函数体可以省略 return 和 {},且一但省略就 return 和 {} 都要一起省略
const add = (x, y) => x + y;
const add = (x, y) => {
return {
value: x + y
};
};
// const add = (x, y) => {value: x + y}; 报错!因为 {} 会产生歧义!
// () 可以将语句变为表达式,从而 {} 就可以被顺理成章解释为对象
const add = (x, y) => ({value: x + y});
// 数组就没有以上问题
const add = (x, y) => [x, y];
推荐:一般情况最好不要简写!
console.log(this);
// window
只有在函数调用的时候 this 指向才能确定,不调用的时候,不知道指向谁。
this 指向和函数在哪儿没有关系,只和谁在调用有关。
function add() {
console.log(this);
}
add(); // window
// 在非严格模式下,this 其实是先指向 undefined,然后被自动转为了 window
'use strict' // 严格模式
function add() {
console.log(this);
}
add(); // undefined
// 在严格模式下,this 为 undefined
'use strict' // 严格模式
function add() {
console.log(this);
}
const calc = {
add: add
};
calc.add(); // 上下文 this 为 calc
const adder = calc.add;
adder(); // 指向 undefined(非严格模式下指向 window)
在 JavaScript 中,要说让人最头疼的知识点中,this 绑定绝对算一个,这是因为 this 的绑定 ‘难以捉摸’,出错的时候还往往不知道为什么,相当反逻辑。下面我们来看一个示例:
var title = "全局标题";
var imooc = {
title: "慕课网 ES6 Wiki",
getTitle : function(){
console.log(this.title);
}
};
imooc.getTitle(); // 慕课网 ES6 Wiki
var bar = imooc.getTitle;
bar(); // 全局标题
通过上面的小例子的打印结果可以看出 this
的问题,说明 this
的指向是不固定的。
这里简单说明一下 this
的指向,this
指向的是调用它的对象。例子中的 this
是在 getTitle 的函数中的,执行 imooc.getTitle()
这个方法时,调用它的对象是 imooc
,所以 this 的指向是 imooc
。
之后把 imooc.getTitle
方法赋给 bar
,这里要注意的是,只是把地址赋值给了 bar
,并没有调用。 而 bar
是全局对象 window
下的方法,所以在执行 bar
方法时,调用它的是 Window 对象,所以这里打印的结果是 window 下的 title——“全局标题”。
TIPS: 上面的示例只是简单的
this
指向问题,还有很多更加复杂的,在面试中经常会被问到,所以还不清楚的同学可以去研究一下this
的问题。
ES6 为了规避这样的问题,提出了箭头函数的解决方案,在箭头函数中没有自己的 this
指向,所有的 this 指向都指向它的上一层 this
,这样规定就比较容易理解了。下面看使用箭头函数下的 this
指向:
var title = "全局标题";
var imooc = {
title: "慕课网 ES6 Wiki",
getTitle : () => {
console.log(this.title);
}
};
imooc.getTitle(); // 全局标题
var bar = imooc.getTitle;
bar(); // 全局标题
上面的打印结果可以看出来,所有的 this
指向都指向了 window 对象下的 title,本身的 imooc 对象下没有了 this
,它的上一层就是 window。
因为箭头函数没有 this,而构造函数的核心就是 this。
因为箭头函数没有 this,所以如果箭头函数中出现了 this,那么这个 this 就是外层的!
箭头函数没有 arguments。
(这个问题有替代解决方案:剩余参数)
var fun = function() {
console.log(arguments)
};
fun(1,2,3); // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
var fun = () => {
console.log(arguments)
};
fun(1,2,3); // Uncaught ReferenceError: arguments is not defined
上面的示例中,对比两种定义函数的方法可以明显的看出,在箭头函数中去取 arguments
时会报引用错误,没有定义的 arguments
。
arguments
的主要作用是获取所有调用函数时所需要传入的参数,在箭头函数中使用剩余参数 ...args
,在函数内可以直接使用。
function foo(...args) {
console.log(args)
}
foo(1); // [1]
foo(1, 2, 3); // [1, 2, 3]
箭头函数不能用作构造器,和 new 一起用会抛出错误。
var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor
箭头函数没有 prototype 属性。
var Foo = () => {};
console.log(Foo.prototype); // undefined
yield 关键字通常不能在箭头函数中使用,因此箭头函数不能用作 Generator 函数。
本节主要讲解了 ES6 的箭头函数,总结了以下几点:
...args
展开运算来获取当前参数的数组。ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。
此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。
function clownsEverywhere(
param1,
param2
) { /* ... */ }
clownsEverywhere(
'foo',
'bar'
);
上面代码中,如果在param2
或bar
后面加一个逗号,就会报错。
如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere
添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。
JavaScript 语言的try...catch
结构,以前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。
try {
// ...
} catch (err) {
// 处理错误
}
上面代码中,catch
命令后面带有参数err
。
很多时候,catch
代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。ES2019做出了改变,允许catch
语句省略参数。
try {
// ...
} catch {
// ...
}