2-3、ES6 详解

const

基础

用于定义常量的,初始化时必须赋值
特点:

  1. 不允许重复赋值
    1. 对于基础类型后续的值不可更改
    2. 对于复杂类型后续的引用地址不可更改,但其中的值可以更改
  2. 有块级作用域
  3. 存在暂时性死区
1
2
3
4
5
6
7
8
9
10
11
12
const LIMIT = 10;
const OBJ_MAP = {
x: 10,
y: 20,
};
const OBJ_ARRAY = [1, 2, 3];

// LIMIT = 20; // Uncaught TypeError: Assignment to constant variable.
OBJ_MAP.z = 30; // {"x":10,"y":20,"z":30}
OBJ_ARRAY.push(4); // [1, 2, 3, 4]
OBJ_MAP = { a: 10 }; // Uncaught TypeError: Assignment to constant variable.
OBJ_ARRAY = [9]; // Uncaught TypeError: Assignment to constant variable.

面试点

在 ES5 中,如何模拟一个常量的[不允许重复赋值]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var NUMBER = 10;
NUMBER = 20; // 不报错,代码有效的

// 模拟操作 - Object.defineProperty(...)

// 无报错版
Object.defineProperty(window, "NUMBER2", {
value: 10,
});

console.log("[ NUMBER2 ] >", NUMBER2); // 10

NUMBER2 = 20; // 不报错

console.log("[ NUMBER2 ] >", NUMBER2); // 10

// 有报错版
Object.defineProperty(window, "NUMBER3", {
get() {
return 10;
},
set() {
throw new Error("Assignment to constant variable");
},
});

console.log("[ NUMBER3 ] >", NUMBER3); // 10

NUMBER3 = 20; // 报错: Uncaught Error: Assignment to constant variable

console.log("[ NUMBER3 ] >", NUMBER3); // 10

在 ES5 中,如何模拟块级作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (true) {
var xsdw = 10;
}
console.log("[ xsdw ] >", xsdw); // 10

if (true) {
const ghnm = 10;
}
console.log("[ ghnm ] >", ghnm); // 报错:Uncaught ReferenceError: ghnm is not defined

// 模拟操作 - IFEE
if (true) {
(function () {
var iws = 10;
})();
}
console.log("[ iws ] >", iws); // 报错:Uncaught ReferenceError: iws is not defined

对于复杂类型,是用 const 还是 let?

答案:const
对于复杂类型,用 const 后将不允许对其重新覆盖(引用地址不可变),但不会限制值的更改
一句话:能用 const 的坚决不用 let

如何对复杂类型常量化

考察点:Object.freeze(obj)
官方文档:Object.freeze() - JavaScript | MDN
被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定
缺点:只能冻结根层属性,对内嵌的复杂类型无法冻结
解决方案:针对多层嵌套的复杂类型,手动循环冻结

1
2
3
4
5
6
7
8
9
10
11
function deepFreeze(_obj) {
const _freeze = Object.freeze(_obj);

Object.keys(_freeze).forEach((key) => {
if (_freeze[key] instanceof Object) {
_freeze[key] = deepFreeze(_freeze[key]);
}
});

return _freeze;
}

解构

解开结构
对象解构:{}
数组解构: []

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const obj = { name: 'lisi', age: 25 }
const { name, age } = obj
// name: 'lisi'
// age: 25

const { name, ...obj2 } = obj // 剩余解构:剩余的以对象形式的放到 obj2 内,不再包含原型链
// name: 'lisi'
// obj2: { age: 25 }

const arr = [100, 200, 300]
const [a, b, c] = arr
// a: 100
// b: 200
// c: 300

const [a, ...b] = arr // 剩余解构:剩余的以数组形式的放到 b 内,不再包含原型链
// a: 100
// b: [200, 300]

解构操作的本质算是取值的语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { name, age } = obj
// 等价于
const name = obj.name
const age = obj.age

const { name, ...obj2 } = obj
// 等价于
const name = obj.name
const obj2 = Object.create({})
obj2.age = obj.age
// 更多属性...

const [a, b, c] = arr
// 等价于
const a = arr[0]
const b = arr[1]
const c = arr[2]

const [a, ...b] = arr
// 等价于
const a = arr[0]
const b = arr.slice(1)

数组解构能很方便的进行变量交换

1
2
3
4
5
6
7
8
9
10
11
12
// 以前的逻辑
let hj = 1;
let jh = 2;
let _c = 0;
_c = hj;
hj = jh;
jh = _c;

// 运用数组解构后
let kj = 1;
let jk = 2;
[kj, jk] = [jk, kj];

箭头函数

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 普通函数
function add(x, y) {
return x + y
}
const add = function(x, y) {
return x + y
}

// 箭头函数
const add = (x, y) => {
return x + y
}
// 等价于
const add = (x, y) => x + y

特点

  1. 没有自己的 this,它的 this 取决于定义时上下文
    1. 所以就不能用于构造函数
  2. 没有内置的 arguments

class

基础

常用语法

1
2
3
4
5
6
7
8
9
10
11
// class 声明
class Person {
x: 10 // 属性
getX(){} // 方法
}

// class 表达式
const Person2 = class {
x: 10 // 属性
getX(){} // 方法
}

完整语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// class 声明:不允许再次声明已经存在的类;不可以提升
class name [extends otherClassName] {
// class body
[key]: value // 属性,将作为实例本身的属性
[key]() {} // 方法,,将作为实例原型上的方法,实例本身不具备该的方法
static [key]: any // 静态[属性/方法](不可枚举),作为类本身的[属性/方法]
}


// class 表达式:可以是命名的,也可以是匿名的;允许你重新定义类
const MyClass = class [className] [extends otherClassName] {
// class body
[key]: value // 属性,将作为实例本身的属性
[key]() {} // 方法,,将作为实例原型上的方法,实例本身不具备该的方法
static [key]: any // 静态[属性/方法](不可枚举),作为类本身的[属性/方法]
}

场景模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// class 时代
// 类声明
class Course {
constructor(teacher, course) {
// 作为实例的属性
this.teacher = teacher;
this.course = course;
}

// 作为实例的属性
age = 29

// 作为实例原型链上的方法
getCourse() {
return this.teacher + ":" + this.course;
}

// 作为类的方法
static getXX() {}
}
// 实例 1
const course3 = new Course("xx", "ES6");
// 实例 2
const course4 = new Course("yy", "ES7");
console.log("[ course3.getCourse() ] >", course3.getCourse()); // xx:ES6
console.log("[ course4.getCourse() ] >", course4.getCourse()); // yy:ES7

// function 时代
// 构造函数声明
function Course(teacher, course) {
// 作为实例的属性
this.teacher = teacher;
this.course = course;
this.age = 29
}
// 作为实例原型链上的方法
Course.prototype.getCourse = function () {
return this.teacher + ":" + this.course;
};
// 作为构造函数的方法
Course.getXX = function() {}

// 实例 1
const course1 = new Course("xx", "ES6");
// 实例 2
const course2 = new Course("yy", "ES7");
console.log("[ course1.getCourse() ] >", course1.getCourse()); // xx:ES6
console.log("[ course2.getCourse() ] >", course2.getCourse()); // yy:ES7

使用场景

适配器模式,封装核心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 class Utils {
constructor(core) {
this._main = core;
this._name = "my_utils_name";
}

get name() {
return this._name || this._main.name;
}

get core() {
return this._main;
}
}

// 封装了下 lodash
const utils = new Utils(_);

面试点

class 的类型是什么?

typeof class'function'

Promise

处理异步的一种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Promise 的 A+ 标准源码
const PENDING = "PENDING";
const REJECTED = "REJECTED";
const FULFILLED = "FULFILLED";

class myPromise {
value = null;
error = null;
status = PENDING;

onFulfilledCallbacks = [];
onRejectedCallbacks = [];

constructor(executor) {
const resolve = (value) => {
this.value = value;
this.status = FULFILLED;
this.onFulfilledCallbacks.forEach((fn) => fn());
};

const reject = (error) => {
this.error = error;
this.status = REJECTED;
this.onRejectedCallbacks.forEach((fn) => fn());
};

try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}

then = (onFulfilled, onRejected) => {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
onRejected =
typeof onRejected === "function"
? onRejected
: (error) => {
throw error;
};

const promise2 = new myPromise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
}

if (this.status === PENDING) {
this.onFulfilledCallbacks.push(
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0)
);

this.onRejectedCallbacks.push(
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0)
);
}
});

return promise2;
};
}

function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError("循环引用"));
}

let called = false;

if ((typeof x === "object" && x !== null) || typeof x === "function") {
try {
let then = x.then;
if (typeof then === "function") {
then.call(
x,
(y) => {
if (called) return;
called = true;

resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;

reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;

reject(e);
}
} else {
resolve(x);
}
}

new myPromise((resolve, reject) => {
resolve(1);
}).then((res) => {
console.log(res);
});

ES6(ECMAScript 2015)新增的数组和对象方法有很多,以下是一些常用的方法:

数组方法

官方文档:Array - JavaScript | MDN
ES6(ECMAScript 2015)新增的数组些常用的方法:

  1. Array.from(): 用于将类数组对象或可遍历对象转为真正的数组。
  2. Array.of(): 用于将一组值转为数组。
    1. Array.of('foo', 2, 'bar', true);// ["foo", 2, "bar", true]
  3. copyWithin(): 用于在当前数组内部,将指定位置的成员复制到其他位置。
  4. fill(): 用于填充数组。
  5. find(): 返回数组中第一个满足条件的元素。
  6. findIndex(): 返回数组中第一个满足条件的元素的下标。
  7. includes(): 检索数组是否包含某个值,返回布尔值。
  8. entries(): 帮助数组遍历每一个 key 值与 value 值。
  9. keys(): 帮助数组遍历所有的 key 值。
  10. values(): 帮助数组遍历所有的 value 值。

对象方法

官方文档:Object - JavaScript | MDN
ES6(ECMAScript 2015)新增的对象常用的方法:

  1. Object.assign(): 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。
  2. Object.keys(): 返回一个由给定对象的自身可枚举属性组成的数组。
  3. Object.getOwnPropertyNames(): 返回一个由指定对象的所有自身属性(包括不可枚举属性但不包括 Symbol 值作为名称的属性)的属性名(包括不可枚举属性)组成的数组。
  4. Object.getOwnPropertySymbols(): 返回一个由指定对象的所有自身 Symbol 属性值的属性名组成的 Array。
  5. Object.is(): 用来检测两个值是否是同一个对象
  6. Object.setPrototypeOf(): 设置一个对象的内部[[Prototype]]链接。
  7. Object.values(): 返回一个由给定对象的所有自身属性的值组成的数组。
  8. Object.entries(): 返回一个由给定对象的所有自身属性的键值对组成的数组。
  9. Object.get(): 获取对象的属性值。
  10. Object.set(): 设置对象的属性值并返回该对象。
  11. Object.create(): 使用给定的原型和可选的属性描述符创建一个新的对象。
  12. Object.defineProperty(): 在对象上定义一个新的属性,或修改一个对象的现有属性,并返回这个对象。
  13. Object.defineProperties(): 在一个对象上定义一个直接以该对象为基础的对象的新属性和/或修改对象的现有属性的性质。
  14. Object.freeze(): 防止对象的属性被更改,并防止对象被扩展。
  15. Object.seal(): 防止对象的属性被删除,并防止新的属性被添加到对象中。
  16. Object.preventExtensions(): 防止新属性添加到对象中,但允许现有属性的修改。

扩展

尾调用

一个函数执行结果作为另一个函数的返回值

1
2
3
4
5
6
7
8
function A(x) {
return x + 1
}
function B(number) {
return A(number)
}
const num = B(1)
// 一个函数执行结果(A)作为另一个函数(B)的返回值(return)

尾调用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 斐波那契函数 - 递归尾调用
function fib(n) {
if (n < 2) {
return n;
} else {
return fib(n - 1) + fib(n - 2); // 递归 + 计算
}
}

// 由于是[递归 + 计算],调用时会存在 压栈 的情况
// fib 每次调用时,都会进栈,等 fib 执行完毕再后出栈,但由于是递归调用的,如果 n 很大
// 则栈中存在大量的 fib,则形成了 压栈
// 改成如下形式则可以优化

function fib(n) {
return fibImpl(0, 1, n)
}
function fibImpl(a, b, n) {
if(a === 0) {
return a
}
return fibImpl(b, a + b, n - 1) // 自递归,JS 会进行尾调用优化
}

// 尾调用优化
// 如果是单纯的自递归,没有计算的话,JS 就会自己进行尾调用优化,优化如下:
// 递归调用不会保留任何状态或局部变量,因此可以重用当前的调用栈帧,而无需创建新的栈帧
// 即 重复使用一个栈帧

2-3、ES6 详解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/2-3、ES6 详解/
作者
黄智强
发布于
2024年1月13日
许可协议