Skip to main content

ES6

简介

  • ES6 是 ECMA 为 JavaScript 制定的第 6 个标准版本
  • ECMAscript 2015 是在 2015 年 6 月发布 ES6 的第一个版本。以此类推,ECMAscript 2016 是 ES6 的第二个版本,也叫 ES7、ES2016。
  • ES6 是一个泛指,含义是 5.1 版本以后的 JavaScript 下一代标准。

let 和 const

let

用来声明变量,只在let命令所在的代码块内有效,即块级作用域。不存在变量提升,不允许重复声明

function varTest() {
var a = 1;
if (true) {
var a = 2;
console.log(a); // 2
}
console.log(a); // 2
}

function letTest() {
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 1;
// let b = 2; // SyntaxError: Identifier 'b' has already been declared
if (true) {
let b = 2;
console.log(b); // 2
}
console.log(b); // 1
}

letTest()的 if 语句中,可以再次声明变量 b,是因为变量 b 只在这个 if 语句中有效。如果在 if 语句中使用var声明变量 b,会报错。

let 很适合在 for 循环时声明索引变量

暂时性死区:在使用 let 或 const 声明变量之前,该变量都不可用

{
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;
}

const

const声明一个只读的常量,必须初始化赋值。一旦声明,常量的值就不能改变,只在声明所在的块级作用域内有效。 不存在变量提升,不允许重复声明。复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改

const a = '123';
a = '234'; // TypeError: Assignment to constant letiable

const arr = [1, 2, 3];
arr.push(4);
console.log(arr); // [1,2,3,4]
arr = [];
console.log(arr); // 改变数组的指向会出错 Uncaught TypeError: Assignment to constant letiable
warning

let 和 const 声明的全局变量不属于顶层对象的属性,只存在于块级作用域中

let a = 1;
const b = 2;
console.log(window.a); // undefined
console.log(window.b); // undefined

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/133

var

var 声明的变量是全局变量,在函数中声明属于局部变量

var a = 1;
function fn() {
var a = 2;
}
fn();
console.log(a); // 1

在函数中不使用 var,该变量是全局的

var a = 1;
function fn() {
a = 2;
}
fn();
console.log(a); // 2

var 声明变量存在变量提升

console.log(a); // undefined
var a = 1;

// 在编辑阶段,变成如下形式:
var a;
console.log(a);
a = 1;

可以重复声明变量,后面的会覆盖前面的

var b = 1;
var b = 2;
console.log(b); // 2

模板字符串

模板字符串是增强版的字符串,用反引号\标识,嵌入的变量名写在${}之中。

基础使用

  1. 基本的字符串格式化
const name = 'world';

// ES5
console.log('hello' + name);

// ES6
console.log(`hello${name}`);
  1. 多行字符串拼接
let say = `<div>
<p>hello, world</p>
</div>`;

标记模版

标记模版(Tagged templates),使用函数解析模版文字。标签函数的第一个参数是一个字符串值数组,其余参数和表达式相关。

在 React 项目中,常用CSS in JS方案管理样式,如EmotionStyled-components。它们使用了标记模版,常见写法:

const Button = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid #bf4f74;
color: #bf4f74;
margin: 0 1em;
padding: 0.25em 1em;
`;

示例:

function myTag(strings, personExp, ageExp) {
console.log('strings', strings); // [ 'That ', ' is a ', '.' ]
const str0 = strings[0];
const str1 = strings[1];
const str2 = strings[2];

const ageStr = ageExp < 100 ? 'youngster' : 'centenarian';
return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const person = 'Tom';
const age = 28;

const output = myTag`That ${person} is a ${age}.`;

console.log(output); // That Tom is a youngster.

解构赋值

1. 数组的解构赋值

可以从数组中提取值,按照对应位置,对变量赋值。这种写法属模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值

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

注意细节:

1、左右结构不同

let [a, b, c, d] = [1, 2, 3];
console.log(a, b, c, d); // 1 2 3 undefined

2、跳过部分

let [a, , c] = [1, 2, 3];
console.log(a, c); // 1 3

3、默认值

let [a, b, c, d = 666] = [1, 2, 3];
console.log(a, b, c, d); // 1 2 3 666

let [a = 11, b = 22, c, d = 666] = [];
console.log(a, b, c, d); // 11 22 undefined 666

4、嵌套

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

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

5、数组的对象解构

const str = '23,zgh,boy';
// { 数组下标: 变量名 }
const { 1: name, 2: sex, 0: age } = str.split(',');
console.log(name, sex, age); // zgh boy 23

2. 对象的解构赋值

let { name, age } = { name: 'zgh', age: 22 };
console.log(name, age); // zgh 22
tip

对象与数组解构的不同点:

  • 数组的元素是按次序排列的,变量的取值由它的位置决定
  • 对象的属性没有次序,变量必须与属性同名,才能取到正确的值

3. 函数参数的解构赋值

let f = ([a, b]) => a + b;
f([1, 2]); // 3

上述代码可将数组[1, 2]看作一个参数param,即param = [1, 2]

函数扩展

为函数的参数设置默认值

可以给函数的参数设置默认值,如果不指定该函数的参数值,就会使用默认参数值

function Person(name = 'zgh', num = 22) {
const name = name || 'zgh';
const num = num || 22;
}
Person();
Person('Jack', 20);

如果没有设置默认值,调用时 num 传入 0,0 为 false,那么例子中的 num 结果就为 22 而不是 0

箭头函数

ES6 允许使用箭头=>定义函数

// 1.不带参数
let sum = () => 1 + 2;
// 等同于
let sum = function () {
return 1 + 2;
};

// 2.带一个参数
let sum = a => a;
// 等同于
let sum = function (a) {
return a;
};

// 3.带多个参数,需要使用小括号将参数括起来
let sum = (a, b) => a + b;
// 等同于
let sum = function (a, b) {
return a + b;
};

// 4.代码块部分多于一条语句需要用大括号将其括起来,并且使用return语句返回。
let sum = (a, b) => {
let c = a + b;
return c;
};

// 5.返回对象,就必须用小括号把该对象括起来
let person = name => ({ name: 'zgh', age: 22 });
// 等同于
let person = function (name) {
return { name: 'zgh', age: 22 };
};

箭头函数的 this 指向

箭头函数本身是没有thisarguments的,在箭头函数中引用 this 实际上是调用的是定义时的父执行上下文的 this。

  • 使用call,apply,bind都不能改变 this 指向
  • 箭头函数没有原型属性prototype
  • 不能用作构造函数,即 new 指令
let obj = {
say() {
let f1 = () => console.log(this);
f1();
}
};
let res = obj.say;
res(); // f1执行时,say函数指向window,所以f1中的this指向window
obj.say(); // f1执行时,say函数指向obj,所以f1中的this指向obj

对象扩展

对象简写

  • 属性的简写

条件:属性的值是一个变量,且变量名称和键名是一致的

let name = 'zgh';
let age = 22;

// ES5写法
let obj = { name: name, age: age };

// ES6写法
let obj = { name, age };
  • 方法的简写
// ES5写法
let obj = {
hello: function () {
console.log('hello');
}
};

// ES6写法
let obj = {
hello() {
console.log('hello');
}
};

Map

Map 是一种用来存储键值对的数据结构。类似于对象

// 创建一个Map实例
let myMap = new Map();

// 添加键值对
myMap.set('a', 'hello');
myMap.set([1, 2, 3], { name: 'zgh' });

// 也可以在声明时就添加键值对,二维数组
const user = new Map([
['foo', 'zgh'],
['baz', 23]
]);

// 查看集合中元素的数量
myMap.size;

// 获取相应的键值
myMap.get('a');

// 删除一个键值对
myMap.delete('a');

// 判断该键值对是否存在
myMap.has('a');

// 删除集合中所有的键值对
myMap.clear();

// 可以遍历
myMap.forEach((value, key) => {
console.log(key + ': ' + value);
});

Map 和 Object 有什么不同?

  • 二者都属于键值对结构
  • 对象的键名只能是String或者Symbol类型,而 Map 的键可以是任意类型的值
  • 对象可以从原型链继承属性和方法,而 Map 不具备继承性

Map 的使用场景

1、缓存

Map 可以用来缓存一些计算结果,避免重复计算。比如缓存斐波那契数列的结果。

const fibCache = new Map();
function fibonacci(n) {
if (n < 2) {
return n;
}
if (fibCache.has(n)) {
return fibCache.get(n);
}
const result = fibonacci(n - 1) + fibonacci(n - 2);
fibCache.set(n, result);
return result;
}

console.log(fibonacci(6)); // 8

2、数据结构

Map 可以用作一些数据结构的基础,比如字典、哈希表等。例如实现哈希表时,可以使用 Map 来存储键值对。

class HashTable {
constructor() {
this.table = new Map();
}

put(key, value) {
this.table.set(key, value);
}

get(key) {
return this.table.get(key);
}

remove(key) {
this.table.delete(key);
}
}

const hashObj = new HashTable();
hashObj.put('a', 1);
console.log(hashObj.get('a'));
console.log(hashObj);

3、状态管理

Map 可以用于管理应用程序的状态。例如在 React 中,可以使用 Map 来存储组件的状态(这里只是例子,在 React 中实际上不要这么做!)

class Counter extends React.Component {
constructor(props) {
super(props);
this.state = new Map([['count', 0]]);
}

increment() {
this.setState(state => {
let count = state.get('count') + 1;
return new Map([['count', count]]);
});
}

render() {
return (
<div>
Count: {this.state.get('count')}
<button onClick={() => this.increment()}>Increment</button>
</div>
);
}
}

关于 Map 的编程题

1、编写一个函数,接受一个数组作为参数,返回一个 Map,其中键为数组中的元素,值为元素在数组中出现的次数。例如:

countOccurrences([1, 2, 3, 2, 3, 3]); // Map { 1 => 1, 2 => 2, 3 => 3 }

2、编写一个函数,接受一个 Map 作为参数,返回一个由 Map 的键值对颠倒后的新 Map。例如:

invertMap(
new Map([
['a', 1],
['b', 2],
['c', 3]
])
); // Map { 1 => 'a', 2 => 'b', 3 => 'c' }

3、编写一个函数,接受两个 Map 作为参数,返回一个新 Map,其中包含两个 Map 的所有键值对。例如:

mergeMaps(
new Map([
['a', 1],
['b', 2]
]),
new Map([
['c', 3],
['d', 4]
])
); // Map { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }

4、编写一个函数,接受一个 Map 作为参数,返回一个新 Map,其中包含原始 Map 中所有值大于 10 的键值对。例如:

filterMap(
new Map([
['a', 5],
['b', 10],
['c', 15]
])
); // Map { 'c' => 15 }

5、编写一个函数,接受一个 Map 作为参数,返回一个新 Map,其中包含原始 Map 中所有键值对的值的平方。例如:

mapValues(
new Map([
['a', 2],
['b', 3],
['c', 4]
])
); // Map { 'a' => 4, 'b' => 9, 'c' => 16 }

6、编写一个函数,接受一个 Map 作为参数,返回一个新 Map,其中包含原始 Map 中所有键值对的值的和。例如:

sumValues(
new Map([
['a', 2],
['b', 3],
['c', 4]
])
); // 9

7、编写一个函数,接受一个 Map 作为参数,返回一个新 Map,其中包含原始 Map 中所有键值对的键和值的乘积。例如:

multiplyKeysAndValues(
new Map([
['a', 2],
['b', 3],
['c', 4]
])
); // Map { 'a' => 2, 'b' => 6, 'c' => 12 }

8、编写一个函数,接受两个 Map 作为参数,返回一个新 Map,其中包含原始 Map1 中所有键值对的键和 Map2 中对应键的值的乘积。例如:

multiplyMaps(
new Map([
['a', 2],
['b', 3],
['c', 4]
]),
new Map([
['a', 10],
['c', 20]
])
); // Map { 'a' => 20, 'c' => 80 }

9、编写一个函数,接受一个 Map 和一个回调函数作为参数,对于 Map 中的每个键值对,使用回调函数将键和值进行操作,并返回一个新 Map。例如:

mapMapValues(
new Map([
['a', 2],
['b', 3],
['c', 4]
]),
(key, value) => [key.toUpperCase(), value * 2]
); // Map { 'A' => 4, 'B' => 6, 'C' => 8 }

10、编写一个函数,接受一个 Map 和一个数组作为参数,将数组中的元素作为键,Map 中对应键的值作为值,返回一个新的 Map。例如:

mapFromArray(
new Map([
['a', 2],
['b', 3],
['c', 4]
]),
['a', 'c']
); // Map { 'a' => 2, 'c' => 4 }

WeakMap

WeakMap 是一种键必须是对象的 Map。WeakMap 的键是弱引用的,如果键所引用的对象被垃圾回收,则键值对会被自动删除。

特点:

  • 键必须是对象,值可以是任意类型
  • 键是弱引用,键所引用的对象可以被垃圾回收
  • 没有 clear 方法,不能遍历其中的键值对
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, 'value');

console.log(weakMap);
console.log(weakMap.get(obj)); // 'value'

obj = null; // obj 被垃圾回收,weakMap 会自动清除该键值对

Set

Set 是一种存储唯一值的数据结构。类似于数组

Set()接受具有iterable可迭代接口的数据结构作为参数,如数组、类数组、字符串等。不能接受对象结构,否则报错。

// 声明一个Set实例
let mySet = new Set();
let mySet2 = new Set([1, 2, 3]);

// 添加元素
mySet.add(1);
mySet.add('hi');
mySet.add([2, 'hello']);

// 判断集合中是否存在一个元素1
mySet.has(1); // true

// 删除集合中的字符串
mySet.delete('hi');

// 获取集合中元素的数量
mySet.size; // 3

// 删除集合中所有的元素
mySet.clear();

// 两个对象是不相等的
const set2 = new Set();
set2.add({});
set2.size; // 1
set2.add({});
set2.size; // 2

遍历操作

let mySet = new Set(['a', 'b', 'c']);

// 遍历
mySet.forEach(item => console.log(item));

// entries()返回的遍历器同时包括键名和键值,二者一样
for (let i of mySet.entries()) {
console.log(i);
}
// ["a", "a"]
// ["b", "b"]
// ["c", "c"]

// keys()返回键名
for (let i of mySet.keys()) {
console.log(i);
}
// 'a'
// 'b'
// 'c'

// values()返回键值,结果同keys()
for (let i of mySet.values()) {
console.log(i);
}

数据去重

Set 只存储唯一值,可给数组去重:

let arr = [1, 1, 2, 2, 3, 3];
let res1 = [...new Set(arr)]; // [1, 2, 3]

// 或者使用 Array.from()
let res2 = Array.from(new Set(arr)); // [1, 2, 3]

也可以给字符串去重:

const str = [...new Set('ababbc')].join('');
console.log(str); // 'abc'

交集、并集、差集

intersection() 求交集,返回一个新集合

const mySet1 = new Set([1, 3, 5, 7, 9]);
const mySet2 = new Set([1, 4, 9]);
const resSet = mySet1.intersection(mySet2);
console.log(resSet); // Set(2) { 1, 9 }

nunion() 求并集

const evens = new Set([2, 4, 6, 8]);
const squares = new Set([1, 4, 9]);
console.log(evens.union(squares)); // Set(6) { 2, 4, 6, 8, 1, 9 }

difference() 求差集

const odds = new Set([1, 3, 5, 7, 9]);
const squares = new Set([1, 4, 9]);
console.log(odds.difference(squares)); // Set(3) { 3, 5, 7 }

symmetricDifference() 返回一个包含此集合或给定集合中的元素的新集合,但不包含同时存在于这两个集合中的元素。

const evens = new Set([2, 4, 6, 8]);
const squares = new Set([1, 4, 9]);
console.log(evens.symmetricDifference(squares)); // Set(5) { 2, 6, 8, 1, 9 }

判断

isSubsetOf() 返回一个布尔值,指示此集合中的所有元素是否都在给定的集合中。

const fours = new Set([4, 8, 12, 16]);
const evens = new Set([2, 4, 6, 8, 10, 12, 14, 16, 18]);
console.log(fours.isSubsetOf(evens)); // true

isSupersetOf() 返回一个布尔值,指示给定集合中的所有元素是否都在此集合中。

const evens = new Set([2, 4, 6, 8, 10, 12, 14, 16, 18]);
const fours = new Set([4, 8, 12, 16]);
console.log(evens.isSupersetOf(fours)); // true

isDisjointFrom() 返回一个布尔值,指示此集合是否与给定集合没有公共元素。

const primes = new Set([2, 3, 5, 7, 11, 13, 17, 19]);
const squares = new Set([1, 4, 9, 16]);
console.log(primes.isDisjointFrom(squares)); // true

WeakSet

WeakSet 是一种类似于 Set 的数据结构,但是其成员必须是对象,并且这些对象都是弱引用的。

特点:

  • 成员必须是对象
  • 成员是弱引用,成员对象可以被垃圾回收
  • 没有 clear 方法,不能遍历其中的成员
let weakSet = new WeakSet();
let obj = {};
weakSet.add(obj);

console.log(weakSet.has(obj)); // true
obj = null; // obj 被垃圾回收,weakSet 会自动清除该成员

扩展操作符

...可以叫做 spread(扩展)或者 rest(剩余)操作符

剩余运算符一般会用在函数的参数里面。比如想让一个函数支持更多的参数,参数的数量不受限制,这个时候就可以使用剩余操作符

function Name(x, y, ...z) {
console.log(x); // a
console.log(y); // b
console.log(z); // ["c", "d", "e"]
}
Name('a', 'b', 'c', 'd', 'e');

剩余操作符后面的变量会变成一个数组,多余的参数会被放入这个数组中

扩展运算符用在数组的前面,作用就是将这个数组展开

const arr1 = ['a', 'b', 'c', 'd', 'e'];
const arr2 = ['f', 'g'];
const arr3 = [...arr1, ...arr2]; // ["a", "b", "c", "d", "e", "f", "g"]

// 等同于concat
const arr4 = arr1.concat(arr2);

展开对象:

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // {a: 1, b: 2, c: 3}

const obj3 = { a: 1, b: 2, c: 3 };
const { a, ...x } = obj3;
console.log(a); // 1
console.log(x); // {b: 2, c: 3}

使用扩展运算符展开一个新的对象,第二个对象的属性值会覆盖第一个对象的同名属性值

const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { b: 30, c: 40, d: 50 };
const merged = { ...obj1, ...obj2 }; // {a: 1, b: 30, c: 40, d: 50}

Proxy

外界对目标对象的访问可以被 Proxy 拦截,进行过滤和改写,意为「代理器」

let proxy = new Proxy(target, handler);
  • target 目标对象
  • handler 配置对象

在 ES6 之前,可以使用Object.defineProperty去保护对象的私有属性。例如:

let sign = { _appid: '12345678', _appkey: '666', desc: 'zgh的密钥' };

Object.defineProperties(sign, {
_appid: {
writable: false
},
_appkey: {
writable: false
}
});

但是如果想对多个属性进行保护,就得对多个属性进行声明writable: false,显然很麻烦,这时就可以用 Proxy 来解决这个问题。

Proxy 意味着我们代理了这个对象,该对象所有的属性操作都会经过 Proxy

let sign = { _appid: '123456', _appkey: '666', desc: 'zgh的密钥' };
let signProxy = new Proxy(sign, {
get(target, property, receiver) {
return target[property];
},
set(target, propName, value, receiver) {
if (propName !== 'desc') {
console.log('该属性是私有属性,不允许修改!');
} else {
target[propName] = value;
}
}
});
console.log(signProxy._appid); // "123456"
signProxy._appkey = 'dd'; // 该属性是私有属性,不允许修改!
console.log(signProxy._appkey); // "666"

这时依然可以直接修改 sign 对象,如果希望对象完全不可修改,可以直接将 sign 写到 Proxy 的 target

应用场景:

  • 数据校检
  • 属性保护

Proxy 的 this 问题:

如果 target 对象存在 this,那么不做任何拦截的情况下,target 的 this 所指向的是 target,而不是代理对象 proxy

const target = {
m() {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);

target.m(); // false
proxy.m(); // true

示例:数据类型验证

有一个记账的对象,记录着用户的存款金额,为了方便以后计算,要保证存入的数据类型必须为Number

let account = { num: 8888 };

let proxyAccount = new Proxy(account, {
get(target, property) {
return target[property];
},
set(target, propName, propValue) {
if (propName === 'num' && typeof propValue != 'number') {
throw new TypeError('The num is not an number');
}
target[propName] = propValue;
}
});

proxyAccount.num = '666';
console.log(proxyAccount.num); // Uncaught TypeError: The num is not an number

Reflect 反射

JS 反射是一种能够在运行时检查、修改对象、类和函数等程序的能力。通过反射可以读取和修改对象属性、调用对象方法、定义新属性、修改原型等。

// 获取对象的属性名称列表
const obj = { a: 1, b: 2, c: 3 };
console.log(Reflect.ownKeys(obj)); // [ 'a', 'b', 'c' ]

// 验证属性存在
const obj2 = { a: 1 };
console.log(Reflect.has(obj2, 'a')); // true

// 获取对象的原型
const obj3 = { a: 1 };
console.log(Reflect.getPrototypeOf(obj3));

// 修改对象的原型
const obj4 = { a: 1 };
const proto = { b: 2 };
Reflect.setPrototypeOf(obj4, proto);
console.log(obj4.b); // 2

// 代替call和apply方法
function fn(a, b, c) {
console.log(a, b, c);
}
Reflect.apply(fn, null, [1, 2, 3]); // 1 2 3

// 获取对象的属性描述符
const obj5 = { a: 1 };
// { value: 1, writable: true, enumerable: true, configurable: true }
console.log(Reflect.getOwnPropertyDescriptor(obj5, 'a'));

// 使用set函数和get函数拦截属性的读取和赋值操作
const obj6 = { a: 1 };
const handler = {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
}
};
const proxy = new Proxy(obj6, handler);
console.log(proxy.a); // 1
proxy.a = 2;
console.log(proxy.a); // 2

空值合并运算符

  • 写法:a ?? b
  • 如果左侧参数是nullundefined,则??返回其右侧参数,否则返回其左侧参数
  • 效果等同于(a !== null && a !== undefined) ? a : b
const a = null ?? 'hi'; // hi

const b = 0 ?? 42; // 0
  • ??运算符的优先级非常低,仅略高于 ?=,使用时要考虑是否添加括号
  • 如果没有明确添加括号,不能将其与||&&一起使用

||的区别

  • || 返回第一个真值,?? 返回第一个已定义的值
  • || 无法区分 false0、空字符串""NaNnullundefined
let a = 0;
a || 1; // 1
a ?? 1; // 0

补充:双感叹号!!

双感叹号确保结果类型是布尔类型

!0; // true

!undefined; // true

!null; // true

!''; // true

!!0; // false

!!undefined; // false

!!null; // false

!!''; // false

可选链

当位于 ?. 前面的值为 undefinednull 时,会立即阻止代码的执行,并返回 undefined

const obj = { name: 'zgh' };
obj?.a;

可选链的三种形式:

  • obj?.pron
  • obj?.[pron]
  • obj.method?.()

假设有表达式为:left ?? right

  • 当 left 是:0、''、false,会返回 left 的值
  • 当 left 是 null、undefined,会返回 right 的值

||??的区别是:当 left 是 0、''、false 时,会返回 right 的值

逻辑运算符和赋值运算符

&&=

x &&= y等价于:x && (x = y),当 x 为真时,x = y

||=

x ||= y等价于:x || (x = y),仅在 x 为 false 的时候,x = y

??=

x ??= y 等价于 x ?? (x = y),仅在 x 为 null 或 undefined 的时候,x = y