1. 数据类型
引用类型,基本类型,有什么区别,堆栈,bigInt
2. 原型/原型链 🔥
- 对象的属性和方法继承:每个对象都有一个原型(prototype),通过原型链,一个对象可以继承其原型对象的属性和方法。
__proto__的指向,当你访问一个对象的属性或方法时,如果当前的对象没有,会沿着原型链往上找,直到原型链的顶端(null) - 对象的方法重用:通过原型链,多个对象可以共享同一个原型对象的方法。这意味着我们可以在原型对象上定义一次方法,然后所有继承自该原型的对象都可以使用这个方法,避免了每个对象都拥有一份相同的方法的重复内存消耗
- 对象的多层继承:原型链可以形成多层继承关系,一个对象可以继承自另一个对象,而后者又可以继承自另一个对象,以此类推。这样的继承关系可以实现复杂的对象结构,帮助我们组织和管理代码
- 扩展对象的功能:通过在原型对象上添加新的属性和方法,可以实现对已有对象的功能扩展。由于原型链的特性,这些扩展会自动应用到所有继承自该原型的对象上,从而实现了动态的对象功能扩展
3. 作用域
- 局部、全局、作用域链(里 => 外)
- 词法作用域:静态的作用域,书写代码时就确定了,能够预测在执行代码的过程中如何查找标识符
4. 闭包 🔥
内部函数访问并记住外部函数的作用域的桥梁,创建函数的时候同时闭包就生成了
- 创建私有变量:定义模块私有变量,将操作函数暴露给外部,处理细节隐藏在模块内部
- 回调函数:实现抽象(数组方法中的各种回调函数,
forEach、map等)// 其他语言的abstract: 父类不知道要做什么的方法,交给子类实现 if (typeof callback === 'function') { callback(div); } - 延长变量的生命周期(本应该在外部函数执行完被垃圾回收的变量,因为内部函数的引用还在,不会被回收)
-
解决定时器延时打印(
var存在变量提升,闭包可以创建一个临时变量记住当前的i)for (var i = 1; i <= 10; i++) { (function () { var j = i; setTimeout(function () { console.log(j); }, 1000); })(); } // 或者这样写 for (var i = 1; i <= 10; i++) { (function (j) { setTimeout(function () { console.log(j); }, 1000); })(i); } // 扩展:直接把var改成let也可以(块级作用域) -
函数柯里化:多元参数转成一元
-
用 ES5 实现迭代器(见下 ES6+ 新特性
Symbol.iterator)
-
5. 同步 & 异步
基于事件循环机制
- 背景:JS是单线程语言,多线程怕DOM操作冲突,但是如果有些事情一直卡住也不好,因此有了“异步”
- 事件循环:一个宏任务 => 所有微任务 => RequestAnimationFrame(重绘前) => 渲染 => requestIdleCallback => 下一个任务 🔥
- 宏任务:
script代码、setTimeout、setInterval、I/O 操作等 - 微任务:
Promise的回调函数(then、catch、finally)、MutationObserver、queueMicrotask等 Promise里面的代码也是同步代码,它被resolve之后then等才会被推入微任务队列;await后面的代码会被推入微任务
async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(function () { console.log('setTimeout0') }, 0) setTimeout(function () { console.log('setTimeout2') }, 300) async1(); new Promise(function (resolve) { console.log('promise1') resolve(); console.log('promise2') }).then(function () { console.log('promise3') }) console.log('script end') // script start // async1 start async1执行的时候打印的 // async2 async1执行的时候打印的, await后面的推入微任务 // promise1 // promise2 resolve后才推微任务 // script end // async1 end // promise3 // setTimeout0 // setTimeout2 300ms后才进的宏任务,最后- 扩展:浏览器的事件循环和node的事件循环有什么区别?【todo, orz】
- 宏任务:
- 异步的解决方案
- 回调函数(定时器回调、node 的各种事件比如
readFile等) - Promise
- Generate
- async/await
- promise 和 async/await 是专门用于处理异步操作的
- Generator 并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署 Interator 接囗...)
- promise 编写代码相比 Generator、async 更为复杂化,且可读性也稍差
- Generator、async需要与 promise 对象搭配处理异步情况
- async 实质是 Generator 和 Promise 的语法糖,相当于会自动执行 Generator 函数
- async 使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程目前的最终方案
- 回调函数(定时器回调、node 的各种事件比如
6. ES6+ 新特性
-
let、const、var(变量提升、暂时性死区、块级作用域;是否可重复赋值)🔥 -
扩展运算符
...- 浅拷贝
- 用于数组的解构赋值或者
rest参数时, 只能放在最后
-
解构赋值
-
Array构造函数新增方法:from、of、find、finIndex、fill、entries、keys、values、includes、flat、flatMap、copyWithin、sort 等 -
函数的新增扩展
- 参数:默认值、
rest参数(这俩不会计入函数的length属性) - 新增
name属性 - 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式
- 箭头函数
- 函数体内的
this对象,就是定义时所在的对象,而不是使用时所在的对象 - 不可以当作构造函数,也就是说,不可以使用
new命令,否则会抛出一个错误 - 不可以使用
arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替 - 不可以使用
yield命令,因此箭头函数不能用作Generator函数
- 函数体内的
- 参数:默认值、
-
对象的新增扩展
- 属性简写
- 属性名表达式
let lastWord = 'last word'; const a = { 'first word': 'hello', [lastWord]: 'world' }; a['first word'] // "hello" a[lastWord] // "world" a['last word'] // "world"-
super关键字:指向当前对象的原型对象 -
属性的遍历
1.
for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)2.
Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)的键名Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键名Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性的键名Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举上述遍历,都遵守同样的属性遍历的次序规则:
- 遍历所有数值键,按照数值升序排列
- 遍历所有字符串键,按照加入时间升序排列
- 最后遍历所有 Symbol 键,按照加入时间升序排
-
对象新增方法:is、assign(浅拷贝)、getOwnPropertyDescriptors(自身属性)、setPrototypeOf、getPrototypeOf、keys、values、entries、fromEntries
-
扩展:
for...in和for...of的区别 🔥- 遍历的对象类型:
- in:用于遍历对象的可枚举属性,包括对象自身的属性以及从原型链继承的属性
- of:用于遍历可迭代对象(实现了迭代器接口的对象),例如数组、字符串、
Map、Set等
- 遍历顺序:
- in:如上(但是取决于 JavaScript 引擎实现的细节。在大多数情况下是上面所说的情况)
- of:根据迭代器接口的定义,按照可迭代对象中元素的顺序进行遍历。
- 遍历的内容:
- in:属性名(字符串类型)
- of:属性值
- 使用场景:
- in:适用于遍历对象的属性,例如遍历对象的键或进行对象属性的操作
- of:适用于遍历可迭代对象中的元素,例如遍历数组、字符串、
Map、Set等
- 再扩展:怎么使用迭代器接口自定义可迭代对象?
const myIterable = { data: [1, 2, 3], // 用[Symbol.iterator]返回一个迭代器对象 [Symbol.iterator]() { let index = 0; return { // next:逐个访问元素,done表示迭代是否结束 next: () => { if (index < this.data.length) { return { value: this.data[index++], done: false }; } else { return { value: undefined, done: true }; } } } } } for (const item of myIterable) { console.log(item);//1,2,3 }
-
Promise🔥- 作用:
- 解决回调地域问题,链式调用代替嵌套调用
- 降低代码编写难度,可读性增强
- 更方便的错误处理机制,
reject和.catch
- 和
async/await的联系:async/await是基于Promise的语法糖,用同步的方式来编写异步代码。async函数返回一个Promise对象(可以通过then和catch方法来处理async函数的执行结果),可以在函数内部使用await关键字来等待一个Promise对象的解析或拒绝async函数可以使用try/catch块来捕获和处理异步操作中的错误,类似于Promise的catch方法await后面要是跟的不是Promise, 会被自动包装成一个已解析的Promise对象
async function main() { let result = await 42 // 相当于 let result = await wrapValue(42); console.log(result);// 42 } async function wrapValue(value) { return new Promise((resolve)=>{ setTimeout(() => resolve(value),1000); }); } - 三个状态:pending、fullfilled、rejected
- 构造函数的实例方法:then、catch、finally
- 构造函数
Promise的方法:all、race、allSettled、any、resolve、reject、try
// 简单手写实现 // Promise.all const promiseAll = (promiseArr) => { let resArr = []; return new Promise((resolve, reject) => { // 挨个执行一遍 for(let p of promiseArr) { p.then((res) => { // 成功的结果push到结果数组里 resArr.push(res) }).catch((err) => { // 一旦有失败的,立马结束 console.log('err:', err) reject(err) }).finally(() => { if(resArr.length === promiseArr.length) { // 说明都执行完了 resolve(resArr) } }) } }) } // Promise.race const promiseRace = (promiseArr) => { return new Promise((resolve, reject) => { for(let p of promiseArr) { p.then((res) => { // 哪个先返回结果就是哪个,then resolve(res), (err) => { reject(err) } }) } }) } // Promise.allSettled const promiseAllSettled = (promiseArr) => { let resArr = []; return new Promise((resolve) => { for(let p of promiseArr) { p.then((res) => { resArr.push({status: 'fulfilled', value: res}) }).catch((err) => { resArr.push({status: 'failed', reason: err}) }).finally(() => { if(resArr.length === promiseArr.length) { // 说明都执行完了 resolve(resArr) } }) } }) } // 测试 (function () { const testArr = [Promise.resolve(1),Promise.reject('error')] promiseAllSettled(testArr) .then((res)=>{console.log('res:', res)}) .catch((err)=>{console.log('err:', err)}) })(); - 作用:
-
模块化 module
- 作用:代码的抽象、封装、复用、管理
- JS的模块化机制:commonJS、AMD(require.js)、CMD(sea.js)
- commonJS 和 es module(import)的区别 🔥
- 语法差异:
- CommonJS:使用
require()函数来导入模块,使用module.exports或exports来导出模块 - ES6 模块:使用
import关键字来导入模块,使用export关键字来导出模块
- 运行时加载 vs 静态加载:
- CommonJS:模块在运行时加载,即在代码执行到
require()时,才会加载所需的模块 - ES6 模块:模块在编译时静态加载,即在代码解析阶段就会生成模块依赖关系,使得模块的导入和导出在代码运行之前就确定
- 模块的复制:
- CommonJs:导入的模块是被复制的,即每次
require()都会生成一个新的实例,模块内部的状态不会被共享 - ES6 模块:模块是单例的,即每个模块只会被加载一次,后续的导入都会返回同一个实例,模块内部的状态是共享的
- 动态导入:
- CommonJS:不支持动态导入,
require()的参数必须是静态字符串 - ES6 模块:支持动态导入,可以在运行时根据条件动态地导入模块,使用
import()函数实现
- 顶层作用域:
- CommonJs:模块中的代码在一个单独的作用域中运行,模块内部的变量不会影响到全局作用域
- ES6 模块:模块中的代码默认在严格模式下执行,模块内部的变量和函数默认不会被绑定在全局作用域,需要显示地使用
export或window对象绑定到全局作用域
- 浏览器兼容性:
- CommonJS:主要用于服务器端的 Node.js 环境,浏览器端需要使用工具(如 Browserify、Webpack)进行转换才能使用。
- ES6 模块:现代浏览器原生支持 ES6 模块,无需额外的转换工具
-
生成器 Generator
Generator函数会返回一个遍历器对象,即具有Symbol.iterator属性,通过yield关键字可以暂停函数返回的遍历器对象的状态(所以可以用for...of遍历)function* helloWorldGenerator() { yield 'hello'; yield 'world'; } var hw = helloWorldGenerator() console.log(hw.next()) // { value: 'hello', done: false } console.log(hw.next()) // { value: 'world', done: false } console.log(hw.next()) // { value: undefined, done: true }
使用场景:
- 异步编程:Generator 函数可以与异步操作结合使用,通过使用
yield和next控制生成器的执行流程。它为异步编程提供了一种更简洁、易读和易理解的方式。在 ES6 之前,Generator 函数常被用于实现基于回调的异步编程模式,而在 ES6 之后,它通常与async/await结合使用 - 控制流管理:Generator 函数允许我们在迭代的过程中显式地控制执行流程。通过
yield语句,我们可以在每个步骤之间进行流程控制,使得代码更加可读和可维护 - 数据流处理:Generator 函数可以作为数据流的生成器和消费者。我们可以使用
yield从生成器函数中产生数据,并使用next将数据传递给生成器函数。这种数据流处理的方式非常灵活,可以用于实现各种数据处理和转换的场景
- 装饰器 Decorator 装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论(可用于装饰器模式,可以类比手机壳🐶)
- 类的装饰: 比如让A类拥有B类的属性,在A类头上加上
@B - 类属性的装饰:
@readonly等 - 使用案例:MobX
-
Set、Map、weakSet、WeakMap
-
Proxy
- 使用案例:vue3 的双向数据绑定实现原理(代替了
Object.defineProperty) Object.defineProperty和Proxy
Object.defineProperty 可以在对象上修改或新增属性,并设置属性的描述符:
// 对象内置了get和set,可以重写 let obj = { name: 1, get age() { return 22 }, set age(value) { obj.name = value } } obj.age = 300 console.log(obj.age) // 22 console.log(obj.name) // 300 // 显示重写 Object.defineProperty(obj, 'a', { value: 'test', // 给obj的a属性赋值为'test' writable: true, // 属性是否可写,如果不设置这个属性,obj.a将不能修改值 enumerable: true, // 属性是否可枚举 configurable: false, // 属性是否可删除或修改 get() { // ... }, set(val) { // ... }, })Proxy 用于创建对象的代理,可以拦载并自定义对象的操作:
const proxy = new Proxy(target, handler) // target:要拦截的目标对象 // handler:拦截属性的操作方法:get、set、has、deleteProperty、ownKeys... // 示例 let validator = { set: function (obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); if (value > 200) { throw new RangeError('The age seems invalid'); // 对于满足条件的 age 属性以及其他属性,直接保存obj[prop]= value; }; } } } } let person = new Proxy({}, validator); person.age = 100; person.age // 100 person.age = 'young'//报错person.age=300 //报错区别: 🔥
-
代理的粒度不同:Object.defineProperty 只能代理属性,Proxy 代理的是对象 & 属性(可以代理对象的所有属性,不需要遍历一个个设置
getter和setter) -
是否破坏原对象: Object.defineProperty 的代理行为是在破坏原对象的基础上实现的 而 Proxy 不会破坏原对象,只是在原对象上覆盖一层代理
-
代理数组属性:Object.defineProperty不适合监听数组,而 Proxy 可以代理数组属性
-
代理范围:Object.defineProperty 只能代理属性的 get 和 set,Proxy 可以代理更多的 行为(如
delete操作、构造函数等) -
兼容性:Object.defineProperty 容性较好,而 Proxy 是 E56 新增的特性(这也是为什么 vue3 不支持IE的原因之一)
扩展: vue2 和 vue3都是怎么监听数组的?【todo, orz】
-
Reflect
若需要在 Proxy 内部调用对象的默认行为,建议使用 Reflect ,它是 ES6 中操作对象提供的新 API。特点:
- 只要 Proxy 对象具有的代理方法,Reflect 对象全部具有,以静态方法的形式存在
- 修改某些 Object 方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回
false) - 让 Object 操作都变成函数行为
-
Proxy 的使用场景
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
- 使用案例:vue3 的双向数据绑定实现原理(代替了
-
es6+ 最新特性:findLast、findLastIndex、toSort、weakRef...【todo, orz】🔥
其他面试常考点
答案整理 【todo, orz】
==和===的区别,类型转换this的指向:new、显示(call、apply、bind的区别、实现)、默认、隐藏(箭头函数、立即执行函数需要注意些什么)typeof、instanceof、Object.prototype.toString.call()的区别
function getType(data) {
// 如果是基本类型,直接用typeof
const type = typeof data
if(type !== 'object') {
return type
}
// (\S+)用中括号括起来的是捕获组,这里也就是[object 类型]里的类型($1,第一个捕获组)
// \S 表示任意非空白字符,>=1个
return Object.prototype.toString.call(data).replace(/^\[object (\S+)\]$/, '$1')
}
// 测试
getType(1)
getType({})
getType(null)
getType(new Date())
getType([1,22])
instanceof实现原理
function myInstanceof(left, right) {
// 如果是基础类型的话,instanceof判断不出来
/*
'sds' instanceof string // Uncaught ReferenceError: string is not defined
'sds' instanceof String // false
*/
if(typeof left !== 'object' || left === null) {
return false
}
// 顺着原型链去找,找得到就是true
// let proto = Object.getPrototypeOf(left)
while(true) {
// 找到尽头都没找到
if(left === null) return false
if(left.__proto__ === right.prototype) return true
left = left.__proto__
}
}
// 测试
let Car = function() {}
let car1 = new Car();
console.log(myInstanceof(car1, Car))
console.log(myInstanceof(car1, Object))
let num = new Number(1)
console.log(myInstanceof(num, String))
console.log(myInstanceof(1, Number))
-
JS 继承方式
-
JS面向对象三大特性,举例(封装、继承、多态)
-
变量提升和函数提升(函数表达式不会提升)
-
DOM & BOM
-
事件冒泡、捕获、委托
-
JS 设计模式
-
函数式编程、数柯里化、纯函数、高阶函数
-
symbol的作用、使用场景 -
JS 的深浅拷贝,实现一个 deepClone
-
toString&valueof -
JS 的错误捕获方法
-
垃圾回收的方法(引用计数、标记清除)
-
内存泄漏的场景、治理
-
babel 的原理
-
TS:JS 的超集,支持面向对象编程的概念(类、接口、继承、泛型)
- 特性(类型批注/推断、擦除、接口、枚举、Mixin、泛型、元组、名字空间...)
- 高级类型(交叉、联合、类型别名/索引、约束、映射类型、条件类型...)
- 类型别名
type和接口的区别:接口只能定义接口类型,但是type声明可以定义对象、交叉/联合、原始类型等
- 类型别名
- TS 的
class:public、private、protect、readonly、static...
-
常见的字符串和数组API真的建议好好掌握,算法和编程题用得到
- 数组:
- push、pop、shift、unshift、find、findIndex、reverse、sort、join
slice:[a, b),b可省略splice:(start, deleteCount, item1, item2...)- start: 修改的起始索引
- deleteCount:可省略,如果此时后面有参数,就是插入(插入位置也是从 start 开始)
- 迭代:forEach、map、some、every、filter(注意区别)
- 字符串:
slice:[a, b),b可省略substring:[a, b),b可省略,a比b大的话会自动交换substr: [a, length)- 参数是负数的话,
slice相当于从字符串末尾开始处理,substring会当成0处理 - 匹配:match、search、replace
- split
- 数组:
-
“子子孙孙,无穷尽也”......