函数的扩展
1. 函数参数的默认值
// es5写法
function add(a,b) {
a = a || 10;
b = b || 20;
return a + b;
}
console.log(add()); // 30
// es6写法
function add(a = 10, b = 20) {
return a + b;
}
console.log(add()); // 30
// 默认值也可以是一个函数
function add(a = 10, b = getVal(5)) {
return a + b;
}
function getVal(value) {
return value + 5;
}
console.log(add()); // 20
2. rest参数
形式为:...变量名
定义函数实现:计算传入所有参数的和
// es5 使用arguments
function sum() {
let result = 0;
for (let i = 0; i < arguments.length; i++) {
result += arguments[i];
}
console.log(arguments);
return result;
}
console.log(sum(1,2,3)); // 6
// es6 使用rest参数
function sum(...value) {
let result = 0;
for(let i of value) {
result += i;
}
console.log(value);
return result;
}
console.log(sum(1,2,3)); // 6
这两种方法看似差不多,但是操作的元素有本质上的区别
arguments
是一个类数组
,本质是对象
,里面保存的是传递给函数的参数
- 而
rest
参数value
是一个真正的数组
,可以正常调用数组的所有方法 - 所以在某些场景中,不用将
arguments
转为真正的数组,可以直接使用rest
参数代替
⚠️:
rest
参数可以和其他形参共存,但是rest
参数只能是最后一个
参数,否则会报错- 函数的
length
属性,不包括rest
参数
console.log((function(a, ...b) {}).length) // 1
3. 扩展运算符
剩余运算符和扩展运算符都是...
剩余运算符:把多个独立的参数合并到一个数组中
扩展运算符:把一个数组分割,将各个项作为分离的参数传入函数
const maxNum = Math.max(20,30);
console.log(maxNum); // 30
const arr = [10,20,50,30,100,200];
cconsole.log(Math.max(...arr)); // 200
4. 箭头函数
let func = a => a;
// 等同于
let func = function a() {
return a;
}
参数不止一个的时候,()
不能省略
代码块不止一条return
语句的时候,{}
不能省略
let sum = (a,b) => {
let newA = a + 10;
return newA + b;
}
如果箭头函数直接返回一个对象,必须在对象外面加上()
,{}
会被解释为代码块
// 报错
let getObj = id => {
id: id,
name: "a"
};
// 不报错
let getObj = id => ({
id: id,
name: "a"
});
箭头函数也能简化回调函数:
const arr = [1,2,3,4,5];
// es5
[1,2,3,4,5].map(function(x) {
return x * x;
})
// es6
[1,2,3,4,5].map(x => x * x);
rest
参数和箭头函数结合使用:
const headAndTail = (head, ...tail) => [head,tail]; // head: 1 tail: [2,3,4,5]
console.log(headAndTail(1,2,3,4,5)); // [1,[2,3,4,5]]
箭头函数的this指向
箭头函数没有自己的this
对象,它内部的this
就是定义时上层作用域
中的this
而普通函数的内部this
指向函数运行时
所在的对象,或者说是调用
该函数的对象
下面例子对比回调函数分别为箭头函数和普通函数时的this
指向:
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
const timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 100);
setTimeout(() => console.log('s2: ', timer.s2), 100);
// s1: 3
// s2: 0
Timer
函数内部设置了两个定时器,分别使用了箭头函数
和普通函数
。前者的this
指向定义时所在的作用域(即Timer函数),后者的this
指向运行时所在的作用域(即全局对象)
下面是一个例子,DOM 事件的回调函数封装在一个对象里面:
let obj = {
id: "123",
init: function() {
document.addEventListener("click", event => {
this.doSomething(event.type);
});
}
doSomething: function(type) {
console.log(type + this.id);
}
};
init()
方法中,使用了箭头函数
,这导致这个箭头函数
里面的this
,总是指向obj
对象。如果回调函数是普通函数,那么运行this.doSomething()
这一行会报错,因为此时this
指向document
对象
总之,箭头函数根本没有自己的this
,导致内部的this就是外层代码块的this
下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this
的指向
// 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
不适用场合
下面两个场合不应该使用箭头函数:
- 定义对象的方法,且该方法内部包括
this
:
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
上面代码中,cat.jumps()
方法是一个箭头函数。调用cat.jumps()
时,如果是普通函数
,该方法内部的this
指向cat对象
,如果写成上面那样的箭头函数
,该方法内部的this
就是它外层的this
,由于对象不构成单独的作用域,所以外层的this
指向的就是全局对象
- 需要
动态this
的时候,也不应使用箭头函数
:
let btn = document.getElementById("press");
btn.addEventListener("click", () => {
this.classList.toggle("on");
});
上面代码运行时,点击按钮会报错
因为btn
的监听函数
是一个箭头函数
,导致里面的this
就是全局对象
如果改成普通函数
,this
就会动态
指向被点击的按钮对象
解构赋值
1. 数组的解构赋值
// es5写法
let a = 1;
let b = 2;
let c = 3;
// es6写法
let [a,b,c] = [1,2,3];
es6的写法:本质上属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值
下面还有一些使用嵌套数组进行解构的例子:
let [a,[b],c] = [1, [2], 3];
console.log(a,b,c); // 1 2 3
let [x, , y] = [1, 2, 3];
console.log(x,y); // 1 3
let [h, ...t] = [1, 2, 3, 4];
console.log(h,t); // 1 [2,3,4]
// 如果解构不成功,变量的值就等于undefined
let [m, n, ...p] = ["hello"];
console.log(m, n, p); // hello undefind []
还有一种情况是不完全结构,即等号左边的模式,只匹配一部分的等号右边的数组
let [x,y] = [1, 2, 3];
console.log(x, y); // 1 2
解构赋值也可以指定默认值:
let [x, y = 'b'] = ['a']; // x = 'a', y = 'b'
let [x, y = 'b'] = ['a', undefined]; // x = 'a', y = 'b'
⚠️:ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined
,默认值
才会生效。
2. 对象的解构赋值
对象的解构与数组有一个重要的不同:数组的元素
是按次序排列
的,变量的取值由它的位置
决定;而对象的属性没有次序
,变量必须与属性同名,才能取到正确的值
let { a, b } = { a: 1, b: 2 };
console.log(a, b); // 1 2
let { c } = { a: 1, b: 2 };
console.log(c); // undefined
对象的解构赋值,可以很方便地将现有对象的属性和方法,赋值到某个变量:
// 变量名和属性名必须一样
let obj = {
name: 'AIpoem',
age: 19,
sayHi: function() {
console.log("hi");
}
};
let { name, age, sayHi } = obj;
console.log(name, age); // AIpoem 19
console.log(sayHi); // ƒ () { console.log("hi")}
如果变量名与属性名不一致,必须写成下面这样:
let { a: newA } = { a: 1, b: 2 };
console.log(newA);
对象的扩展
1. 属性简洁表示
let name = 'AIpoem';
let age = 19;
// 简洁表示
let obj = {
name,
age
}
// 等同于
let obj = {
name: name
age: age
}
要属性名和变量名相等时才能这样写
方法也可以简写:
// 简洁写法
let obj = {
method: function() {
console.log(1);
}
}
// 等同于
let obj = {
method() {
console.log(1);
}
}
2. 属性名表达式
定义对象的属性,有两种方法:
let obj = {
a: 1,
b: 2
};
// 方法一
let attr1 = obj.a;
// 方法二
let attr2 = obj['b'];
es6允许字面量定义对象时,使用表达式作为对象的属性名:
let obj = {
['a' + 'bc']: 123
};
console.log(obj.abc); // 123
console.log(obj['abc']); // 123
3. 属性的可枚举性
对象的每个属性都有一个描述对象
,用来控制该属性的行为
Object.getOwnPropertyDescriptor()
可以获取到该属性的描述对象
let obj = { a: 1 };
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
//{
// configurable: true
// enumerable: true
// value: 1
// writable: true
//}
enumerable
属性,称为可枚举性
。如果该属性为false
,表示某些操作会忽略当前属性
目前,有四个操作会忽略enumerable
为false
的属性:
for...in
:只遍历对象自身的和继承的
可枚举的属性Object.keys()
:返回对象自身
所有可枚举的属性的键名JSON.stringify()
:只串行化对象自身
的可枚举的属性Object.assign()
:只拷贝对象自身
的可枚举的属性
其中,只有for...in
会返回继承的属性
,其他三个方法都会忽略继承的属性,只处理对象自身的属性
实际上,引入可枚举
这个概念的最初目的,就是让某些属性可以规避掉for...in
操作,比如对象原型的toString
方法,数组的length
属性
Object.getOwnPropertyDescriper(Object.prototype, 'toString').enumerable // false
Object.getOwnPropertyDescriper([], 'length').enumerable // false
toString
和length
属性的enumerable
都是false
,因此for...in
不会遍历到这两个属性
Set
es6提供了新的数据结构Set
,它类似于数组,但是成员的值都是唯一的,没有重复的值
// 创建Set数据结构
let set = new Set();
// 添加元素
set.add(2);
// 删除元素
set.delete(2);
// 校验某个值是否在Set中
console.log(set.has(2));
// Set实例的成员总数
console.log(set.size);
// 接受一个数组用来初始化
let set2 = new Set([1,2,3,4,4]);
⚠️:向Set
中加入值的时候,不会发生类型转换。而且Set
内部两个NaN
是相等的。但两个对象总是不相等的
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
1. 遍历操作
有四个遍历方法:
// 返回键名的遍历器
set.keys()
// 返回键值的遍历器
set.values()
// 返回键值对的遍历器
set.entries()
// 使用回调函数遍历每个成员
set.forEach()
Set
的遍历顺序就是插入顺序
⚠️:Set
结构没有键名
,只有键值
(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致
let set = new Set(['red','green','blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
set.forEach((value,key) => {
console.log(key + ":" + value);
})
// red:red
// green:green
// blue:blue
2. 将集合转为数组
利用扩展运算符
let set = new Set([1,2,3,4,4,2,2]);
let arr = [...set];
console.log(arr); // [1, 2, 3, 4]
Map
es6提供了Map
数据结构,它类似于对象
,也是键值对
的集合
键和值可以是任意类型
let map = new Map();
let key = {p: 'hello world'};
// 添加键值对,第一个参数是键,第二个参数是值
map.set(key,'content');
// 读取这个键
console.log(map.get(key)); // "content"
// 校验是否有某个键
console.log(map.has(key)); // true
// 删除键
map.delete(key);
// 接收一个数组用来初始化,指定了两个键name和title
let map2 = new Map([
['name','AIpoem'],
['age',19]
]);
遍历操作
有四个遍历方法:
// 返回键名的遍历器
map.keys()
// 返回键值的遍历器
map.values()
// 返回键值对的遍历器
map.entries()
// 使用回调函数遍历每个成员
map.forEach()
let map = new Map([
['F','no'],
['T','yes']
]);
数组的扩展
1. 扩展运算符的应用
1. 1 复制数组
直接复制数组的话只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组
const arr1 = [1,2];
const arr2 = arr1;
arr2[0] = 2;
console.log(arr1); // [2,2]
上面代码中,arr2
并不是arr1
的克隆,而是指向同一份数据的另一个指针
。修改arr2
,会直接导致arr1
的变化
// es5 复制数组
const arr1 = [1,2];
const arr2 = arr1.concat();
arr2[0] = 2;
console.log(arr1); // [1,2]
扩展运算符提供了复制数组的简便写法:
// es6 复制数组
const arr1 = [1,2];
const arr2 = [...arr1];
1. 2 合并数组
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// es5 合并数组
arr1.concat(arr2,arr3);
// es6 合并数组
[...arr1,...arr2,...arr3]
1.3 与解构赋值结合
扩展运算符和解构赋值可以一起用于生成数组
:
如果扩展运算符用于数组赋值,只能放在最后一位
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first,rest); // 1 [2, 3, 4, 5]
// 报错
const [...first, last] = [1, 2, 3, 4, 5];
1.4 字符串
扩展运算符还可以将字符串
转为数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
2. Array.from()
Array.from()
用于将类数组对象
和可遍历对象
转为真正的数组
// 类数组对象
let arrLike = {
'0': 'a',
'1': 'b'
length: 2
};
// es5 将类数组对象转为数组
let arr1 = [].slice.call(arrLike);
// es6 将类数组对象转为数组
let arr2 = Array.from(arrLike);
实际应用中,常见类数组对象是dom操作返回的NodeList集合,函数内部的arguments对象,Array.from
都可以将它们转为真正的数组
// NodeList对象
let p = document.querySelectorAll('p');
// 之后就能使用一些数组的方法
let arrP = Array.from(p);
// arguments对象
function func() {
// 之后就能使用一些数组的方法
let args = Array.from(arguments);
}
⚠️:扩展运算符
能将一些可遍历对象
转为数组
,而Array.from()
还支持类数组对象
,类数组对象
本质特征是必须有length
属性,因此任何有length
属性的对象可以使用Array.from()
转为数组,扩展运算符此时则不能
Array.from()
还可以接受第二个参数,对每个元素进行处理,将处理完的值放入返回的数组
let arrLike = [1, 2, 3];
Array.from(arrLike, x => x * x);
// 等同于
Array.from(arrLike).map(x => x * x);
// [1, 4, 9]
3. Array.of()
Array.of()
用于将一组值
转换为数组
console.log(Array.of(3, 11, 8)) // [3, 11, 8]
console.log(Array.of(3, 11, 20, [1,2,3], {id:1})) // [3, 11, 20, [1,2,3], {id:1}]
4. copyWithin()
在数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),使用这个方法,会修改当前数组
接受3个参数:
- target(
必需
):从该位置开始替换数据 - start(
可选
):从该位置开始读取数据 - end(
可选
):到该位置前停止读取数据,默认为数组长度
// 从下标3开始读取数据,未传第三个参数,所以默认读到末尾,就是复制4和5
// 从下标0开始替换数据,4和5替换掉1和2
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// 下标-2倒着数是下标3 = 4,下标-1倒着数是下标4 = 5,到下标4之前停止读取,也就是复制4
// 从下标0开始替换数据,4替换掉1
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
5. find() 和 findIndex()
find()
用于找出第一个符合条件
的数组成员
参数是一个回调函数,数组所有成员依次执行该回调,直到找出第一个返回值为true
的成员
如果没有符合条件的成员,则返回undefined
// 找出数组中第一个小于0的成员
[1, 4, -5, 10].find(n => n < 0)
// -5
findIndex()
用法与find()
非常类似,返回第一个符合条件
的数组成员
的位置
,如果所有成员都不符合条件,则返回-1
[1, 4, -5, 10].findIndex(n => n < 0)
// 2 (-5的下标是2)
6. entries()、 keys()、 values()
es6提供三个新的方法:
entries()
、 keys()
、 values()
用于遍历数组
,它们都返回一个遍历器对象
,可以用for...of
循环进行遍历
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
如果不使用for...of
循环,可以调用遍历器对象的next()
方法,进行遍历:
let letter = ['a', 'b', 'c'];
let entries
7. includes()
includes()
用于判断数组
是否包含某个值
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
没有includes()
之前,我们通常使用indexOf()
,来检查是否包含某个值,相比起来includes()
更加语义化,而且indexOf()
内部使用===
进行判断,会导致对NaN
的误判
if ([1, 2, 3].indexOf(2) !== -1) {//
}
[NaN].indexOf(NaN)
// -1
[NaN].includes(NaN)
// true
遍历器Iterator
Iterator
是一种新的遍历机制
- 遍历器是一个接口,能快捷访问数据,通过
Symbol.iterator
来创建遍历器,通过遍历器的next()
获取遍历之后的结果 - 遍历器是用于遍历数据结构的指针对象
Iterator
的遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说遍历器对象本质上是一个指针对象
第一次
调用指针对象的next()
方法,可以将指针指向数据结构的第一个
成员第二次
调用指针对象的next()
方法,可以将指针指向数据结构的第二个
成员- 不断调用指针对象的
next()
方法,直到指针指向数据结构的结束位置
每一次调用next()
方法,都会返回数据结构的当前成员的信息,就是返回一个对象,有value
和done
两个属性,其中value
是当前成员的值,done
表示遍历是否完成
const items = ['one', 'two', 'three'];
// 创建新的迭代器
const ite = items[Symbol.iterator]();
console.log(ite.next()); // {value: "one", done: false}
console.log(ite.next()); // {value: "two", done: false}
console.log(ite.next()); // {value: "three", done: false}
console.log(ite.next()); // {value: "undefined", done: true}
for…of 循环
一个数据结构只要部署了Symbol.iterator
属性,就被视为具有iterator
接口,就可以用for...of
循环遍历它的成员
也就是说,for...of
循环内部调用的是数据结构的Symbol.iterator
方法
for...of
可以使用的范围包括:
数组、Set
、Map
、类数组对象、后文的Generator
对象、字符串
// 数组
const arr1 = ['red', 'green', 'blue'];
for (let item of arr1) {
console.log(item); // red green blue
}
// 原有的for...in循环,只能拿到键名,for...of可以获得键值
const arr2 = [
{
a: 1,
b: 2
},
{
c: 3
}
];
for (let item in arr2) {
console.log(item); // 0 1
}
for (let item of arr2) {
console.log(item); // {a: 1, b: 2} {c: 3}
}
// 注意:for...of循环调用遍历器接口,数组的遍历器接口值返回具有数字索引的属性
const arr3 = ['one', 'two', 'three'];
arr3.foo = 'hello';
for (let item in arr3) {
console.log(item); // 0 1 2 foo
}
for (let item of arr3) {
console.log(item); // one two three
}
参考:
https://es6.ruanyifeng.com/?search=filter&x=0&y=0#README