@gyyin
2020-03-07T18:27:15.000000Z
字数 6957
阅读 321
慕课专栏
2015年6月17日发布的 ECMAScript 2015(ES6)是一个具有里程碑意义的语言标准。在该标准中,不仅引入了诸如类、模块、Promise、Map 等众多新的语言特性,还制定了更加规范化和快速的标准制定发布流程。
虽然浏览器还未全部实现对 ES6 标准的支持,但通过 Babel 等转译器,人们已经可以使用全部 ES6 特性。
一些情况下,我们也常常泛指 ES6 之后的新增特性,这些特性可能原属于 ES7、ES8 等等。
在 ES6 之前,声明变量常用的是 var
,var
有很多奇怪的表现。比如用 var
声明的变量会挂载到 window
对象上。
var a = 10;
console.log(window.a); // 10
var
还存在变量提升的问题。
console.log(a); // undefined
var a = 10;
除此之外,在 ES6 之前一直有一个问题困扰着开发者,那就是块作用域的问题。
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000)
}
以上面这段代码为例,原本希望在 1s 后打印出 0-9
,但最后却打印出了十个10,这就是因为在 for 循环中没有块作用域导致的。
因此,ES6 中引入了新的 let 关键字,提供了除 var
之外的变量声明方式。
和 var
不同的地方在于,使用 let
声明的变量无法重复声明,不会出现变量提升,也不会被挂载到 window
上面。
console.log(a); // ReferenceError: a is not defined
let a = 10;
console.log(window.a); // undefined
let a = 20; // Identifier 'a' has already been declared
let 的更大作用体现在块作用域上。在 ES5 中只有全局作用域和函数作用域,没有块级作用域也常常导致一些诡异的问题。比如上面说的 for 循环,最后 i 会暴露到全局作用域中。
var a = 10;
if (true) {
var a = 20;
}
console.log(a); // 20
从上面这段程序可以看出,由于没有块级作用域,导致了 if 里面重新声明的 a 修改了全局作用域中的 a 的值。
而 let 为 ES6 新增了块级作用域,块级作用域一般是指 {}
。上面这段代码将 var
换成 let
后就会得到截然不同的效果。
var a = 10;
if (true) {
let a = 20;
}
console.log(a); // 10
有了块级作用域后,我们可以随便嵌套。
{
let i = 10;
}
console.log(i); // ReferenceError: i is not defined
需要注意的是,虽然 {}
一般指块级作用域,但是对象字面量中的 {}
却不属于块级作用域。
var name = "tom";
let obj = {
name: "jerry",
name2: this.name
}
console.log(obj.name2); // "tom"
const 和 let 基本上一致,唯一的不同点在于用 const 声明的是常量,无法重新赋值。
const a = 10;
a = 20; // TypeError: Assignment to constant variable.
但是用 const 声明的复杂类型,还是可以修改其中的某一项。
const arr = [1, 2, 3];
arr[0] = 2;
const obj = {};
obj.name = "tom";
ES6 允许我们用箭头来定义一个函数。我们以 test 函数的三种声明方式为例:
// 函数声明
function test() {}
// 函数表达式
const test = function() {}
// 箭头函数
const test = () => {}
可以看到箭头函数省略了 function
的关键字,在其后面用 =>
来代替。
箭头函数还允许我们省略花括号和 return
关键字,这在一定程度上简化了函数声明。
function add(a, b) {
return a + b;
}
const add = (a, b) => a + b;
在高阶函数的场景下,箭头函数无疑是更加简洁的。
const add = function(a) {
return function(b) {
return a + b;
}
}
// 箭头函数
const add = a => b => a + b;
箭头函数中的 this 默认指向了上上一级作用域,而不是调用时的对象。
var name = "jerry"
const obj = {
name: 'tom',
say: () => {
console.log(this.name);
}
}
obj.say();
在普通函数中,obj.say
里面的 this 指向了 obj
对象,所以最后打印出来的一定是 "tom"。而在箭头函数中,this 指向了上上一级的作用域,在这里是全局对象,所以打印出了 jerry
。
箭头函数和普通函数有几点不同之处:
1. 箭头函数不能通过 bind、call、apply 等函数修改 this 指向。
2. 箭头函数不能当做构造函数,也就是说不能和 new
一起使用。
3. 箭头函数中不能使用 arguments
,必须用 rest
参数来代替。
4. 箭头函数不能当做 generator 函数来使用。
扩展运算符是三个点,可以将对象按照键值对的形式展开,也可以将数组转换成按照逗号分开的序列。
扩展运算符可以将数组展开成一个个项,可以将其当做参数传给函数。
const arr = [1, 2, 3];
console.log(...arr); // 1 2 3
也可以用一个新的数组来保存这些子项。
const arr = [1, 2, 3];
const arr2 = [1, ...arr]; // [1, 1, 2, 3]
通过扩展运算符展开的特性,可以将类数组转换为真正的数组。
function test() {
const args = [...arguments];
}
const divs = [...document.querySelectorAll("div")];
扩展运算符可以对数组进行一系列操作,比如数组合并、复制。
// 数组的合并
const arr = [1, 2, 3],
arr2 = [4, 5];
const arr3 = [...arr, ...arr2];
// 数组的复制
const arr3 = [...arr];
除了上面这些,还有一种逆向的用法,常常结合解构赋值使用,那就是 rest 剩余参数。
const arr = [1, 2, 3];
const [1, ...rest] = arr;
console.log(rest); // [2, 3]
扩展运算符同样可以用在对象中,常用于对象的浅拷贝。
const obj = {
name: "tom"
}
const person = {age: 20, ...obj}; // {age: 20, name: "tom"}
const obj2 = { ...obj }; // 相当于 Object.assign({}, obj);
注意:这里对象上的属性指的是对象自身且可枚举的。
解构赋值是对赋值运算符的一种扩展。这种方式在代码书写上更加简洁易读,方便了复杂对象类型中值的提取。
从语义上来理解,解构就是按照原有结构,将复杂类型展开,并重新赋值给新的变量。
我们知道,想要在 JavaScript 中声明并赋值多个变量,只有一个个写,这样比较繁琐。
let a = 10, b = 20, c = 30;
毕竟 JavaScript 不支持其他语言中直接对多个变量赋值的用法。
let a, b, c = 10, 20, 30; // SyntaxError: Unexpected number
有了解构赋值之后,声明赋值多个变量就变得更加简洁。
let [a, b, c] = [10, 20, 30];
解构的时候,还可以对变量设置默认值。
let [a = 0, b, c] = [, 20, 30];
解构赋值在对象中有广泛的应用。
const obj = {
name: "tom",
age: 20
}
// old
const name = obj.name,
age = obj.age;
// 解构赋值
const { name, age } = obj;
对象解构还能用于比较深层的嵌套对象,对属性值也可以做进一步解构。
const country = {
province: {
city: {
name: "shenzhen"
}
}
}
const {
province: {
city: {
name
}
}
} = country
console.log(name) // "shenzhen"
注意,上面的 province 和 city 只是模式,并非变量,如果想将其作为变量赋值,需要单独写。
const {
province,
province: {
city,
city: {
name
}
}
} = country
console.log(province); // {city: {name: "shenzhen"}}
除此之外,我们还可以对函数的参数进行解构。
const test = ({name, age}) => {
console.log(`name=${name}|age=${age}`);
}
test({name: "tom", age: 20})
使用数组的解构赋值,可以简单地实现变量的值互换。
let a = 10, b = 20;
[b, a] = [a, b];
console.log(a); // 20
console.log(b); // 10
同样地,也可以对深层的数组进行解构。
const [a, [b, c], d] = [1, [2, 3], 4];
如果你有经历过 jQuery 的时代,那么一定会对下面的代码深恶痛绝吧。
var html = "<h1 class='title'>hello, " + name + "</h1>"
不仅需要考虑各种引号的问题,还要考虑字符串和变量的拼接,让人非常头疼。
ES6 中对字符串进行了扩展,增加了模板字符串的特性,我们使用反引号 ` 来表示模板字符串,可以在其中嵌套变量。
const html = `<h1 class="title">hello, ${name}</h1>`
普通的字符串如果想要换行,只有在里面加入换行符,如果直接换行就是报错。
// SyntaxError: Invalid or unexpected token
"hello
world
"
// good
"hello\nworld"
而在模板字符串里面,可以直接输入回车。
`hello
world
`
模板字符串里面潜入的变量,还支持运算。
const a = 1, b = 2;
const str = `sum is: ${a + b}`;
Symbol 是 ES6 中新增加的一个基本类型,是 undefined
、null
、数值(Number)、布尔(Boolean)、对象(Object)、字符串(String)之外的第七种类型。
一般来说,Symbol 值都是通过 Symbol 函数来生成。Symbol 值是独一无二的,即使传入相同的参数,返回的结果依然不相等。
Symbol("hello") === Symbol("hello"); // false
由于 Symbol 具有唯一值的特性,可以用在对象的属性名上。由于对象的属性名都是字符串,常常会造成冲突,这样会导致后面的覆盖掉前面的。
const obj = {
name: "tom",
name: "jerry"
}
obj.name; // "jerry"
const s1 = Symbol("jerry");
const obj = {
name: "tom",
[s1]: "jerry"
}
obj[s1]; // "jerry"
在前面关于 JavaScript 类型转换这节中,我们介绍过使用 Symbol 可以重写 toPrimitive 方法的用法。
Symbol 中提供了 Symbol.toPrimitive
方法,该方法在类型转换的时候优先级最高。
const obj = {
toString() {
return '1111'
},
valueOf() {
return 222
},
[Symbol.toPrimitive]() {
return 666
}
}
const num = 1 + obj; // 667
const str = '1' + obj; // '1666'
Set 和 Map 是 ES6 中提供的新的数据结构。
Set 是一个构造函数,需要用 new
来创建一个 Set 对象。
const set = new Set();
Set 也可以接收一个数组作为初始值。
const set = new Set([1, 2, 3]);
Set 类似于数组,但里面的值都是唯一的,所以可以用来做数组去重。
const set = new Set([1, 2, 3, 2]);
[...set]; // [1, 2, 3]
虽然类似数组,但 Set 的操作方式和数组还是不一样,一共有下面这些方法。
1. add:添加一项,返回自身。
2. delete:删除一项,返回删除是否成功。
3. has:判断当前值是否在 Set 里面。
4. clear:清除所有值。
5. size:返回 Set 的成员数。
6. keys:返回 Set 的键名遍历器。
7. values:返回 Set 的值遍历器。
8. entries:返回 Set 键值对的遍历器。
9. forEach:类似数组的 forEach 用法。
const s = Set();
s.add(1).add(2).add(3);
s.has(2); // true
s.delete(3);
s.size; // 2
// 分别打印出1, 2
for(let key of s.keys()) {
console.log(key);
}
// 分别打印出1, 2
for(let value of s.values()) {
console.log(value);
}
// 分别打印出 key=1|value=1, key=2|value=2
for(let [key, value] of s.entries()) {
console.log(`key=${key}|value=${value}`);
}
// 分别打印出 key=1|value=1, key=2|value=2
s.forEach((k, v) => {
console.log(`key=${key}|value=${value}`);
})
注意:Set 中的 key 和 value 都是一样的。
前面我们讲过,对象的键都是字符串,所以容易造成冲突。如果键可以支持其他的数据类型呢?
ES6 中提供了 Map 这个数据结构,允许你使用各种数据类型,甚至包括对象。
const m = new Map();
const obj1 = {},
obj2 = {};
m.set(obj1, "obj1")
m.set(obj2, "obj2")
m.get(obj1) // "obj1"
m.get(obj2) // "obj2"
Map 的遍历方法和 Set 保持一致,都有 keys
、values
、entries
和 forEach
。
Map 的操作方法有下面这些:
1. set(key, value):和 Object 定义新属性用法一致,返回当前 Map 对象。
2. get(key): 根据键名获取值。
3. has(key): 判断 Map 对象是否有当前这个键名。
4. delete(key): 删除这个键值对,返回布尔值。
5. clear(): 清空所有成员。
6. size: 返回 Map 成员总数。
const m = new Map();
m.set(1, "a")
.set(2, "b")
.set(3, "c")
m.has(3); // true
m.size; // 2
m.get(2); // b
// 分别打印出1, 2, 3
for(let key of m.keys()) {
console.log(key);
}
// 分别打印出a, b, c
for(let value of m.values()) {
console.log(value);
}
// 分别打印出 key=1|value=a, key=2|value=b, key=3|value=c
for(let [key, value] of m.entries()) {
console.log(`key=${key}|value=${value}`);
}
// 分别打印出 key=1|value=a, key=2|value=b, key=3|value=c
m.forEach((k, v) => {
console.log(`key=${key}|value=${value}`);
})
通过扩展运算符,Map 可以转换为数组。
const m = new Map()
.set(1, "a")
.set(2, "b");
[...m]; // [[1, "a"], [2, "b"]]
与此相反的是,数组也可以转换成 Map。
const arr = [[1, "a"], [2, "b"]];
const m = new Map(arr);
Map 也可以转换为对象,但如果 Map 的键不是字符串,就会被隐式转换为字符串类型,可能造成一些键值丢失。
const m = new Map()
.set(1, "a")
.set(2, "b");
const obj = {};
m.forEach((k, v) => {
obj[k] = v;
})
对象转为 Map 则比较简单,可以直接使用 Object.entries()
来转。
const obj = {
1: "a",
2: "b"
}
const m = new Map(Object.entries(obj));
ES6 提供了许多新的语法,这些新语法在很大程度上都方便了我们开发。
时至今日,这些新特性在前端开发中已经被广泛使用了。作为前端开发,我们也应该时刻保持自身技术栈的更新,不断地去学习新语法,这样才不会被时代抛弃。