ES6全称ECMAScript 6.0 ,是JavaScript 的下一个版本标准。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
在前文中提到了ES6的新特性——class语法糖的使用。本文来讲讲除了class外ES6~ES12其他的新的特性,如果没有读的可以去看看:JavaScript|Class定义类。
字面量的增强
ES6中对对象字面量进行了增强,主要包括三个部分:
- 属性的简写
- 方法的简写
- 计算属性名
var name = 'ricky'
var age = 18
var obj = {
// 属性的简写
name,
age,
// 方法的简写
foo: function() { // 一般写法
console.log(this);
},
bar() { // 简写
console.log(this);
},
bar2: ()=> { // 箭头函数
console.log(this);
}
// 计算属性名
[name+233]: 'Hello, world!',
}
解构
数组解构
var names = ["abc", "cba", "nba"]
// 对数组的解构: []
var [item1, item2, item3] = names
console.log(item1, item2, item3) // abc cba nba
// 解构后面的元素
var [, , itemz] = names
console.log(itemz) // nba
// 解构出一个元素,后面的元素放到一个新数组中
var [itemx, ...newNames] = names
console.log(itemx, newNames) // abc ['cba', 'nba']
// 解构的默认值
var [itema, itemb, itemc, itemd = "aaa"] = names
console.log(itemd) // aaa
对象解构
var obj = {
name: "Ricky",
age: 18,
height: 1.88
}
// 对象的解构: {}
var { name, age, height } = obj
console.log(name, age, height) // Ricky 18 1.88
// 解构后赋予其他变量名
var { name: newName } = obj
console.log(newName) // Ricky
// 设置默认值
var { address: newAddress = "广州市" } = obj
console.log(newAddress) // 广州市
function foo(info) {
console.log(info.name, info.age)
}
foo(obj) // Ricky 18
function bar({name, age}) {
console.log(name, age)
}
bar(obj) // Ricky 18
let/const的使用
在ES5中,我们使用var来声明变量,从ES6开始我们可以通过let/const来声明。
let关键字:从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量。
const关键字: 它表示保存的数据一旦被赋值,就不能被修改; 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容。
作用域提升
let、const和var的另一个重要区别是作用域提升:我们知道var声明的变量是会进行作用域提升的;但是如果我们使用let声明的变量,在声明之前访问会报错。
console.log(a); // undifined
var a = 'Hello, world!';
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 'Hello, world!';
这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值。从这里可以看出let、const没有进行作用域提升,但是会在解析阶段被创建出来。
变量保存
我们知道通过var声明的变量会被保存在window中,但let/const是不会给window添加属性的。在ECMA标准有这样一个描述:
每一个执行上下文会被关联到一个变量环境 (variable object, VO),在源代码中的变量和函数声明会被作为属性添加到VO中。对于函数来说,参数也会被添加到VO中。
每一个执行上下文会关联到一个变量环境 (VariableEnvironment) 中,在执行代码中变量和函数的声明会作为环境记录 (Environment Record) 添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。
所以通过let/const声明的变量会被添加到变量环境中,但不是window对象上。在v8中通过VariableMap的hashmap来实现变量存储。
块级作用域
在ES5中只有两个东西会形成作用域:
- 全局作用域
- 函数作用域
在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的,但块级作用域内不函数可以被外部访问,因为引擎会对函数进行特殊处理:
{
let foo = "why"
function demo() {
console.log("demo function")
}
class Person {}
}
console.log(foo) // foo is not defined
// 不同的浏览器有不同实现的(大部分浏览器为了兼容以前的代码, 让function是没有块级作用域)
demo() // demo function
var p = new Person() // Person is not defined
块级作用域案例——多个按钮监听点击:
<button>button1</button>
<button>button2</button>
<button>button3</button>
<button>button4</button>
<script>
const btns = document.getElementsByTagName('button')
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
console.log("第" + i + "个按钮被点击")
}
}
</script>
暂时性死区
是在一个代码中,使用let、const声明的变量,在声明之前,变量都是不可以访问的:
var foo = 'foo';
if (1) {
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 'bar';
}
模版字符串
通过${...}
可以实现模版字符串
// ES6之前拼接字符串和其他标识符
const name = "Ricky"
const age = 18
const height = 1.88
// ES6提供模板字符串 ``
const message = `my name is ${name}, age is ${age}, height is ${height}`
console.log(message) // my name is Ricky, age is 18, height is 1.88
// 能够实现运算
const info = `age double is ${age * 2}`
console.log(info) // 36
// 能够实现函数
function doubleAge() {
return age * 2
}
const info2 = `double age is ${doubleAge()}`
console.log(info2) // 36
一个不怎么常用的方法:标签模块字符串
第一个参数依然是模块字符串中整个字符串, 只是被切成多块,放到了一个数组中
第二个参数是模块字符串中, 第一个 ${}
function foo(m, n, x) {
console.log(m, n, x)
}
foo`Hello World` // [ 'Hello World' ] undefined undefined
const name = "Ricky"
const age = 18
// ['Hello', 'Wo', 'rld']
foo`Hello${name}Wo${age}rld` // [ 'Hello', 'Wo', 'rld' ] Ricky 18
函数默认值
可以在创建函数的时候很方便地为参数设置默认值,但最好有默认值的参数放在最后;
另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了.
// 1.ES6可以给函数参数提供默认值
function foo(m = "aaa", n = "bbb") {
console.log(m, n)
}
foo() // aaa bbb
foo(0, "") // 0
// 2.对象参数和默认值以及解构
function printInfo({name, age} = {name: "Ricky", age: 18}) {
console.log(name, age)
}
printInfo({name: "kobe", age: 40}) // kobe 40
// 另外一种写法
function printInfo1({name = "Ricky", age = 18} = {}) {
console.log(name, age)
}
printInfo1() // Ricky 18
// 3.有默认值的形参最好放到最后
function bar(x, y, z = 30) {
console.log(x, y, z)
}
bar(10, 20) // 10 20 30
bar(undefined, 10, 20) // undefined 10 20
// 4.有默认值的函数的length属性
function baz(x, y, z, m, n = 30) {
console.log(x, y, z, m, n)
}
function baz1(x, y, z, m, n) {
console.log(x, y, z, m, n)
}
console.log(baz.length) // 4
console.log(baz1.length) // 5
函数剩余参数
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组。
function fun(m, n, ...args) {
console.log(m, n);
console.log(args);
}
fun('a', 'b', 'c', 'd'); //a b [ 'c', 'd' ]
与arguments区别:剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
function fun1(m, n) {
console.log(m, n);
console.log(arguments);
}
fun1('a', 'b', 'c', 'd'); // a b [Arguments] { '0': 'a', '1': 'b', '2': 'c', '3': 'd' }
箭头函数
箭头函数没有显示原型,不能作为构造函数,即不能new:
const foo = () => {
console.log('foo');
}
console.log(foo.prototype); // undefined
var f = new foo(); // TypeError: foo is not a constructor
展开语法
可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开,还可以在构造字面量对象时, 将对象表达式按key-value的方式展开:
const str = 'Hello';
console.log(...str); // H e l l o
const arr = [1, 2, 3];
const arr1 = [...arr, 4, 5, 6]; // [1, 2, 3, 4, 5, 6]
const obj = {a: 1, b: 2};
const obj1 = {...obj, c: 3, d: 4}; // {a: 1, b: 2, c: 3, d: 4}
数值表示
在ES6中规范了二进制和八进制的写法:
const binary = 0b11111111; // 二进制 -> 十进制: 255
const octal = 0o377; // 八进制 -> 十进制: 255
const hex = 0xFF; // 十六进制 -> 十进制: 255
在ES2021新增特性:数字过长时,可以使用_作为连接符:
const bigNum = 123_456_789_123; // 123456789123
Symbol作为属性名
Symbol是一种新的基本数据类型,可以用来描述对象的属性名,在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突。比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性。
const key1 = Symbol('key');
const key2 = Symbol('key');
console.log(key1 === key2); // false
const obj = {
[key1]: 'value1',
[key2]: 'value2'
}
// Symbol只能通过[key]访问
console.log(obj[key1], obj[key2]); // value1 value2
Set&Map
在ES6之前,我们存储数据的结构主要有两种:数组、对象。 在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。
Set
Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式):
const set = new Set();
// 添加元素
set.add(1);
set.add(2);
set.add(3);
set.add(1);
console.log(set); // Set(3) { 1, 2, 3 }
// 删除元素
set.delete(1);
console.log(set); // Set(2) { 2, 3 }
// 判断是否存在元素
console.log(set.has(1)); // false
console.log(set.has(2)); // true
// 清空Set
set.clear();
console.log(set); // Set(0) {}
// 遍历Set: 可以forEach和for of
const set1 = new Set(['a', 'b', 'c', 'd'])
set1.forEach(item => {
console.log(item); // a b c d
}
for (let value of set1) {
console.log(value); // a b c d
}
常见用法——数组去重:
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5];
const set = new Set(arr);
const newArr = Array.from(set);
console.log(newArr); // 1 2 3 4 5 6 7 8 9 10
WeakSet
WeakSet是新增和Set类似的数据结构,可以用来保存对象,但是和Set不同的是:
- WeakSet中只能存放对象类型,不能存放基本数据类型
- WeakSet的每个元素都是弱引用,即垃圾回收机制不考虑WeakSet对该元素的引用,因此,如果元素所在的内存被回收,该元素也会被回收。
- WeakSet不能遍历:因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。所以存储到WeakSet中的对象是没办法获取的
一个作用——防止通过非构造方法创建出来的对象调用:
const set = new WeakSet();
class Person {
constructor(name) {
this.name = name;
set.add(this)
}
sayHi() {
if (!set.has(this)) throw new Error('不能调用非构造方法创建出来的对象');
console.log(`Hi, I am ${this.name}`);
}
};
const person1 = new Person('张三');
person1.sayHi(); // Hi, I am 张三
person1.sayHi.call({name: '李四'}); // Error: 不能调用非构造方法创建出来的对象
Map
Map是一个新增的数据结构,可以用来保存键值对,类似于对象,但是和对象的区别是键不能重复,而且可以用其他类型作为键。创建Map我们需要通过Map构造函数(暂时没有字面量创建的方式):
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
// 添加元素
map.set('d', 4);
console.log(map); // Map(4) { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }
// 可以设置其他类型的键
map.set({ name: 'a' }, 5); // Map(5) { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, { name: 'a' } => 5 }
// 查找元素
console.log(map.get('a')); // 1
// 删除元素
map.delete('a');
console.log(map); // Map(4) { 'b' => 2, 'c' => 3, 'd' => 4 , { name: 'a' } => 5 }
// 判断是否存在元素
console.log(map.has('a')); // false
console.log(map.has('b')); // true
// 清空Map
map.clear();
console.log(map); // Map(0) {}
// 遍历Map: 可以forEach和for of
const map1 = new Map([['a', 1], ['b', 2], ['c', 3]])
map1.forEach((value, key) => {
console.log(key, value); // a 1 b 2 c 3
}
for (let [key, value] of map1) {
console.log(key, value); // a 1 b 2 c 3
}
WeakMap
WeakMap是新增的数据结构,可以用来保存对象,但是和Map不同的是:
- WeakMap中只能存放对象类型,不能存放基本数据类型
- WeakMap的每个元素都是弱引用,即垃圾回收机制不考虑WeakMap对该元素的引用,因此,如果元素所在的内存被回收,该元素也会被回收。
- WeakMap不能遍历:因为WeakMap只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。所以存储到WeakMap中的对象是没办法获取的
可以通过WeakMap来实现响应式原理,将对象和方法存储在一起,当对象的属性发生变化时,可以自动触发对应的方法,而对象销毁的时候WeakMap也会销毁:
// 应用场景(vue3响应式原理)
const obj1 = {
name: "Ricky",
age: 18
}
function obj1NameFn1() {
console.log("obj1NameFn1被执行")
}
function obj1NameFn2() {
console.log("obj1NameFn2被执行")
}
function obj1AgeFn1() {
console.log("obj1AgeFn1")
}
function obj1AgeFn2() {
console.log("obj1AgeFn2")
}
const obj2 = {
name: "kobe",
height: 1.88,
address: "广州市"
}
function obj2NameFn1() {
console.log("obj1NameFn1被执行")
}
function obj2NameFn2() {
console.log("obj1NameFn2被执行")
}
// 1.创建WeakMap
const weakMap = new WeakMap()
// 2.收集依赖结构
// 2.1.对obj1收集的数据结构
const obj1Map = new Map()
obj1Map.set("name", [obj1NameFn1, obj1NameFn2])
obj1Map.set("age", [obj1AgeFn1, obj1AgeFn2])
weakMap.set(obj1, obj1Map)
// 2.2.对obj2收集的数据结构
const obj2Map = new Map()
obj2Map.set("name", [obj2NameFn1, obj2NameFn2])
weakMap.set(obj2, obj2Map)
// 3.如果obj1.name发生了改变
// Proxy/Object.defineProperty
obj1.name = "james"
const targetMap = weakMap.get(obj1)
const fns = targetMap.get("name")
fns.forEach(item => item()) // obj1NameFn1被执行 obj1NameFn2被执行