八、函数的拓展----

1. 函数参数的默认值

1.1 基本用法

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World  // 此处需要输出为 hello

上面代码检查函数log()的参数y有没有赋值,如果没有,则指定默认值为World

这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。

就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

function log(x, y) {
  if (typeof y === 'undefined') {
  	  y = 'World';
  }
  console.log(x, y);
}

ES6 允许为函数的参数设置默认值,即 直接写在参数定义的后面

// 默认值直接写在参数定义的后面
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

————
可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }

除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档

其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行
————
参数变量是 默认声明 的,所以不能用letconst再次声明,否则会报错。

function foo(x = 5) {
  let x = 1;  // error  都不能再次声明
  const x = 2; // error
}

————
使用参数默认值时,函数不能有同名参数。

// 不报错
function foo(x, x, y) {
  // ...
}

// 报错:使用参数默认值时,不能有同名参数
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

————
另外,一个容易忽略的地方是,参数默认值不是传值的,而是 每次都重新计算默认值表达式的值

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo(),都会重新计算x + 1,而不是默认p等于 100


1.2 与解构赋值结合使用

// 使用的是 对象解构赋值的默认值,不是函数参数的默认值
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({})

以上代码 只使用了 对象解构赋值的默认值,没有使用 函数参数的默认值

只有当函数foo()的参数是一个 对象 时,变量xy才会通过 解构赋值 生成。

如果函数 foo() 调用时没提供参数,变量xy就不会生成,从而报错。

通过提供 函数参数的默认值,就可以避免这种情况。

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5
      // 即 没有传参时,通过提供函数参数的默认值,来执行解构赋值的默认值,参考《2.2 对象解构默认值》

————
下面是另一个解构赋值默认值的例子。

function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// 报错

上面代码中,如果函数fetch()的第二个参数是一个对象,就可以为它的三个属性设置默认值。

这种写法不能省略第二个参数,如果结合 函数参数的默认值,就可以省略第二个参数。这时,就出现了 双重默认值

  • 即:在对函数没有传参时,通过提供函数参数的默认值,来执行解构赋值的默认值。
    参考《2.2 对象解构默认值》
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"
// 1、函数没有传参触发:【函数参数的默认值】
// 2、函【数参数的默认值】 触发后,触发 【解构赋值的默认值】

注意,函数参数的默认值生效以后,参数解构赋值依然会进行。

默认值生效的条件是,对象的属性值严格等于 undefined

function f({ a, b = 'world' } = { a: 'hello' }) {
  // 此时 b 为 undefined,触发解构赋值的默认值 b = 'world'
  console.log(b);
}

f() // world

————
作为练习,大家可以思考一下,下面两种函数写法有什么差别?

// 这两种写法在解构赋值中都有讲述到
// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined] // 直接替换掉 函数参数的默认值:{ x: 0, y: 0 }

// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

1.3 参数默认值的位置

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]  必须写 undefined

// 例二
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  // 传入null值不能触发默认值

1.5 函数的 length 属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

……


1.6 作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

var x = 1;

function f(x, y = x) {  // 这里面形成了作用域,变量查找机制。
  console.log(y);
}

f(2) // 2

在这个作用域里面,默认值变量 x 指向第一个参数 x,而不是 全局变量 x,所以输出是2

let x = 1;

function f(y = x) { // 实际执行的是 let y = x;
  let x = 2;
  console.log(y);
}

f() // 1
function f(y = x) {  // 形成单独的作用域,不会查找 { } 中的内容
  let x = 2;
  console.log(y);
}

f() // ReferenceError: x is not defined
// 报错
var x = 1;
function foo(x = x) {  // 实际执行的是 let x = x;
  // ...
}

foo() // ReferenceError: Cannot access 'x' before initialization

上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错。
————
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。

let foo = 'outer';

function bar(func = () => foo) {  // 匿名函数: 立即执行函数
  let foo = 'inner';
  console.log(func());
}

bar(); // outer

上面代码中,函数 bar 的参数 func 的默认值是一个匿名函数,返回值为变量 foo。函数参数形成的单独作用域里面,并没有定义变量 foo,所以 foo 指向外层的全局变量 foo,因此输出 outer

// 报错
function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}

bar() // ReferenceError: foo is not defined

————
下面是一个更复杂的例子。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1

上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量yy的默认值是一个匿名函数。

这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

如果将var x = 3的var去除,函数foo的内部变量 x就指向第一个参数x与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。

var x = 1;
function foo(x, y = function() { x = 2; }) {  
                    // 匿名函数中的 x 与 第一个x是同一个变量
  x = 3;  // x 向外查找,找到第一个x
  y();
  console.log(x);
}

foo() // 2
x // 1

2. rest 参数:(…变量名)

ES6 引入 rest 参数(形式为 ...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}

add(2, 5, 3) // 10

————
下面是一个 rest 参数代替 arguments 变量的例子。

// arguments变量的写法
function sortNumbers() {
  return Array.from(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

arguments 对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用 Array.from 先将其转为数组。

rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

————
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

// 报错
function f(a, ...b, c) {
  // ...
}

————
函数的 length 属性,不包括 rest 参数。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

3. 严格模式

3.1 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式。

从 ES5 开始,函数内部可以设定为严格模式。

function doSomething(a, b) {
  'use strict';
  // code
}

ES2016 做了一点修改,规定:只要 函数参数 使用了默认值、解构赋值、或者扩展运算符,那么函数内部 就不能显式设定为严格模式,否则会报错。

// 报错  使用了 "默认值"
function doSomething(a, b = a) {
  'use strict';
  // code
}

// 报错  使用了 "解构赋值"
const doSomething = function ({a, b}) {
  'use strict';
  // code
};

// 报错  使用了 "扩展运算符"
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 报错  使用了 "解构赋值"
  doSomething({a, b}) {
    'use strict';
    // code
  }
};

这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体

这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。所以这样规定。

// 报错
function doSomething(value = 070) {
  'use strict';
  return value;
}

上面代码中,参数value的默认值是八进制数070,但是严格模式下不能用前缀0表示八进制,所以应该报错。

但是实际上,JavaScript 引擎会 先成功执行 value = 070然后进入函数体内部发现需要用严格模式执行,这时才会报错。

虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

两种方法可以规避这种限制。第一种是 设定全局性的严格模式,这是合法的。

'use strict';

function doSomething(a, b = a) {
  // code
}

第二种是把函数包在一个 无参数的立即执行函数里面。

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

4. 函数的 name 属性(foo.name 返回函数的函数名)

函数的 name 属性,返回该函数的函数名。

function foo() {}
foo.name // "foo"

这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。

需要注意的是,ES6 对这个属性的行为做出了一些修改。如果 将一个匿名函数赋值给一个变量,ES5 的 name 属性,会返回空字符串,而 ES6 的 name 属性会 返回实际的函数名

// 将匿名函数赋给一个变量
var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

如果将一个 具名函数赋值给一个变量,则 ES5 和 ES6 的 name 属性都返回这个 具名函数原本的名字

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

————
Function 构造函数返回的函数实例,name 属性的值为 anonymous

(new Function).name // "anonymous"

bind 返回的函数,name 属性值会加上 bound 前缀。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

5. 箭头函数

5.1 箭头函数

ES6 允许使用 “箭头”(=>)定义函数。

var 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 语句返回。

var sum = (num1, num2) => { return num1 + num2; }

————
由于 大括号被解释为代码块,所以如果箭头函数 直接返回一个对象,必须 在对象外面加上括号()(不是数组 [] ),否则会报错。

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

下面是一种特殊情况,虽然可以运行,但会得到错误的结果。

let foo = () => { a: 1 };
foo() // undefined

上面代码中,原始意图是返回一个对象 { a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句 a: 1

这时,a 可以被解释为语句的标签,因此实际执行的语句是 1;,然后函数就结束了,没有返回值。
————
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();

————
箭头函数可以与变量解构结合使用。

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}
// 确实,因为{ first, last }解构赋值 取得了 person.first、person.first  属性

箭头函数使得表达更加简洁。

const isEven = n => n % 2 === 0;
const square = n => n * n;

————
箭头函数的一个用处是 简化回调函数

// 普通函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);
// 普通函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

rest 参数与箭头函数结合:

// nums 是一个数组
const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
                           // tail 是一个数组,放到一个数组里面
const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

5.2 使用注意点

箭头函数有几个使用注意点。
(1)箭头函数没有自己的 this 对象(详见下文)。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误。
(3)不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

上面四点中,最重要的是第一点。对于普通函数来说,内部的 this 指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的 this 对象,内部的 this 就是定义时上层作用域中的 this。也就是说,箭头函数内部的 this 指向是固定的,相比之下,普通函数的this指向是可变的。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代码中,setTimeout() 的参数是一个箭头函数,这个箭头函数的定义生效是在foo 函数生成时,而它的真正执行要等到 100 毫秒后。

如果是普通函数,执行时this 应该指向全局对象 window,这时应该输出21。但是,箭头函数导致 this 总是指向 函数定义生效时 所在的对象(本例是 {id: 42}),所以打印出来的是 42

下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的 this 指向。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数: this绑定定义时所在的作用域(Timer函数)
  setInterval(() => this.s1++, 1000);
  // 普通函数: this指向运行时所在的【对象】(即全局对象)
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

上面代码中,Timer 函数内部设置了两个定时器,分别使用了箭头函数和普通函数。
前者的this绑定 定义时所在的作用域 / 对象(即Timer函数),后者的this指向 运行时所在的作用域 / 对象(即全局对象)。所以,3100 毫秒之后,timer.s1 被更新了 3 次,而 timer.s2 一次都没更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值