JavaScript|ES6日常用法详解

ES6全称ECMAScript 6.0 ,是JavaScript 的下一个版本标准。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

在前文中提到了ES6的新特性——class语法糖的使用。本文来讲讲除了class外ES6~ES12其他的新的特性,如果没有读的可以去看看:JavaScript|Class定义类

字面量的增强

ES6中对对象字面量进行了增强,主要包括三个部分:

  1. 属性的简写
  2. 方法的简写
  3. 计算属性名
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来实现变量存储。

62f3758c370c7

块级作用域

在ES5中只有两个东西会形成作用域:

  1. 全局作用域
  2. 函数作用域

在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不同的是:

  1. WeakSet中只能存放对象类型,不能存放基本数据类型
  2. WeakSet的每个元素都是弱引用,即垃圾回收机制不考虑WeakSet对该元素的引用,因此,如果元素所在的内存被回收,该元素也会被回收。
  3. 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不同的是:

  1. WeakMap中只能存放对象类型,不能存放基本数据类型
  2. WeakMap的每个元素都是弱引用,即垃圾回收机制不考虑WeakMap对该元素的引用,因此,如果元素所在的内存被回收,该元素也会被回收。
  3. 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被执行