第5集丨ES6 江湖 —— 函数扩展

一、箭头函数

1.1 基本用法

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

  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回。
  • 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
//例1
var f = v => v;
	// 等同于
	var f = function (v) {
	  return v;
	};

//例2
var f = () => 5;
	// 等同于
	var f = function () { return 5 };

//例3
var sum = (num1, num2) => { 
	let result = num1 + num2; 
	return result
}

	// 等同于
	var sum = function(num1, num2) {
	  let result = num1 + num2; 
	  return num1 + num2;
	};

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

1.2 与变量解构结合使用

const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

1.3 表达更加简洁

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

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

1.4 rest 参数与箭头函数结合

const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

1.5 注意点

箭头函数有几个使用注意点。

(1)函数体内的 this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用 yield命令,因此箭头函数不能用作 Generator 函数。

1.6 this对象

上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的.

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

所以,箭头函数转成 ES5 的代码如下。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}
// ES5
function foo() {
  var _this = this;
  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的 this ,而是引用外层的 this

1.6.1 利于封装回调函数

箭头函数可以让 this 指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

var handler = {
  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 对象。

1.6.2 练习

请问下面的代码之中有几个 this

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个 this ,就是函数 foothis ,所以 t1t2t3 都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的 this ,它们的 this 其实都是最外层 foo 函数的 this

1.7 arguments 、 super、 new.target

除了 this ,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量: arguments supernew.target

function foo() {
  setTimeout(() => {
    console.log('args:', arguments);
  }, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]

上面代码中,箭头函数内部的变量 arguments ,其实是函数 fooarguments 变量。

1.8 call() 、 apply() 、 bind()

另外,由于箭头函数没有自己的 this ,所以当然也就不能用 call() 、 apply() 、 bind() 这些方法去改变 this 的指向。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']

上面代码中,箭头函数没有自己的 this ,所以 bind 方法无效,内部的this指向外部的 this

二、rest 参数

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

2.1 rest 参数代替 arguments

下面是一个 rest 参数代替 arguments 变量的例子。arguments 对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用 Array.prototype.slice.call 先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

2.2 rest 参数改写数组 push

下面是一个利用 rest 参数改写数组 push 方法的例子。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}
var a = [];
push(a, 1, 2, 3)

2.3 rest 参数之后不能再有其他参数

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

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

2.4 函数length 属性不包括 rest 参数

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

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

三、函数name 属性

函数的name属性,返回该函数的函数名。这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。

3.1 匿名函数

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

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

var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"

3.2 具名函数

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

const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"

3.3 构造函数

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

(new Function).name // "anonymous"

3.4 bind的函数

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

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

四、函数参数的默认值

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

这种写法的缺点在于,如果参数 y 赋值了,但是对应的布尔值为 false ,则该赋值不起作用。就像上面代码的最后一行,参数 y 等于空字符,结果被改为默认值。

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

if (typeof y === 'undefined') {
  y = 'World';
}

4.1 基本用法

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

function log(x, y = 'World') {
  console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

4.2 不能用 let 或 const 再次声明

参数变量是默认声明的,所以不能用 let 或 const 再次声明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

4.3 不能有同名参数

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

// 不报错
function foo(x, x, y) {
  // ...
}
// 报错
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

4.4 参数默认值是惰性求值

一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

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

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

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

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

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 调用时没提供参数,变量 xy 就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。

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

4.6 参数默认值的位置

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

// 例一
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

4.7 练习

作为练习,请问下面两种写法有什么差别?

// 写法一
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 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

五、函数的 length 属性

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后, length 属性将失真。

length 属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入 length 属性。

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

如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了。

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

六、函数参数作用域

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

6.1 情况一

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

上面代码中,参数 y 的默认值等于变量 x 。调用函数 f 时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 x 指向第一个参数 x ,而不是全局变量 x ,所以输出是 2

6.2 情况二

let x = 1;
function f(y = x) {
  let x = 2;
  console.log(y);
}
f() // 1

上面代码中,函数 f 调用时,参数 y = x 形成一个单独的作用域。这个作用域里面,变量 x 本身没有定义,所以指向外层的全局变量 x 。函数调用时,函数体内部的局部变量 x 影响不到默认值变量 x

6.3 情况三

如果此时,全局变量 x 不存在,就会报错。

function f(y = x) {
  let x = 2;
  console.log(y);
}
f() // ReferenceError: x is not defined

下面这样写,也会报错。

var x = 1;
function foo(x = x) {
  // ...
}
foo() // ReferenceError: x is not defined

上面代码中,参数 x = x 形成一个单独作用域。实际执行的是 let x = x ,由于暂时性死区的原因,这行代码会报错”x 未定义“。

6.4 情况四

如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。

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

6.5 案例

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

var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}
foo() // 2
x // 1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值