跳至主要內容

04 【函数的扩展】

约 5015 字大约 17 分钟...

04 【函数的扩展】

1.函数参数的默认值

1.1 认识函数参数的默认值

调用函数的时候传参了,就用传递的参数;如果没传参,就用默认值

1.2 函数参数默认值的基本用法

// 之前的默认值实现方式
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

1.3 默认值的生效条件

不传参数,或者明确的传递 undefined 作为参数,只有这两种情况下,默认值才会生效。

注意:null 就是 null,不会使用默认值。

1.4 与解构赋值默认值结合使用

参数默认值可以与解构赋值的默认值,结合起来使用。

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()的参数是一个对象时,变量xy才会通过解构赋值生成。如果函数foo()调用时没提供参数,变量xy就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。

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的默认值生效。

1.5 参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

// 例一
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,就没有触发默认值。

1.6 函数参数默认值的应用

接收很多参数的时候

// 普通时候
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) { ··· }

2.rest 参数

2.1 前言

剩余语法(Rest syntax 也可以叫剩余参数)看起来和展开语法完全相同都是使用 ... 的语法糖,不同之处在于剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来成为一个整体。

2.2 函数参数

在讲解剩余参数前,我们先来看看,剩余参数在函数参数中都解决了哪些问题?为什么会引入剩余参数的概念?

在 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 对象有了一定的了解。

image-20220826180356855
image-20220826180356855

在 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 对象不具备数组的方法,所以很多时候在使用之前要先转换成一个数组。而剩余参数本来就是一个数组,避免了这多余的一步,使用起来既优雅又自然。

2.3 注意事项

箭头函数的剩余参数

箭头函数的参数部分即使只有一个剩余参数,也不能省略圆括号。

const add = (...args) => {};

使用剩余参数替代 arguments 获取实际参数

  • 剩余参数是一个 “真数组”,arguments 是一个 “伪数组”
  • 剩余参数的名字可以自定义

剩余参数的位置

剩余参数只能是最后一个参数,之后不能再有其他参数,否则会报错。

2.4 剩余参数的应用

作为数组的应用:

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 会去解构传入参数,加上有剩余语法的作用,对应的值从数组中的项解构出来,在函数内部直接使用解构出来的参数即可。剩余语法看起来和展开语法完全相同,不同点在于,剩余参数用于解构数组和对象。

2.5 小结

本节结合了 ES5 函数中的 arguments 对象引入了为什么 ES6 会引入剩余参数的概念,可以看到剩余参数所带来的好处。本节内容可以总结以下几点:

  1. 剩余参数是为了能替代函数内部的 arguments 而引入的;
  2. 和展开语法相反,剩余参数是将多个单个元素聚集起来形成一个单独的个体的过程。

3.箭头函数

3.1 前言

在编程中使用最多的就是函数,在 ES5 中是用 function 关键字来定义函数的,由于历史原因 function 定义的函数存在一些问题,如 this 的指向、函数参数 arguments 等。

ES6 规定了可以使用 “箭头” => 来定义一个函数,语法更加简洁。它没有自己的 thisargumentssupernew.target,箭头函数表达式更适用于那些本来需要匿名函数的地方,但它不能用作构造函数。

3.2 认识箭头函数

普通函数:

  • function 函数名() {}
  • const 变量名 = function () {};

箭头函数:

  • 参数 => 函数体

  • () => {}

由于箭头函数是匿名函数,所以我们通常把它赋给一个变量

const add = (x, y) => {
    return x + y;
};

console.log(add(1, 1));        // 2

3.3 箭头函数注意事项

3.3.1 省略写法

const add = (x) => {
    return x + 1;
};

// 单个参数可以省略 ()
const add = x => {
    return x + 1;
};

// 无参数
const test = () => {
    return 1;
};
//或者
const test = _ => {
    return 1;
};

3.3.2 单行函数体

const add = (x, y) => {
    return x + y;
};

// 单行函数体可以省略 return 和 {},且一但省略就 return 和 {} 都要一起省略
const add = (x, y) => x + y; 

3.3.3 单行对象

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];

推荐:一般情况最好不要简写!

3.4 非箭头函数中的 this 指向

3.4.1 全局作用域中的 this 指向

console.log(this);
// window

3.4.2 一般函数(非箭头函数)中的 this 指向

只有在函数调用的时候 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)

3.5 箭头函数没有 this

在 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。

3.6 不适用箭头函数的场景

3.6.1 作为构造函数

因为箭头函数没有 this,而构造函数的核心就是 this。

3.6.2 需要 this 指向调用对象的时候

因为箭头函数没有 this,所以如果箭头函数中出现了 this,那么这个 this 就是外层的!

3.6.3 需要使用 arguments 的时候

箭头函数没有 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]

3.7 其他注意点

3.7.1 不能用作构造器

箭头函数不能用作构造器,和 new 一起用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

3.7.2 没有 prototype 属性

箭头函数没有 prototype 属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

3.7.3 不能使用 yield 命令

yield 关键字通常不能在箭头函数中使用,因此箭头函数不能用作 Generator 函数。

3.8 小结

本节主要讲解了 ES6 的箭头函数,总结了以下几点:

  • 更短的函数,优雅简洁;
  • 箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this;
  • 不能绑定 arguments, 只能使用 ...args 展开运算来获取当前参数的数组。

4.函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。

function clownsEverywhere(
  param1,
  param2
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar'
);

上面代码中,如果在param2bar后面加一个逗号,就会报错。

如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

5.catch 命令的参数省略

JavaScript 语言的try...catch结构,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。

try {
  // ...
} catch (err) {
  // 处理错误
}

上面代码中,catch命令后面带有参数err

很多时候,catch代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。ES2019做出了改变,允许catch语句省略参数。

try {
  // ...
} catch {
  // ...
}
已到达文章底部,欢迎留言、表情互动~
  • 赞一个
    0
    赞一个
  • 支持下
    0
    支持下
  • 有点酷
    0
    有点酷
  • 啥玩意
    0
    啥玩意
  • 看不懂
    0
    看不懂
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8