【面试题】2024前端面试真题之JS篇,2024年字节跳动+京东+美团面试总结

141 阅读24分钟
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(_resolve, _reject)

}

// then方法,接收一个成功的回调和一个失败的回调 then(resolveFn, rejectFn) { this._resolveQueue.push(resolveFn) this._rejectQueue.push(rejectFn) } }


代码测试



const p1 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('result') }, 1000); }) p1.then(res =>console.log(res)) //一秒后输出result




---


### async await



> 
> `async/await` 实际上是对 `Generator`(生成器)的封装,是一个**语法糖**。
> 
> 
> 


`*/yield``async/await`看起来其实已经很相似了,它们都提供了暂停执行的功能,但二者又有三点不同:


1. `async/await`**自带执行器**,不需要手动调用 `next()`就能**自动执行下一步**
2. `async` 函数返回值是 `Promise` 对象,而 `Generator` 返回的是**生成器对象**
3. `await` 能够返回 `Promise` 的 `resolve/reject` 的值



> 
> 不管`await`后面跟着的是什么,`await`都会阻塞后面的代码
> 
> 
> 


### Generator


`Generator` 实现的核心在于上下文的保存,函数并没有真的被挂起,每一次 `yield`,其实**都执行了一遍传入的生成器函数**,只是在这个过程中间**用了一个 context 对象储存上下文**,使得每次执行生成器函数的时候,都**可以从上一个执行结果开始执行**,看起来就像函数被挂起了一样。


用`babel`编译后生成`regeneratorRuntime`


1. `mark()`方法为**生成器函数**绑定了一系列原型
2. `wrap()`相当于是给 `generator` 增加了一个`_invoke` 方法




---


### 两者的区别


`Promise`的出现解决了传统`callback`函数导致的**地域回调**问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。


而`async await`代码看起来会简洁些,使得**异步代码看起来像同步代码**,`await`的本质是可以**提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句**。



> 
> `async/await``Promise`一样,是**非阻塞**的。
> 
> 
> 


`async/await`是基于`Promise`实现的,可以说是改良版的`Promise`,它不能用于普通的回调函数。




---


## ES6迭代器


### 迭代器模式



> 
> 可以把有些结构称为{可迭代对象|iterable},它们实现了正式的 `Iterable` 接口 而且可以通过{迭代器|Iterator}消费
> 
> 
> 


{迭代器|Iterator}是**按需创建的一次性对象**



> 
> 每个迭代器都会关联一个**可迭代对象**
> 
> 
> 


#### 可迭代协议


实现 `Iterable` 接口(可迭代协议)要求同时具备两种能力


1. 支持迭代的**自我识别能力**
2. 创建实现 `Iterator` 接口的对象的能力


这意味着必须暴露一个属性作为**默认迭代器**,这个属性必须使用特殊的 `Symbol.iterator` 作为键,这个默认迭代器属性必须引用一个**迭代器工厂函数**。调用这个工厂函数必须返回一个**新迭代器**


#### **内置类型**都实现了 `Iterable` 接口


1. 字符串
2. 数组
3. `Map`
4. `Set`
5. `arguments` 对象
6. `NodeList` 等 DOM 集合类型


#### 接收可迭代对象的原生语言特性包括


1. **`for-of` 循环**
2. **数组解构**
3. 扩展操作符
4. `Array.from()`
5. 创建`Set`
6. 创建`Map`
7. `Promise.all()`接收由`Promise`组成的可迭代对象
8. `Promise.race()`接收由`Promise`组成的可迭代对象
9. `yield*`操作符,在生成器中使用


#### 迭代器协议



> 
> 迭代器是一种**一次性使用**的对象,用于迭代与其关联的可迭代对象
> 
> 
> 


迭代器 API **使用 `next()`方法在可迭代对象中遍历数据,每次成功调用 `next()`,都会返回一个 `IteratorResult` 对象**,其中包含迭代器返回的下一个值。


`next()`方法返回的迭代器对象 `IteratorResult` 包含两个属性


1. `done`
	* 一个布尔值,表示是否还可以再次调用 `next()`取得下一个值
2. `value`
	* 包含可迭代对象的下一个值



> 
> 每个迭代器都表示对可迭代对象的**一次性有序遍历**
> 
> 
> 


##### 手写一个迭代器



function makeIterator(array) { var nextIndex = 0; return { next: function() { return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true }; }, }; }


代码测试



var it = makeIterator(["a", "b"]);

it.next(); // { value: "a", done: false } it.next(); // { value: "b", done: false } it.next(); // { value: undefined, done: true }




---


## 设计模式的分类


总体来说设计模式分为三大类:(`C5S7B11`)


1. **创建型模式**,共五种:**工厂方法模式**、抽象工厂模式、**单例模式**、建造者模式、**原型模式**2. **结构型模式**,共七种:**适配器模式****装饰器模式****代理模式**、外观模式、桥接模式、组合模式、享元模式。
3. **行为型模式**,共十一种:策略模式、模板方法模式、**观察者模式/发布订阅模式**、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。


### 手写单例模式(创建模式)



let CreateSingleton = (function(){ let instance; return function(name) { if (instance) { return instance; } this.name = name; return instance = this; } })(); CreateSingleton.prototype.getName = function() { console.log(this.name); }


代码测试



let Winner = new CreateSingleton('Winner'); let Looser = new CreateSingleton('Looser');

console.log(Winner === Looser); // true console.log(Winner.getName()); // 'Winner' console.log(Looser.getName()); // 'Winner'


### 手写观察者模式(行为模式)



// 定义observe const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn);

const observable = obj => new Proxy(obj, { set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); // notify queuedObservers.forEach(observer => observer()); return result; } });


代码测试



obj = observable({ name:'789' })

observe(function test(){ console.log('触发了') })

obj.name ="前端柒八九" // 触发了 // 前端柒八九


### 手写发布订阅 (行为模式)



class Observer { caches = {}; // 事件中心

// eventName事件名-独一无二, fn订阅后执行的自定义行为 on (eventName, fn){ this.caches[eventName] = this.caches[eventName] || []; this.caches[eventName].push(fn); }

// 发布 => 将订阅的事件进行统一执行 emit (eventName, data) { if (this.caches[eventName]) { this.caches[eventName] .forEach(fn => fn(data)); } } // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息 off (eventName, fn) { if (this.caches[eventName]) { const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : []; this.caches[eventName] = newCaches; } }

}


代码测试



ob = new Observer();

l1 = (data) => console.log(l1_${data}) l2 = (data) => console.log(l2_${data})

ob.on('event1',l1) ob.on('event1',l2)

//发布订阅 ob.emit('event1',789) // l1_789 // l2_789

// 取消,订阅l1 ob.off('event1',l1)

ob.emit('event1',567) //l2_567


### 观察者模式 VS 发布订阅模式



![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/31023fb3789c4c88b44e170e505ebf84~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=4HLNFOVGLuleEHpGoehJu1UTt14%3D)


1. 从表面上看:
	* 观察者模式里,只有两个角色 —— **观察者** + **被观察者**
	* 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— {经纪人|Broker}
2. 往更深层次讲:
	* 观察者和被观察者,是`松耦合`的关系
	* 发布者和订阅者,则完全不存在耦合
3. 从使用层面上讲:
	* 观察者模式,多用于**单个应用内部**
	* 发布订阅模式,则更多的是一种{跨应用的模式|cross-application pattern} ,比如我们常用的消息中间件




---


## WebGL和canvas的关系


* `Canvas`就是画布,只要浏览器支持,可以在`canvas`上获取`2D上下文``3D上下文`,其中**3D上下文一般就是WebGL**,当然`WebGL`也能用于2D绘制,并且`WebGL`提供硬件渲染加速,性能更好。
* 但是 `WEBGL` 的支持性`caniuse`还不是特别好,所以在不支持 `WebGL` 的情况下,只能使用 `Canvas 2D api`,注意这里的降级不是降到 `Canvas`,它只是一个画布元素,而是降级使用 浏览器提供的 `Canvas 2D Api`,这就是很多库的兜底策略,如 `Three.js``PIXI` 等




---


## CommonJS和ES6 Module的区别


1. `CommonJS` 是同步加载模块,`ES6`是异步加载模块
	* `CommonJS`规范加载模块是**同步的**,也就是说,只有加载完成,才能执行后面的操作。由于`Node.js`主要用于**服务器编程**,模块文件一般都已经存在于**本地硬盘**,所以加载起来比较快,不用考虑非同步加载的方式,所以`CommonJS`规范比较适用。
	* 浏览器加载 `ES6` 模块是**异步加载**,不会造成堵塞浏览器,即**等到整个页面渲染完,再执行模块脚本**
2. `CommonJS` 模块输出的是一个值的拷贝,`ES6` 模块输出的是值的引用。
	* `CommonJS` 模块输出的是值的拷贝,也就是说,**一旦输出一个值,模块内部的变化就影响不到这个值**
	* `ES6` 模块的运行机制与 `CommonJS` 不一样。**JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值**3. `CommonJS` 模块是运行时加载,ES6 模块是编译时输出接口。


### 是否可以在浏览器端使用 CommonJS



> 
> `CommonJS`不适用于浏览器环境
> 
> 
> 




---


## 声明变量的方式(2 + 4 )


* `ES5`
	1. `var`命令
	2. `function`命令
* `ES6`
	1. `let`
	2. `const`
	3. `import`
	4. `class`




---


### 函数的声明


1. `function` 命令
	* `function fn(s) {}`
2. 函数表达式
	* `var fn = function(s) {}`
3. `Function` 构造函数
	* `new Function('x','y','return x + y' )`




---


## Object/Map/WeakMap的区别


`ES6` 提供了 `Map` 数据结构。它`类似于对象`,也是**键值对的集合**,但是“键”的范围不限于字符串,**各种类型的值**(包括对象)都可以当作键。



> 
> 也就是说,
> 
> 
> * `Object` 结构提供了字符串—值的对应,
> * `Map` 结构提供了值—值的对应,是一种更完善的 `Hash` 结构实现。
> 
> 
> 


`WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合。


`WeakMap``Map`的区别有两点。


* 首先,`WeakMap`只接受对象作为键名(null除外),不接受其他类型的值作为键名。
* 其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制。


总之,`WeakMap`的专用场合就是,它的键所对应的对象,可能会在将来消失。`WeakMap`结构有助于**防止内存泄漏**> 
> `WeakMap` 弱引用的只是**键名**,而不是键值。**键值依然是正常引用**
> 
> 
> 




---


## JS 深浅复制



> 
> JS在语言层面**仅支持浅复制**,深复制需要手动实现
> 
> 
> 


### 浅复制(3个)


1. 扩展运算符
2. `Object.assign()`
3. `Object.getOwnPropertyDescriptors()`+`Object.defineProperties()`


#### 扩展运算符(...)复制对象和数组



const copyOfObject = {...originalObject}; const copyOfArray = [...originalArray];


扩展运算符不足和特性。




| 不足&特性 |
| --- |
| 不能复制普通对象的`prototype`属性 |
| 不能复制*内置对象***特殊属性**(internal slots) |
| 只复制对象的本身的属性(非继承) |
| 只复制对象的可枚举属性(enumerable) |
| 复制的数据属性都是**可写的**(writable)和**可配置的**(configurable) |


#### `Object.assign()`


`Object.assign()`的工作方式和扩展运算符类似。



const copy1 = {...original}; const copy2 = Object.assign({}, original);


`Object.assign()`并非完全和扩展运算符等同,他们之间存在一些细微的差别。


* 扩展运算符在副本中**直接定义新的属性**
* `Object.assign()`通过**赋值的方式**来处理副本中对应属性


#### `Object.getOwnPropertyDescriptors()`和`Object.defineProperties()`


`JavaScript`允许我们通过**属性描述符**来创建属性。



function copyAllOwnProperties(original) { return Object.defineProperties( {}, Object.getOwnPropertyDescriptors(original)); }


1. 能够复制所有自有属性
2. 能够复制非枚举属性




---



### 深复制


#### 通过嵌套扩展运算符实现深复制



const original = {name: '789', work: {address: 'BeiJing'}}; const copy = {name: original.name, work: {...original.work}};

original.work !== copy.work // 指向不同的引用地址


#### 使用JSON实现数据的深复制


先将普通对象,


1. 先转换为`JSON`串(`stringify`)
2. 然后再解析(`parse`)该串



function jsonDeepCopy(original) { return JSON.parse(JSON.stringify(original)); }


而通过这种方式有一个很明显的缺点就是:



> 
> 只能处理`JSON`所能识别的`key`和`value`。对于不支持的类型,会被直接忽略掉。
> 
> 
> 


### 手动实现


#### 递归函数实现深复制


实现逻辑就是(`FHT`1. 利用 `for-in`对对象的属性进行遍历(**自身属性+继承属性**)
2. `source.hasOwnProperty(i)`判断是否是**非继承****可枚举**属性
3. `typeof source[i] === 'object'`判断值的类型,如果是对象,递归处理



function clone(source) { let target = {}; for(let i in source) { if (source.hasOwnProperty(i)) { if (typeof source[i] === 'object') { target[i] = clone(source[i]); // 递归处理 } else { target[i] = source[i]; } } }

return target;

}




---



#### 闭包


### 函数即对象


**在JS中,一切皆对象**。那从语言的设计层面来讲,**函数是一种特殊的对象**。


函数和对象一样可以拥有*属性和值*。



function foo(){ var test = 1 return test; } foo.myName = 1 foo.obj = { x: 1 } foo.fun = function(){ return 0; }


根据对象的数据特性:`foo` 函数拥有`myName`/`obj`/`fun` 的属性



> 
> 但是函数和普通对象不同的是,**函数可以被调用**
> 
> 
> 


#### 从**V8内部**来看看函数是如何实现可调用特性


在 V8 内部,会为函数对象添加了两个**隐藏属性**


* `name` 属性:属性的值就是**函数名称**
* `code` 属性:表示**函数代码**,以字符串的形式存储在**内存**中



![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/47579a942f1043b99aa59c9422213823~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=NKxOC8vDOhER1nCXE%2BGvuqwjpJU%3D)


#### code 属性


当执行到,一个**函数调用**语句时,`V8` 便会从*函数对象*中取出 `code` 属性值(也就是函数代码),然后再**解释执行**这段函数代码。



> 
> 在解释执行函数代码的时候,又会生成该函数对应的**执行上下文,并被推入到调用栈里**
> 
> 
> 




---


#### 闭包



> 
> 在 JS 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。  
>  当通过**调用**一个外部函数**返回**一个内部函数后,即使该外部函数已经执行结束了。但是内部函数引用外部函数的变量依然保存在内存中,就把这些变量的集合称为闭包。
> 
> 
> 



function test() { var myName = "fn_outer" let age = 78; var innerObj = { getName:function(){ console.log(age); return myName }, setName:function(newName){ myName = newName } } return innerObj } var t = test(); console.log(t.getName());//fn_outer t.setName("global") console.log(t.getName())//global


* 根据**词法作用域的规则**,内部函数 `getName` 和 `setName` 总是可以访问它们的外部函数 `test` 中的变量
	+ 在执行`test`时,调用栈的情况  ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0ecc5af434a44de0bd90ea4ceae7891c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=fn0C3M%2FVasN%2BPNFqlEFHhQxKDZo%3D)
* `test` 函数执行完成之后,其执行上下文**从栈顶弹出**	+ 但是由于返回`innerObj`对象中的 `setName` 和 `getName` 方法中使用了 `test` 函数内部的变量 `myName` 和 `age` 所以这两个变量依然保存在内存中(`Closure (test)`	+ ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c8371ed869894e6689410da5680d1fe8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=W%2FIOM6RJJt803HTefEZNyWRaOT4%3D)
* 当执行到`t.setName`方法的时,调用栈如下:
	+ ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4819ae9c2efa4405b8d9ac455699b6df~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=oktfhSrahuMa8raJIazwkqwcNiI%3D)
* 利用debugger来查看对应的作用链和调用栈信息
	+ ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9ba972ae298484cb6ef7eecb2f01929~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=3l88wz89ryiePUrcfXxQV%2FoOZ3A%3D)


通过上面分析,然后参考作用域的概念和使用方式,我们可以做一个简单的结论



> 
> 闭包和词法环境的**强相关**
> 
> 
> 


而JS的作用域由词法环境决定,并且作用域是**静态**的。


所以,我们可以得出一个结论:



> 
> 闭包在每次创建函数时创建(闭包在JS编译阶段被创建)
> 
> 
> 




---


### 闭包是如何产生的?



> 
> 产生闭包的核心两步:
> 
> 
> 1. **预扫描**内部函数
> 2. 把`内部函数`引用的外部变量**保存到堆中**
> 
> 
> 



function test() { var myName = "fn_outer" let age = 78; var innerObj = { getName:function(){ console.log(age); return myName }, setName:function(newName){ myName = newName } } return innerObj } var t = test();


当 `V8` **执行**到 `test` 函数时


* 首先会编译,并创建一个空执行上下文。


	+`编译过程`中,遇到**内部函数** `setName`, `V8`还要对内部函数做一次快速的词法扫描(预扫描) 发现该内部函数引用了 **外部函数**(`test`)中的 `myName` 变量
	+ 由于是内部函数引用了外部函数的变量,所以 **V8 判断这是一个闭包**
	+ 于是在堆空间创建换一个`closure(test)`的对象 (这是一个内部对象,`JavaScript` 是无法访问的),用来保存 `myName` 变量
* 当 `test` 函数**执行结束之后**,返回的 `getName` 和 `setName` 方法都引用`clourse(test)`对象。


	+ 即使 `test` 函数退出了,`clourse(test)`依然被其内部的 `getName` 和 `setName` 方法引用。
* 所以在下次调用`t.setName`或者`t.getName`时,在进行**变量查找**时候,根据作用域链来查找。




---


## Event Loop



![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8cb7b260ffe942beb6526f23e4d236a0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=%2F%2BhbbAdmTmkwQFDtzFhXRgtXZx8%3D)


### {事件循环|Event Loop}



> 
> 事件循环是一个不停的从 **宏任务队列**/**微任务队列**中取出对应任务的循环函数。  
>  在一定条件下,你可以将其类比成一个永不停歇的**永动机**。 它从*宏/微任务队列***取出**任务并将其**推送****调用栈**中被执行。
> 
> 
> 


事件循环包含了四个重要的步骤:


1. **执行Script**:以同步的方式执行`script`里面的代码,直到**调用栈为空**才停下来。
	* 其实,在该阶段,JS还会进行一些**预编译**等操作。(例如,变量提升等)。
2. 执行一个宏任务:从宏任务队列中挑选**最老**的任务并将其推入到调用栈中运行,**直到调用栈为空**3. 执行所有微任务:从微任务队列中挑选**最老**的任务并将其推入到调用栈中运行,直到调用栈为空。
	* **但是,但是,但是**(转折来了),继续从微任务队列中挑选最老的任务并执行。直到**微任务队列为空**4. **UI渲染**:渲染UI,然后,**跳到第二步**,继续从宏任务队列中挑选任务执行。(这步只适用浏览器环境,不适用Node环境)



> 
> **事件循环的单次迭代过程被称为tick**
> 
> 
> 


### {宏任务队列|Task Queue}


也可以称为{回调队列| Callback queue}。



> 
> 调用栈是用于*跟踪*正在被执行函数的机制,而宏任务队列是用于*跟踪*将要被执行函数的机制。
> 
> 
> 


事件循环**不知疲倦**的运行着,并且按照*一定的规则*从宏任务队列中不停的取出任务对象。



> 
> 宏任务队列是一个**FIFO**(先进先出)的*队列结构*。结构中存储的宏任务会被事件循环**探查**到。并且,这些任务是**同步阻塞**的。当一个任务被执行,其他任务是被挂起的(按顺序排队)。
> 
> 
> 


### {微任务队列|Microtask Queue}


微任务队列也是一个**FIFO**(先进先出)的**队列结构**。并且,结构中存储的*微任务*也会被事件循环**探查**到。**微任务队列和宏任务队列很像**。作为ES6的一部分,它被添加到JS的*执行模型*中,以*处理Promise回调*。


微任务和宏任务也很像。它也是一个**同步阻塞代码**,运行时也会**霸占**调用栈。像宏任务一样,在运行期间,也会触发**新的**微任务,并且将新任务**提交**到微任务队列中,按照队列排队顺序,将任务进行合理安置。



> 
> * 宏任务是在循环中被执行,并且UI渲染**穿插**在宏任务中。
> * 微任务是在*一个*宏任务完成之后,在UI渲染之前被触发。
> 
> 
> 


微任务队列是ES6新增的专门用于处理`Promise`调用的数据结构。它和宏任务队列很像,它们最大的不同就是微任务队列是专门处理微任务的相关处理逻辑的。




---


## {垃圾回收机制|Garbage Collecation}


### 垃圾回收算法


1. 通过 `GC Root` 标记空间中**活动对象****非活动对象**
	* V8 采用的{可访问性| reachability}算法,来判断堆中的对象是否是**活动对象**
	* 这个算法是将一些 `GC Root` 作为**初始存活**的对象的集合
	* 从 `GC Roots` 对象出发,遍历 `GC Root` 中的所有对象
	* 通过 `GC Roots` 遍历到的对象,认为该对象是{可访问的| reachable},也称可访问的对象为**活动对象**
	* 通过 `GC Roots` 没有遍历到的对象,是{不可访问的| unreachable},不可访问的对象为**非活动对象**
	* 浏览器环境中,`GC Root` 包括1.**全局的 `window` 对象**,2.**文档 `DOM` 树**,由可以通过遍历文档到达的所有原生 `DOM` 节点组成,3.存放**栈上变量**
2. 回收*非活动对象*所占据的内存
3. 内存整理
	* 频繁回收对象后,内存中就会存在大量不连续空间
	* 这些不连续的内存空间称为**内存碎片**


### 代际假说


**代际假说**是垃圾回收领域中一个重要的术语


两个特点



> 
> 1. 第一个是大部分对象都是朝生夕死的
> 	* 大部分对象在内存中存活的时间很短
> 	* 比如函数内部声明的变量,或者块级作用域中的变量
> 2. 第二个是不死的对象,会活得更久
> 	* 比如全局的 `window`、`DOM`、`Web API` 等对象
> 
> 
> 


### 堆空间


在 `V8` 中,会把`堆`分为


1. **新生代**
	* 存放的是生存时间短的对象
	* 新生代通常只支持 1~8M 的容量
	* {副垃圾回收器| Minor GC} (Scavenger)
	* 负责新生代的垃圾回收
2. **老生代**
	* 存放生存时间久的对象
	* {主垃圾回收器| Major GC}
	* 负责老生代的垃圾回收



![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/772756d63e0547fdafaf11b10b8d73f8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=w1tv7jXWZX8f6Ep5XsdnWDPE4Cc%3D)




---


#### {副垃圾回收器| Minor GC}


新生代中的垃圾数据用 `Scavenge` 算法来处理。


所谓 `Scavenge` 算法,把新生代空间对半划分为两个区域:


* 一半是**对象区域** (`from-space`)
* 一半是**空闲区域** (`to-space`)


当对象区域快被写满时,就需要执行一次垃圾清理操作,


1. 首先要对对象区域中的垃圾**做标记**,
2. 标记完成之后,就进入垃圾清理阶段,


	* 把这些存活的对象**复制到空闲区域**中,把这些对象有序地排列起来
	* ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14847b40a448444b90033db83bbd9771~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=M%2B%2BQsawE54YzIjrG1Qr3xuL5lTo%3D)
3. 完成复制后,**对象区域与空闲区域进行角色翻转**


	* ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5cb064e5b1a946e8a1bee13ffe50f8fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=6VzY5Z5i8PAzcVCXl0wUfa7o0Z4%3D)


副垃圾回收器采用**对象晋升策略****移动那些经过两次垃圾回收依然还存活的对象到老生代中**。




---


#### {主垃圾回收器| Major GC}


负责老生代中的垃圾回收,除了新生代中晋升的对象,大的对象会**直接**被分配到老生代里。


老生代中的对象有两个特点


1. 对象占用空间大
2. 对象存活时间长


##### {标记 - 清除|Mark-Sweep}算法


1. 标记过程阶段
	* 从一组根元素开始,递归遍历这组根元素
	* 这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据
2. 垃圾的清除过程
	* *主垃圾回收器会直接将标记为垃圾的数据清理掉*
	* ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/65b13bf495154d51b625faa59b9c4675~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=Ig1OnIpiPHFO68V09%2BzpZOsN4Vc%3D)


##### {标记 - 整理|Mark-Compact}


1. 标记可回收对象
2. 垃圾清除
	* 不是直接对可回收对象进行清理
	* *而是让所有存活的对象都向一端移动*
	* **直接清理掉这一端之外的内存**


* ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8a1988cedeb34fa6b908f3b60846795f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=1eD3uRxGt67Uw69kR6PYggm1f5Y%3D)




---


## 内存问题


### 内存泄漏 (Memory leak)



> 
> 不再需要 (没有作用) 的内存数据依然被其他对象引用着。
> 
> 
> 


#### 污染全局(window)



function foo() { //创建一个临时的temp_array temp_array = new Array(200000) /** * 使用temp_array */ }


函数体内的对象没有被 `var``let``const` 这些关键字声明。


`V8` 就会使用 `this.temp_array` 替换 `temp_array`



> 
> **在浏览器,默认情况下,`this` 是指向 `window` 对象的**
> 
> 
> 


#### 闭包



function foo(){
var temp_object = new Object() temp_object.x = 1 temp_object.y = 2 temp_object.array = new Array(200000) /** * 使用temp_object */ return function(){ console.log(temp_object.x); } }


闭包会引用父级函数中定义的变量。


如果引用了不被需要的变量,那么也会造成内存泄漏。



#### detached 节点



let detachedTree; function create() { var ul = document.createElement('ul'); for (var i = 0; i < 100; i++) { var li = document.createElement('li'); ul.appendChild(li); } detachedTree = ul; }

create()



> 
> 只有同时满足 `DOM` 树和 `JavaScript` 代码都不引用某个 `DOM` 节点,该节点才会被作为垃圾进行回收。
> 
> 
> 


“detached ”节点:如果某个节点已从 `DOM` 树移除,但 `JavaScript` 仍然引用它




---


## 作用域的产生


作用域被分为3大类



> 
> 1. 声明式作用域
> 	* **函数作用域**
> 	* module作用域
> 2. 对象作用域
> 3. 全局作用域
> 
> 
> 


### 声明式作用域


声明式ER可以通过 `var/const/let/class/module/import/function`生成。


常说的**ES6块级作用域和函数作用域属于同一大类**(声明式作用域)。


根据实现层级,还有一个更准确的结论:



> 
> ES6块级作用域是函数作用域的子集
> 
> 
> 


### 全局作用域


全局作用域是**最外面的作用域**,它没有外部作用域。即全局环境的`OuterEnv``null`。


全局ER使用两个ER来管理其变量:


1. 对象ER
	* 将变量存储在**全局对象**	* 顶层作用域下,`var` 和 `function` 声明的变量被绑定在对象ER里(在浏览器环境下, `window` 指向全局对象)
2. 声明式ER
	* 使用内部对象来存储变量
	* 顶层作用域下,`const/let/class`声明的变量被绑定在声明ER



> 
> 当声明式ER和对象ER有共同的变量,**声明式优先级高**
> 
> 
> 




---


## this指向



![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b5e885e322774bc6aedfaec6339407e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=GFXXRMG7vGDkYSx0vg%2BtZwxNQas%3D)


 {执行上下文 |Execution context} 中包含了


1. {变量环境 |Viriable Environment}
2. {词法环境 |Lexical Environment}
3. {外部环境 |outer}
4. `this`



> 
> `this` 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 `this`
> 
> 
> 


### 执行上下文主要分为三种


1. 全局执行上下文
2. 函数执行上下文
3. `eval` 执行上下文


#### 全局执行上下文


全局执行上下文中的 `this` 是指向 `window` 对象的


这也是 `this` 和作用域链的唯一交点


* 作用域链的最底端包含了 `window` 对象
* 全局执行上下文中的 `this` 也是指向 `window` 对象


#### 函数执行上下文



> 
> 默认情况下调用一个函数,其执行上下文中的 `this` 也是指向 `window` 对象的
> 
> 
> 


##### 设置函数执行上下文中的 this 值


###### 通过函数的 `call/bind/apply` 方法设置



let bar = { myName : " 北宸 ", test1 : 1 } function foo(){ this.myName = " 南蓁 " } foo.call(bar) console.log(bar) // 南蓁 console.log(myName) // myName is not defined


###### 通过对象调用方法设置



var myObj = { name : " 北宸", showThis: function(){ console.log(this) } } myObj.showThis()


使用对象来调用其**内部的一个方法**,该方法的 `this` 是**指向对象本身**> 
> 可以认为 `JavaScript 引擎`在执行`myObject.showThis()`时,将其转化为了:`myObj.showThis.call(myObj)`
> 
> 
> 


把 `showThis` 赋给一个全局对象,然后再调用该对象



var myObj = { name : " 北宸 ", showThis: function(){ this.name = " 南蓁 " console.log(this) } } var foo = myObj.showThis foo()


`this` 又指向了全局 `window` 对象




> 

> * 在全局环境中调用一个函数,函数内部的 `this` 指向的是全局变量 `window`

> * 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 `this` 指向对象本身

> 

> 

> 




###### 通过构造函数中设置



function CreateObj(){ this.name = " 北宸南蓁 " } var myObj = new CreateObj()


此时,`this`指向实例对象




---



### this 的设计缺陷以及应对方案


#### 嵌套函数中的 内部函数`this` 不会从外层函数中继承



var myObj = { name : " 北宸南蓁 ", showThis: function(){ console.log(this) function inner(){console.log(this)} inner() } } myObj.showThis()


* 函数 `inner` 中的 `this` 指向的是全局 `window` 对象
* 函数 `showThis` 中的 `this` 指向的是 `myObj` 对象


#### 解决方案



> 
> 把 this 体系转换为了作用域的体系
> 
> 
> 



var myObj = { name : " 北宸 ", showThis: function(){ console.log(this) var self = this function inner(){ self.name = " 南蓁 " } inner() } } myObj.showThis() console.log(myObj.name) console.log(window.name)


在 `showThis` 函数中声明一个变量 `self` 用来保存 `this`,然后在 `inner` 函数中使用 `self`


##### 使用 ES6 中的箭头函数



var myObj = { name : " 北宸 ", showThis: function(){ console.log(this) var inner = ()=>{ this.name = " 南蓁 " console.log(this) } inner() } } myObj.showThis() console.log(myObj.name) console.log(window.name)


`ES6` 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 `this` 取决于它的外部函数


#### 普通函数中的 `this` 默认指向全局对象 window


通过设置 `JavaScript` 的“严格模式”来解决


在严格模式下,默认执行一个函数,其函数的执行上下文中的 `this` 值是 `undefined`




---



## 函数式编程,柯里化,redux 中间件



const curry = (fn,arity=fn.length,...args) => arity<=args.length ? fn(...args) : curry.bind(null,fn,arity,...args)


测试函数



const add = (a,b,c) => a+b+c; curry(add)(1,2,3) // 结果为6


### applyMiddleware



function applyMiddleware(...middlewares){ return function(createStore){ return function(reducer,initialState){ var store = createStore(reducer,initialState); var dispatch = store.dispatch; var chain = [];

  var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
  };

  chain = middlewares.map(
      middleware => middleware(middlewareAPI)
      );

  dispatch = compose(...chain)(store.dispatch);
  return { ...store, dispatch };
}

} }


`applyMiddleware` 函数是一个三级柯里化函数




---



## 图片懒加载


利用`JavaScript`实现懒加载的3种方式,原理都是判断图片是否出现在可视区后给图片赋值src属性。


### 利用HTML提供的 `data-` 属性来嵌入自定义数据


**自定义数据**存放这个标签原本的图片地址。


#### 利用offsetTop计算位置


用`JavaScript`实现当**滚动滚动条**时,如果图片出现在可视区,那么加载图片。**加载图片其实就是给img标签src属性赋值为本来的地址,那么此时图片便会请求加载渲染出来**





### 性能优化

1.webpack打包文件体积过大?(最终打包为一个js文件)  

2.如何优化webpack构建的性能  

3.移动端的性能优化  

4.Vue的SPA 如何优化加载速度  

5.移动端300ms延迟  

6.页面的重构

**所有的知识点都有详细的解答,我整理成了280页PDF《前端校招面试真题精编解析》。**



![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/23888062b19a451bb5cc1a29f135d0ca~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=YJKHtH1bFPB4A0iY9rOXVYsK4Aw%3D)

![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9c3a0a6d930d461bb09a7a0c62e745e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3NTc5MjMwMTY3MDI=:q75.awebp?rk3s=f64ab15b&x-expires=1771407688&x-signature=tE%2Bx2k5lCoNSnEMzbn9kMMNjvsU%3D)

**开源分享:https://docs.qq.com/doc/DSmRnRGxvUkxTREhO**