《ES6标准入门》阅读笔记——-函数的扩展

写在前面

本系列为在我读完阮一峰老师的《ES6标准入门》第二版之后,所做的阅读笔记的整理。

许多初学ES6, 同时和我一样初次阅读阮老师的这本书的时候, 读第一遍会越发的困惑, 因为阮老师上面说的很多的定义,方法,之前都是没有见过的, 读到后面才发现, 哦~原来是这样.

有一句话叫做”大神的世界我们不懂”, 所以我在初读第一遍《ES6标准入门》这本书的时候,也是踩了不少坑,读书的时候查阅了不少的资料.

所以,在这一系列的笔记教程中,我会从一个初学者的角度,向您讲述ES6的相关知识,在后面介绍的知识我会尽量不提前用,即使提前使用,也会同时做好标注,避免了阅读时各种查阅资料的烦恼.

系列博客将采用一个一个的样例,来说明书中的精华部分(当然,这只是我认为的),同时引导新手,快速入门ES6,并逐步将其投入到生产实践中。

同时,在阅读前也提醒您, 为了系统连贯性的学习ES6的基础知识,建议您从我的博客第一章开始阅读,当然,如果您对对应的知识已经有所了解,那么可以跳转到任意章节阅读,每一篇博客名中均有介绍该博客中涉及到的ES6的内容.

阮一峰老师的这本书是开源的,在其官方博客就可以下载到,但是我强烈建议大家去购买一本书, 一是方便自己查阅ES6中新增的众多API, 二也是表达一下对大神的敬仰.

函数的默认值

在ES6之前,函数的默认值只能在函数体内进行判断

1
2
3
4
5
6
7
8
9
function log(x, y) {
y = y || "world"
console.log(x, y)
}
log("hello ") // hello world
// 当我们采用ES5的写法时,是存在问题的,当我们给第二个参数传入布尔值为false的值的时候,实际上并不会使用这个值而是仍然使用默认值.
log("hello ", false) // hello world
log("hello ", "") // hello world
log("hello ", undefined) // hello world

在ES6中就不会有这个烦恼了,ES6允许将参数设置默认值,直接写在参数定义的后面即可

1
2
3
4
5
6
7
function log(x, y = "world") {
console.log(x, y)
}
log("hello ") // hello world
log("hello ", false) // hello false
log("hello ", "") // hello
log("hello ", undefined) // hello world

由此可见,ES6的写法比之前更加自然,也更加的语义化了.

1
2
3
4
5
6
7
8
9
function Point(x = 0, y = 0) {
this.x = x
this.y = y
}
var p1 = new Point()
console.log(p1.x, p1.x) // 0 0

var p2 = new Point(2)
console.log(p2.x, p2.y) // 2 0

因为变量的参数是默认声明的,所以我们在内部重新使用let或const对变量声明的时候,是会报错的

1
2
3
4
5
6
function foo(x = 5) {
let x = 1 // 报错
const x = 1 // 报错
}

## 函数的默认值与解构赋值默认值结合使用

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

1
2
3
4

在为函数使用解构赋值时,因为有必填的参数,所以这个情况下必须要填需要的参数,否则会报错.

但是当我们在这种情况下,如果不想让它报错的话,可以使用双重解赋值的方法解决此问题

function fetch(url, {method = “GET”} = {}) {
console.log(method)
}
fetch(“http://example.com") // GET

1
2

## 解构赋值的默认值和函数默认值的区别

function m1({x = 0, y = 0} = {}) {
return [x, y]
}

1
2
3
4

以上代码将函数参数默认值设置为空对象, 但设置了对象解构赋值的默认值

我们在来看一下下面的代码

function m2({x, y} = {x: 0, y: 0}) {
return [x, y]
}

1
2

在函数都有参数或都没有参数的情况下,两者是相等的

m1() // [0, 0]
m2() // [0, 0]
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

1
2

但是在当一个有值,一个没有值的情况下,区别就显现出来了

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

1
2

当我们传入的是不同的对象的时候, m1的默认值为0, 所以, 会输出0, 而因为输入的对象与解构赋值的默认值不匹配,所以会输出undefined

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

1
2

当我们将非尾部的参数设置了默认值,那么这个时候这个参数是没有办法省略的

function f(x = 1, y) {
return [x, y]
}
console.log(f()) // [1, undefined]
console.log(f(1)) // [1, undefined]
console.log(f(, 1)) // 报错

1
2
3
4
5
6
7
8

## 参数默认值的位置

通常情况下,定义了默认值的参数应该是函数的尾参数.

因为这样比较容易看出到底省略了哪些参数,如果非尾部的参数设置默认值实际上这个参数是无法省略的.

换句话说,就是当中间设置了默认值,如果要给其后的非指定默认值参数设置值的话,必须要给这个参数设置值,或者显式的设置undefined或null, 否则会报错

function f(x, y = 5, z) {
return [x, y, z]
}
console.log(f()) // [undefined, 5, undefined]
console.log(f(1)) // [1, 5, undefined]
console.log(f(1, undefined, 3)) // [1, 5, 3]
console.log(f(1, , 2)) // 报错

1
2
3
4
5
6

## 函数的length属性

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

同时,经过测试,当我们设置默认值在前时,其实显示的形参个数是设置默认值的形参前面的形参个数,后面的形参个数是不计入在内的

console.log((function(x) {}).length) // 1
console.log((function(x = 90, a, b) {}).length) // 0
console.log((function(x, a = 90, b) {}).length) // 1
console.log((function(x, a, b = 90) {}).length) // 2

1
2

同样的,因为rest参数,也就是俗称的三点运算符的特殊性,其长度是不会计入length属性的

console.log((function(…args){}).length) // 0

1
2
3
4

## 函数的作用域

一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域与其他的变量的作用域是一样的,即先是当前函数的作用域,然后才是全局作用域

let x = 1

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

1
2
3
4
5
6

上面的代码中,参数的默认值等于x. 调用时, 由于函数作用域内部的变量x已经生成, 所以y等于参数x而不是全局变量x

但当调用的时候函数作用域内部的变量x没有生成,那么x就会指向全局变量,因为全局变量中没有这个参数,所以y依然指向自身

另外说明一下,这个和阮一峰老师的书中结果是不一致的,经过在chrome59版本下测试,结果为1

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

1
2
3
4

## 函数的参数默认值的小应用

引用参数默认值,可以指定某一个参数不得省略,否则会抛出一个错误

throwIfMissing = () => {
throw new Error(‘Missing parameter’)
}

function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided
}

foo() // Error: Missing parameter

1
2
3
4

# rest参数(三点运算符)

在ES6中引入了rest参数, 用于获取函数的多余参数,这样就不需要使用arguments对象了,rest参数搭配的变量是一个数组,该变量将多余的参数放入其中

function add(…values) {
let sum = 0
console.log(values) // [2, 3, 4]
for (var val of values) {
sum += val
}
return sum
}
console.log(add(2, 3, 4)) // 9

1
2

下面是rest参数代替arguments变量的例子

//在ES5中
const shortNumbers = () => {
return Array.prototype.slice.call(arguments).sort()
}

1
2

// 使用rest参数无疑让写法更加简洁自然了

const sortNumbers = (…numbers) => {
return numbers.sort()
}

使用rest参数的特性,可以改写push方法

1
2
3
4
5
6
7
8
function push(array, ...items) {
items.forEach(function(item) {
array.push(item)
})
return array
}
console.log(push([1, 2, 3], 1, 2, 3, 4, 54))
// [1, 2, 3, 1, 2, 3, 4, 54]

扩展运算符 …

扩展运算符好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列

1
2
3
4
5
6
7
8
console.log(...[1, 2, 3]) // 1 2 3
console.log(1, ...[2, [[3], 4]], 5) // 1, 2, [[3], 4]], 5

## 扩展运算符的应用

### 替代数组的apply方法

在ES5之前,我们如果需要将数组的值作为函数的参数逐一传进函数的时候,最简便的方法就是使用apply方法

function f(x, y, z) {}
var args = [1, 2, 3]
f.apply(null, args)

1
2

在ES6中,写法是这样的

function f2(x, y, z) {}
var args = [0, 1, 2]
f2(…args)

1
2
3
4

### 替代Math.max方法

当我们需要对数组进行最大最小值的判断的时候,在ES5中,用法是这样的

var a = [1, 2, 4, 12, 3, 5, 1, 12]
console.log(Math.max.apply(null, a)) // 12

1
2

但是在ES6中,我们就不必要这么麻烦了,因为扩展运算符 ... 可以将数组直接转换为普通的单个数字

var a = [1, 2, 4, 12, 3, 5, 1, 12]
console.log(Math.max(…a)) // 12

1
2
3
4

### 替代push方法

同样的,在ES5中,如果我们需要使用push方法将两个数组连接起来的时候,我们需要这样做

var a = [1, 2, 3]
var b = [4, 5, 6]
Array.prototype.push.apply(a, b)
console.log(a) // [1, 2, 3, 4, 5, 6]
console.log(b) // [4, 5, 6]

1
2
3
4

因为最后需要得到扁平化的数组,所以并不能直接使用`a.push(b)`这样的方法,这样会使得到的变成多维数组

同样的,ES6还提供了数组合并的新写法:

var a = [1, 2, 3]
var b = [4, 5, 6]
a.push(…b)
console.log(a) // [1, 2, 3, 4, 5, 6]
console.log(b) // [4, 5, 6]

1
2

### 替代数组合并方法

var a = [1, 2, 3]
var b = [4, 5, 6]
var c = a.concat(b)
console.log(c) // [1, 2, 3, 4, 5, 6]

1
2

在ES6中,因为有了 ... 运算符,那么其实concat是可以省略的

var a = [1, 2, 3]
var b = [4, 5, 6]
var d = […a, …b]
console.log(d) // [1, 2, 3, 4, 5, 6]

1
2
3
4

### 与解构赋值结合起来,用于生成数组

那么,我们先来看一下,在ES5中,是怎么做到的

let a = [1, 2, 3, 4]
let b = a[0]
let c = a.slice(1)
console.log(b, c) // 1 [2, 3, 4]

1
2

那么在ES6中,这样的形式就很容易实现了,通过解构赋值和三点运算符结合的方式

let a = [1, 2, 3, 4]
let [d, …e] = a
console.log(d, e) // 1 [2, 3, 4]

1
2

当我们对一个空的数组进行解构赋值的时候,会出现什么情况呢?

let [f, …g] = []
console.log(f, g) //undefined []
let [h, …i] = [‘foo’]
console.log(h, i) // foo []

1
2

需要注意的是,我们使用扩展运算符,也就是三点运算符,这个运算是必须要放在参数的最后一位的,否则会报错

let […j, k] = [1, 2, 3, 4]
console.log(j, k)
//报错,Rest element must be last element

1
2

## 扩展运算符对字符串的支持

console.log([…’hello’]) // [“h”, “e”, “l”, “l”, “o”]
// 扩展运算符将字符串中的每一个值都拆分成了单个的字符组成的数组

1
2

当然,这还不是最重要的,重要的是, 扩展运算符对32位Unicode字符的支持

console.log([…’x\u{20bb7}\u20bb7’].length) // 4
// 可见,两个大括号完美地解决了32位字符这个bug的尴尬

1
2
3
4

对于unicode不太了解的童鞋, 可以先跳转到之前,看下我写的ES6中对Unicode的支持这一章节

括展运算符内部调用的是数据结构的iterator接口,因此只要具有iterator接口的对象,都可以使用扩展运算符

let map = new Map([
[1, ‘one’],
[2, ‘two’],
[3, ‘three’]
])
let arr = […map.values()]
console.log(arr)

1
2

Generator函数运行后返回一个遍历器对象,因此也可以使用扩展运算符

var go = function*() {
yield 1
yield 2
yield 3

}
console.log([…go()])

1
2
3
4
5
6

# 函数的name属性

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

通过bind返回的函数, name属性会加上'bound'前缀

function foo() {}
console.log(foo.name) // foo
function foo() {}
console.log(foo.bind({}).name) //bound foo

1
2
3
4
5
6
7
8

# ES6的箭头函数

ES6允许使用箭头定义函数

` var f = v => v `

上面的箭头其实就等价于

var f = function(v) {
return v
}

1
2
3
4
5
6
7
8

那么,上面是需要参数的情况下,当我们不需要参数的时候怎么办呢?

在这个情况下是可以使用圆括号代替的

`var f = () => 5`

等价于

var f = function() {
return 5
}

1
2
3
4
5
6

当存在多个参数的时候

`var f = (num1, num2) => num1 + num2`

等价于

var f = function(num1, num2) {
return num1 + num2
}

1
2
3
4

如果箭头函数的代码块部分多于一条语句, 就要使用大括号将其括起来, 并使用return语句返回

由于大括号被解释为代码块,所以当我们需要箭头函数返回一个对象的时候,需要使用圆括号将对象括起来

var f = id => ({
id: id,
name: ‘temp’
})

1
2

还有一点是,箭头函数还可以和解构赋值一起使用

var full = ({first, last}) => first + ‘ ‘ + last

1
2

等价于

var full = function(person) {
return first + “ “ + last
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

`[1, 2, 3].map(x => x * x)`

等价于

`[1, 2, 3].map(function(x) {return x * x})`

rest参数的写法

`const headAndTail = (head, ...tail) => [head, tail]`

# 箭头函数的使用注意点

>函数体内的this对象就是定义的时候所在的对象,而不是使用的时候所在的对象

>不可以拿来当做构造函数,也就是不可以拿来new一下,否则会抛出一个错误

>不可以使用arguments对象,该对象在函数体内是不存在的,如果需要使用,可以使用rest参数将arguments对象转为数组之后使用

>不可以使用yield命令,因此箭头函数不能用作Generator参数

# 箭头函数中的this

this的指向在之前是可变的,但是在箭头函数中是固定的

function foo() {
setTimeout(() => {
console.log(‘id:’, this.id) // id: 42
}, 100)
}

foo.call({id: 42})

1
2

而且不论嵌套多少层,都是没有问题的,朝阳指向调用的对象

function foo3() {
return () => {
return () => {
console.log(‘id: ‘, this.id) // id: 42
}
}
}
foo3.call({id: 42})()()

1
2

因为箭头函数没有自己的this, 所以我们在内部绑定this,是不能生效的

var a = (function() {
return [
(() => this.x).bind({
x: ‘inner’
})
]
}).call({
x: ‘outer’
})
console.log(a()) // () => this.x

1
2

箭头函数的内部还可以再使用箭头函数

function insert(value) {
return {
into: function(array) {
return {
after: function(afterValue) {
array.splice(array.indexOf(afterValue) + 1, 0, value)
}
}
}
}
}

console.log(insert(2).into([1, 3]).after(1)) // [1, 2, 3]

1
2
3
4
5
6

# 函数绑定

箭头函数可以绑定this对象, 大大减少了显式绑定this对象的写法, 但是因为this在箭头函数中的特殊性,因此其并不适用于所有场合

所以ES7提出了函数绑定的写法, 目前浏览器暂不支持,但babel转码器已经支持

foo::bar
// 等同于
bar.bind(foo)
`

文章目录
  1. 1. 写在前面
  2. 2. 函数的默认值
    1. 2.1. 使用rest参数的特性,可以改写push方法
  3. 3. 扩展运算符 …
|