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
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
模板字符串
模板字符串是增强版的字符串,用反引号\
标识,嵌入的变量名写在${}
之中。
基础使用
- 基本的字符串格式化
const name = 'world';
// ES5
console.log('hello' + name);
// ES6
console.log(`hello${name}`);
- 多行字符串拼接
let say = `<div>
<p>hello, world</p>
</div>`;
标记模版
标记模版(Tagged templates),使用函数解析模版文字。标签函数的第一个参数是一个字符串值数组,其余参数和表达式相关。
在 React 项目中,常用CSS in JS
方案管理样式,如Emotion、Styled-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
对象与数组解构的不同点:
- 数组的元素是按次序排列的,变量的取值由它的位置决定
- 对象的属性没有次序,变量必须与属性同名,才能取到正确的值
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 指向
箭头函数本身是没有this
和arguments
的,在箭头函数中引用 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
- 如果左侧参数是
null
或undefined
,则??
返回其右侧参数,否则返回其左侧参数 - 效果等同于
(a !== null && a !== undefined) ? a : b
const a = null ?? 'hi'; // hi
const b = 0 ?? 42; // 0
??
运算符的优先级非常低,仅略高于?
和=
,使用时要考虑是否添加括号- 如果没有明确添加括号,不能将其与
||
或&&
一起使用
与||
的区别
||
返回第一个真值,??
返回第一个已定义的值||
无法区分false
、0
、空字符串""
、NaN
、null
、undefined
let a = 0;
a || 1; // 1
a ?? 1; // 0
补充:双感叹号!!
双感叹号确保结果类型是布尔类型
!0; // true
!undefined; // true
!null; // true
!''; // true
!!0; // false
!!undefined; // false
!!null; // false
!!''; // false
可选链
当位于 ?.
前面的值为 undefined
或 null
时,会立即阻止代码的执行,并返回 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