最后
本人分享一下这次字节跳动、美团、头条等大厂的面试真题涉及到的知识点,以及我个人的学习方法、学习路线等,当然也整理了一些学习文档资料出来是给大家的。知识点涉及比较全面,包括但不限于前端基础,HTML,CSS,JavaScript,Vue,ES6,HTTP,浏览器,算法等等
前端视频资料:
- 如果
async
函数里的多个异步操作之间没有依赖关系,建议将他们写到一起减少执行时间:
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 写法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
共享内存和Atomics对象
-
SharedArrayBuffer
-
Atomics
ES2018
Promise.prototype.finally()
Promise.prototype.finally()
用于给 Promise
对象添加 onFinally
函数,这个函数主要是做一些清理的工作,只有状态变化的时候才会执行该 onFinally
函数。
function onFinally() {
console.log(888) // 并不会执行
}
new Promise((resolve, reject) => {
}).finally(onFinally)
finally()
会生成一个 Promise
新实例,finally 一般会原样后传父 Promise,无论父级实例是什么状态:
let p1 = new Promise(() => {})
let p2 = p1.finally(() => {})
setTimeout(console.log, 0, p2) // Promise {}
let p3 = new Promise((resolve, reject) => {
resolve(3)
})
let p4 = p3.finally(() => {})
setTimeout(console.log, 0, p3) // Promise {: 3}
上面说的是一般,但是也有特殊情况,比如 finally
里返回了一个非 fulfilled
的 Promise
或者抛出了异常的时候,则会返回对应状态的新实例:
let p1 = new Promise((resolve, reject) => {
resolve(3)
})
let p2 = p1.finally(() => new Promise(() => {}))
setTimeout(console.log, 0, p2) // Promise {}
let p3 = p1.finally(() => Promise.reject(6))
setTimeout(console.log, 0, p3) // Promise {: 6}
let p4 = p1.finally(() => {
throw new Error(‘error’)
})
setTimeout(console.log, 0, p4) // Promise {: Error: error}
参考:
- 深入理解Promise
异步迭代器
想要了解异步迭代器最好的方式就是和同步迭代器进行对比。我们知道可迭代数据的内部都是有一个 Symbol.iterator
属性,它是一个函数,执行后会返回一个迭代器对象,这个迭代器对象有一个 next()
方法可以对数据进行迭代,next()
执行后会返回一个对象,包含了当前迭代值 value
和 标识是否完成迭代的 done
属性:
let iterator = [1, 2]Symbol.iterator
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: undefinde, done: true }
上面这里的 next()
执行的是同步操作,所以这个是同步迭代器,但是如果 next()
里需要执行异步操作,那就需要异步迭代了,可异步迭代数据的内部有一个 Symbol.asyncIterator
属性,基于此我们来实现一个异步迭代器:
class Emitter {
constructor(iterable) {
this.data = iterable
}
let length = this.data.length,
index = 0;
return {
next:() => {
const done = index >= length
const value = !done ? this.data[index++] : undefined
return new Promise((resolve, reject) => {
resolve({value, done})
})
}
}
}
}
异步迭代器的 next()
会进行异步的操作,通常是返回一个 Promise
,所以需要对应的处理函数去处理结果:
let emitter = new Emitter([1, 2, 3])
let asyncIterator = emitterSymbol.asyncIterator
asyncIterator.next().then(res => {
console.log(res) // { value: 1, done: false }
})
asyncIterator.next().then(res => {
console.log(res) // { value: 2, done: false }
})
asyncIterator.next().then(res => {
console.log(res) // { value: 3, done: false }
})
另外也可以使用 for await...of
来迭代异步可迭代数据:
let asyncIterable = new Emitter([1, 2, 3])
async function asyncCount() {
for await (const x of asyncIterable ) {
console.log(x)
}
}
asyncCount()
// 1 2 3
另外还可以通过异步生成器来创建异步迭代器:
class Emitter {
constructor(iterable) {
this.data = iterable
}
async *Symbol.asyncIterator {
let length = this.data.length,
index = 0;
while (index < length) {
yield this.data[index++]
}
}
}
async function asyncCount() {
let emitter = new Emitter([1, 2, 3])
const asyncIterable = emitterSymbol.asyncIterator
for await (const x of asyncIterable ) {
console.log(x)
}
}
asyncCount()
// 1 2 3
参考:
-
Iteration_protocols
-
for-await…of
s修饰符(dotAll模式)
正则表达式新增了一个 s
修饰符,使得 .
可以匹配任意单个字符:
/foo.bar/.test(‘foo\nbar’) // false
/foo.bar/s.test(‘foo\nbar’) // true
上面这又被称为 dotAll
模式,表示点(dot
)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式:
/foo.bar/s.dotAll // true
具名组匹配
正则表达式可以使用捕获组来匹配字符串,但是想要获取某个组的结果只能通过对应的索引来获取:
let re = /(\d{4})-(\d{2})-(\d{2})/
let result = re.exec(‘2015-01-02’)
// result[0] === ‘2015-01-02’
// result[1] === ‘2015’
// result[2] === ‘01’
// result[3] === ‘02’
而现在我们可以通过给捕获组 (?<name>...)
加上名字 name
,通过名字来获取对应组的结果:
let re = /(?\d{4})-(?\d{2})-(?\d{2})/
let result = re.exec(‘2015-01-02’)
// result.groups.year === ‘2015’
// result.groups.month === ‘01’
// result.groups.day === ‘02’
配合解构赋值可以写出非常精简的代码:
let {groups: {year, month, day}} = /(?\d{4})-(?\d{2})-(?\d{2})/.exec(‘2015-01-02’)
console.log(year, month, day) // 2015 01 02
具名组也可以通过传递给 String.prototype.replace
的替换值中进行引用。如果该值为字符串,则可以使用 $<name>
获取到对应组的结果:
let re = /(?\d{4})-(?\d{2})-(?\d{2})/
let result = ‘2015-01-02’.replace(re, ‘ < d a y > / <day>/ <day>//$’)
// result === ‘02/01/2015’
参考:
- proposal-regexp-named-groups
后行断言
后行断言:(?<=y)x
,x
只有在 y
后面才能匹配:
/(?<=$)\d+/.exec(‘I have $100.’) // [‘100’]
后行否定断言:(?<!y)x
,x
只有不在 y
后面才能匹配:
/(?<!$)\d+/.exec(‘I have $100.’) // [‘00’]
Unicode属性转义
允许正则表达式匹配符合 Unicode
某种属性的所有字符,\p{...}
是匹配包含,\P{...}
是匹配不包含的字符,且必须搭配 /u
修饰符才会生效:
/\p{Emoji}+/u.exec(‘???笑死我了???不行了’) // [‘???’]
/\P{Emoji}+/u.exec(‘???笑死我了???不行了’) // [‘笑死我了’]
这里可以查询到更多的 Unicode
的属性 Full_Properties
对象扩展运算符
对象的扩展运算符可以用到解构赋值上,且只能应用到最后一个变量上:
let {x, …y} = {x: 1, a: 2, b: 3}
console.log(y) // {a: 2, b: 3}
对象扩展运算符不能解构原型上的属性:
let obj = { x: 1 }
obj.proto = { y: 2 }
let {…a} = obj
console.log(a.y) // undefined
应用一:可以实现浅拷贝,但是不会拷贝原始属性:
let person = Object.create({ name: ‘布兰’ })
person.age = 12
// 浅拷贝写法一
let { …pClone1 } = person
console.log(pClone1) // { age: 12 }
console.log(pClone1.name) // undefined
// 浅拷贝写法二
let pClone2 = {…person}
console.log(pClone2) // { age: 12 }
console.log(pClone2.name) // undefined
应用二:合并两个对象:
let ab = {…a, …b}
// 等同于
let ab = Object.assign({}, a, b);
应用三:重写对象属性
let aWithOverrides = { …a, x: 1, y: 2 };
应用四:给新对象设置默认值
let aWithDefaults = { x: 1, y: 2, …a };
应用五:利用扩展运算符的解构赋值可以扩展函数参数:
function baseFunction({ a, b }) {}
function wrapperFunction({ x, y, …restConfig }) {
// 使用 x 和 y 参数进行操作
// 其余参数传给原始函数
return baseFunction(restConfig)
}
参考:
-
Object Spread Initializer
-
Object Rest Destructuring
放松对标签模板里字符串转义的限制
参考:
- ECMAScript 6 入门
ES2019
允许省略catch里的参数
异常被捕获的时候如果不需要做操作,甚至可以省略 catch(err)
里的参数和圆括号:
try {
} catch {
}
JSON.stringify()变动
UTF-8
标准规定,0xD800
到 0xDFFF
之间的码点,不能单独使用,必须配对使用。所以 JSON.stringify()
对单个码点进行操作,如果码点符合 UTF-8
标准,则会返回对应的字符,否则会返回对应的码点:
JSON.stringify(‘\u{1f600}’) // “”???“”
JSON.stringify(‘\u{D834}’) // “”\ud834""
Symbol.prototype.description
Symbol
实例新增了一个描述属性 description
:
let symbol = Symbol(‘foo’)
symbol.description // ‘foo’
Function.prototype.toString()
函数的 toString()
会原样输出函数定义时候的样子,不会省略注释和空格。
Object.fromEntries()
Object.fromEntries()
方法是 Object.entries()
的逆操作,用于将一个键值对数组转为对象:
let person = { name: ‘布兰’, age: 12 }
let keyValueArr = Object.entries(person) // [[‘name’, ‘布兰’], [‘age’, 12]]
let obj = Object.fromEntries(arr) // { name: ‘布兰’, age: 12 }
常用可迭代数据结构之间的装换:
let person = { name: ‘布兰’, age: 12 }
// 对象 -> 键值对数组
let keyValueArr = Object.entries(person) // [[‘name’, ‘布兰’], [‘age’, 12]]
// 键值对数组 -> Map
let map = new Map(keyValueArr) // Map {“name”: “布兰”, “age”: 12}
// Map -> 键值对数组
let arr = Array.from(map) // [[‘name’, ‘布兰’], [‘age’, 12]]
// 键值对数组 -> 对象
let obj = Array.from(arr).reduce((acc, [ key, val ]) => Object.assign(acc, { [key]: val }), {}) // { name: ‘布兰’, age: 12 }
参考:
- Object.fromEntries
字符串可直接输入行分隔符和段分隔符
JavaScript
规定有 5 个字符,不能在字符串里面直接使用,只能使用转义形式。
-
U+005C
:反斜杠(reverse solidus) -
U+000D
:回车(carriage return) -
U+2028
:行分隔符(line separator) -
U+2029
:段分隔符(paragraph separator) -
U+000A
:换行符(line feed)
但是由于 JSON
允许字符串里可以使用 U+2028
和 U+2029
,所以使得 JSON.parse()
去解析字符串的时候可能会报错,所以 ES2019
允许模板字符串里可以直接这两个字符:
JSON.parse(‘“\u2028”’) // “”
JSON.parse(‘“\u2029”’) // “”
JSON.parse(‘“\u005C”’) // SyntaxError
String.prototype.trimStart
消除字符串头部空格,返回一个新字符串;浏览器还额外增加了它的别名函数 trimLeft()
:
let str = ’ hello world ’
let newStr = str.trimStart()
console.log(newStr, newStr === str)
// 'hello world ’ false
String.prototype.trimEnd
消除字符串尾部空格,返回一个新字符串;浏览器还额外增加了它的别名函数 trimRight()
:
let str = ’ hello world ’
let newStr = str.trimEnd()
console.log(newStr, newStr === str)
// ’ hello world’ false
Array.prototype.flat()
arr.flat(depth)
按照 depth
(不传值的话默认是 1)深度拍平一个数组,并且将结果以新数组形式返回:
// depth 默认是 1
const arr1 = [1, 2, [3, 4]]
console.log(arr1.flat()) // [1, 2, 3, 4]
// 使用 Infinity,可展开任意深度的嵌套数组;自动跳过空数组;
const arr2 = [1, , [2, [3, [4]]]]
console.log(arr2.flat(Infinity))
// [1, 2, 3, 4]
用 reduce
实现拍平一层数组:
const arr = [1, 2, [3, 4]]
// 方法一
let newStr = arr.reduce((acc, cur) => acc.concat(cur), [])
// 方法二
const flattened = arr => [].concat(…arr)
flattened(arr)
参考:
- flat
Array.prototype.flatMap()
flatMap(callback)
使用映射函数 callback
映射每个元素,callback
每次的返回值组成一个数组,并且将这个数组执行类似 arr.flat(1)
的操作进行拍平一层后最后返回结果:
const arr1 = [1, 2, 3, 4]
arr1.flatMap(x => [x * 2])
// 将 [[2], [4], [6], [8]] 数组拍平一层得到最终结果:[2, 4, 6, 8]
参考:
- flatMap
ES2020
String.prototype.matchAll()
String.prototype.matchAll()
方法,可以一次性取出所有匹配。不过,它返回的是一个 RegExpStringIterator
迭代器同是也是一个可迭代的数据结构,所以可以通过 for...of
进行迭代:
let str = ‘test1test2’
let regexp = /t(e)(st(\d?))/g
let iterable = str.matchAll(regexp)
for (const x of iterable) {
console.log(x)
}
// [‘test1’, ‘e’, ‘st1’, ‘1’, index: 0, input: ‘test1test1’, groups: undefined]
// [‘test2’, ‘e’, ‘st2’, ‘2’, index: 5, input: ‘test1test2’, groups: undefined]
❝
注意当使用 matchAll(regexp) 的时候,正则表达式必须加上 /g 修饰符。
❞
也可以将这个可迭代数据转成数组形式:
// 方法一
[…str.matchAll(regexp)]
// 方法二
Array.from(str.matchAll(regexp))
动态import()
标准用法的 import
导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。
比如按需加载一个模块可以这样:
if (xxx) {
import(‘./module.js’)
}
import()
是异步导入的,结果会返回一个 Promise
:
import(‘/module.js’)
.then((module) => {
// Do something with the module.
})
动态 import()
的应用场景挺多的,比如 Vue
中的路由懒加载就是使用的动态导入组件。另外由于动态性不便于静态分析工具和 tree-shaking
工作,所以不能滥用。
BigInt
BigInt
是一种内置对象,它提供了一种方法来表示大于 - 1 的整数。这原本是 Javascript
中可以用 Number
表示的最大数字。BigInt
可以表示任意大的整数。
为了区分 Number
,定义一个 BigInt
需要在整数后面加上一个 n
,或者用函数直接定义:
const num1 = 10n
const num2 = BigInt(20)
Number
和 BigInt
之间能进行比较,但他们之间是宽松相等;且由于他们表示的是不同类型的数字,所以不能直接进行四则运算:
10n == 10 // true
10n === 10 // false
10n > 8 // true
10 + Number(10n) // 20
10 + 10n // TypeError
Promise.allSettled
Promise.allSettled(iterable)
当所有的实例都已经 settled
,即状态变化过了,那么将返回一个新实例,该新实例的内部值是由所有实例的值和状态组合成的数组,数组的每项是由每个实例的状态和内部值组成的对象。
function init(){
return 3
}
let p1 = Promise.allSettled([
new Promise((resolve, reject) => {
resolve(9)
}).then(res => {}),
new Promise((resolve, reject) => {
reject(6)
}),
init()
])
let p2 = p1.then(res => {
console.log(res)
}, err => {
console.log(err)
})
// [
// {status: “fulfilled”, value: undefined},
// {status: “rejected”, reason: 6},
// {status: “fulfilled”, value: 3}
// ]
只要所有实例中包含一个 pending
状态的实例,那么 Promise.allSettled()
的结果为返回一个这样 Promise {<pending>}
的实例。
globalThis
在以前,从不同的 JavaScript
环境中获取全局对象需要不同的语句。在 Web
中,可以通过 window
、self
或者 frames
取到全局对象,但是在 Web Workers
中,只有 self
可以。在 Node.js
中,它们都无法获取,必须使用 global
。
而现在只需要使用 globalThis
即可获取到顶层对象,而不用担心环境问题。
// 在浏览器中
globalThis === window // true
import.meta
import.meta
是一个给 JavaScript
模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的 URL
,import.meta
必须在一个模块里使用:
// 没有声明 type=“module”,就使用 import.meta 会报错
// 在module.js里
console.log(import.meta)
// {url: “http://localhost/3ag/js/module.js”}
如果需要在配置了 Webpack
的项目,比如 Vue
里使用 import.meta
需要加一个包且配置一下参数,否则项目编译阶段会报错。
包配置详情参考:
- @open-wc/webpack-import-meta-loader
比如我用的是 4.x
版本的 vue-cli
,那我需要在 vue.config.js
里配置:
module.exports = {
chainWebpack: config => {
config.module
.rule(‘js’)
.test(/.js$/)
.use(‘@open-wc/webpack-import-meta-loader’)
.loader(‘@open-wc/webpack-import-meta-loader’)
.end()
}
}
可选链操作符(?.)
通常我们获取一个深层对象的属性会需要写很多判断或者使用逻辑与 &&
操作符,因为对象的某个属性如果为 null
或者 undefined
就有可能报错:
let obj = {
first: {
second: ‘布兰’
}
}
// 写法一
let name1 = ‘’
if (obj) {
if (obj.first) {
name1 = obj.first.second
}
}
// 写法二
let name2 = obj && obj.first && obj.first.second
?.
操作符允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。如果某个属性为 null
或者 undefined
则结果直接为 undefined
。有了可选链操作符就可以使得表达式更加简明了,对于上面例子用可选链操作符可以这么写:
let name3 = obj?.first?.second
空值合并操作符(??)
对于逻辑或 ||
运算符,当对运算符左侧的操作数进行装换为 Boolean
值的时候,如果为 true
,则取左边的操作数为结果,否则取右边的操作数为结果:
最后
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
必须在一个模块里使用:
// 没有声明 type=“module”,就使用 import.meta 会报错
// 在module.js里
console.log(import.meta)
// {url: “http://localhost/3ag/js/module.js”}
如果需要在配置了 Webpack
的项目,比如 Vue
里使用 import.meta
需要加一个包且配置一下参数,否则项目编译阶段会报错。
包配置详情参考:
- @open-wc/webpack-import-meta-loader
比如我用的是 4.x
版本的 vue-cli
,那我需要在 vue.config.js
里配置:
module.exports = {
chainWebpack: config => {
config.module
.rule(‘js’)
.test(/.js$/)
.use(‘@open-wc/webpack-import-meta-loader’)
.loader(‘@open-wc/webpack-import-meta-loader’)
.end()
}
}
可选链操作符(?.)
通常我们获取一个深层对象的属性会需要写很多判断或者使用逻辑与 &&
操作符,因为对象的某个属性如果为 null
或者 undefined
就有可能报错:
let obj = {
first: {
second: ‘布兰’
}
}
// 写法一
let name1 = ‘’
if (obj) {
if (obj.first) {
name1 = obj.first.second
}
}
// 写法二
let name2 = obj && obj.first && obj.first.second
?.
操作符允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。如果某个属性为 null
或者 undefined
则结果直接为 undefined
。有了可选链操作符就可以使得表达式更加简明了,对于上面例子用可选链操作符可以这么写:
let name3 = obj?.first?.second
空值合并操作符(??)
对于逻辑或 ||
运算符,当对运算符左侧的操作数进行装换为 Boolean
值的时候,如果为 true
,则取左边的操作数为结果,否则取右边的操作数为结果:
最后
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-mC0MWczR-1715477942070)]