世界上只有一种真正的英雄主义,那就是看清生活的真相之后,依然热爱生活。 – 罗曼罗兰
大家好,我是柒八九。
在如今的互联网大环境下,每天都充斥着各种负能量。有可能,你上午还在工位摸鱼,下午HR
已经给你单独开小灶,很煞有介事的通知你,提前毕业了。在这个浮躁的互联网环境下,总有一种我们永远不知道明天和意外哪个先到的感觉。
《古兰经》中有一句很契合的话,山不过来,我就过去。
既然,外部环境我们无法去改变,那就从我们内部改变。所以,我又重新总结了一套,2023
年最新的面试集锦,以便大家一起度过寒冬,拥抱更好的未来。
note:
- 其中有些知识点,在前面的文章中,有过涉猎,为了行文的方便和资料的完整性,我就又拿来主义了,免去大家去翻找。但是,前面的文章有更深的解读,如果想更深的学习,可以移步到对应文章中。
- 如果在行文中,有技术披露和考虑不周的地方,不吝赐教。
- JS执行流程 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 基本数据类型 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- ES6的新特性有哪些 推荐阅读指数⭐️⭐️⭐️
- 箭头函数和普通函数的区别 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- Promise VS async/await 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- ES6迭代器 推荐阅读指数⭐️⭐️⭐️
- 设计模式的分类 推荐阅读指数⭐️⭐️⭐️⭐️
- WebGL和canvas的关系 推荐阅读指数⭐️⭐️
- CommonJS和ES6 Module的区别 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 声明变量的方式 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- Object/Map/WeakMap的区别 推荐阅读指数⭐️⭐️⭐️⭐️
- JS 深浅复制 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 闭包 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- Event Loop 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 垃圾回收机制 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 内存问题 推荐阅读指数⭐️⭐️⭐️
- 作用域的产生 推荐阅读指数⭐️⭐️⭐️⭐️
- this指向 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 图片懒加载 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- PromiseQueue 推荐阅读指数⭐️⭐️⭐️⭐️
- 数组常用方法 推荐阅读指数⭐️⭐️⭐️⭐️
需要准备执行 JS
时所需要的一些基础环境
堆和栈
结构内置函数
,全局变量
等信息全局变量
, 在执行过程中的数据都需要存放在内存中V8
接收到要执行的 JS 源代码
源代码
对 V8
来说只是一堆字符串,V8
并不能直接理解这段字符串的含义V8
结构化这段字符串,生成了{抽象语法树|AST},同时还会生成相关的作用域AST
和机器代码
的中间代码)
ignition
),按照顺序解释执行字节码,并输出执行结果。从图中得出一个结论:执行JS代码核心流程
- 先编译
- 后执行
通过V8
将js
转换为字节码
然后经过解释器
执行输出结果的方式执行JS
,有一个弊端就是,如果在浏览器中再次打开相同的页面,当页面中的 JavaScript
文件没有被修改,再次编译之后的二进制代码也会保持不变,意味着编译这一步浪费了 CPU 资源。
为了,更好的利用CPU资源,V8采用JIT(Just In Time)技术提升效率:而是混合编译执行和解释执行这两种手段。
- 解释执行的启动速度快,但是执行时的速度慢
- 编译执行的启动速度慢,但是执行时的速度快
为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。
在 JavaScript
引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。
如果同一行代码运行了几次,这个代码段就被标记成了
warm
,如果运行了很多次,则被标记成hot
。
如果一段代码变成了 warm
,那么 JIT
就把它送到编译器去编译,并且把编译结果存储起来。
代码段的每一行都会被编译成一个“桩”(stub
),同时给这个桩分配一个以行号 + 变量类型
的索引。如果监视器监视到了执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push
出来给浏览器。
如果一个代码段变得 very hot
,监视器
会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。
为了生成一个更快速的代码版本,优化编译器必须做一些假设。
例如,它会假设由同一个构造函数生成的实例都有相同的形状
就是说所有的实例
- 都有
相同的属性名
- 并且都以
同样的顺序初始化
那么就可以针对这一模式进行优化。
整个优化器
起作用的链条是这样的
可是对于 JavaScript
从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性。
正是由于这样的情况,所以编译代码需要在运行之前检查其假设是不是合理的。
JIT
会认为做了一个错误的假设,并且把优化代码丢掉
优化编译器
最成功一个特点叫做类型特化。
JavaScript
所使用的动态类型体系在运行时需要进行额外的解释工作,例如下面代码:
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
我们假设 arr
是一个有 100 个整数的数组。当代码被标记为 “warm” 时,基线编译器就为函数中的每一个操作生成一个桩。sum += arr[i]
会有一个相应的桩,并且把里面的 +=
操作当成整数加法。
但是,sum
和 arr[i]
两个数并不保证都是整数。因为在 JavaScript
中类型都是动态类型,在接下来的循环当中,arr[i]
很有可能变成了 string
类型。整数加法和字符串连接是完全不同的两个操作,会被编译成不同的机器码。
JIT
处理这个问题的方法是编译多基线桩。
这就是说 JIT
在选择一个桩之前,会进行多分枝选择,类似于决策树
,问自己很多问题才会确定最终选择哪个,见下图:
undefined
null
Boolean
String
Number
Symbol
(es6
)BigInt
(es2020
)Object
存储位置不同
typeof
typeof null
特例,返回的是"object"
Object.prototype.toString.call(xx)
null
或 undefined
,则将参数转为对象,再作判断[Symbol.toStringTag]
属性值(可能会遍历原型链)作为 tag
,然后返回 "[object " + tag + "]"
形式的字符串。instanceof
constructor
const
和 let
rest
参数Array.from()
将类数组转为数组find()
、findIndex()
找出第一个符合条件的成员/下标entries()
、keys()
、values()
用于遍历数组。(配合for...of
)includes()
是否存在指定无素(返回布尔值)Object.assign()
Object.keys()
, Object.values()
, Object.entries()
Symbol
Set
、Map
Promise
Iterator
和for...of
Generator
与async await
prototype
(原型),所以箭头函数本身没有this
this
this
,箭头函数的this
指向在定义的时候继承自外层第一个普通函数的thiscall
| apply
| bind
无法改变箭头函数中this
的指向arguments
,取而代之用rest
参数...
代替arguments
对象,来访问箭头函数的参数列表Generator
函数,不能使用yield
关键字
Promise
对象就是为了解决回调地狱而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
分析 Promise
的调用流程:
Promise
的构造方法接收一个executor()
,在new Promise()
时就立刻执行这个 executor
回调executor()
内部的异步任务被放入宏/微任务队列,等待执行then()
被执行,收集成功/失败回调,放入成功/失败队列executor()
的异步任务被执行,触发resolve/reject
,从成功/失败队列中取出回调依次执行其实熟悉设计模式,很容易就能意识到这是个观察者模式,这种
的方式,被广泛运用于观察者模式的实现,
在
Promise
里,执行顺序是
then
收集依赖- 异步触发
resolve
resolve
执行依赖。
//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
// 构造方法接收一个回调
constructor(executor) {
this._status = PENDING // Promise状态
this._resolveQueue = [] // 成功队列, resolve时触发
this._rejectQueue = [] // 失败队列, reject时触发
// 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
let _resolve = (val) => {
if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = FULFILLED // 变更状态
// 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
// 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 实现同resolve
let _reject = (val) => {
if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
this._status = REJECTED // 变更状态
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// 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
实际上是对Generator
(生成器)的封装,是一个语法糖。
*/yield
和async/await
看起来其实已经很相似了,它们都提供了暂停执行的功能,但二者又有三点不同:
async/await
自带执行器,不需要手动调用 next()
就能自动执行下一步async
函数返回值是 Promise
对象,而 Generator
返回的是生成器对象await
能够返回 Promise
的 resolve/reject
的值不管
await
后面跟着的是什么,await
都会阻塞后面的代码
Generator
实现的核心在于上下文的保存,函数并没有真的被挂起,每一次 yield
,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样。
用babel
编译后生成regeneratorRuntime
mark()
方法为生成器函数绑定了一系列原型wrap()
相当于是给 generator
增加了一个_invoke
方法Promise
的出现解决了传统callback
函数导致的地域回调问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。
而async await
代码看起来会简洁些,使得异步代码看起来像同步代码,await
的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。
async/await
与Promise
一样,是非阻塞的。
async/await
是基于Promise
实现的,可以说是改良版的Promise
,它不能用于普通的回调函数。
可以把有些结构称为{可迭代对象|iterable},它们实现了正式的
Iterable
接口
而且可以通过{迭代器|Iterator}消费
{迭代器|Iterator}是按需创建的一次性对象
每个迭代器都会关联一个可迭代对象
实现 Iterable
接口(可迭代协议)要求同时具备两种能力
Iterator
接口的对象的能力这意味着必须暴露一个属性作为默认迭代器,这个属性必须使用特殊的 Symbol.iterator
作为键,这个默认迭代器属性必须引用一个迭代器工厂函数。调用这个工厂函数必须返回一个新迭代器
Iterable
接口Map
Set
arguments
对象NodeList
等 DOM 集合类型for-of
循环Array.from()
Set
Map
Promise.all()
接收由Promise
组成的可迭代对象Promise.race()
接收由Promise
组成的可迭代对象yield*
操作符,在生成器中使用迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象
迭代器 API 使用 next()
方法在可迭代对象中遍历数据,每次成功调用 next()
,都会返回一个 IteratorResult
对象,其中包含迭代器返回的下一个值。
next()
方法返回的迭代器对象 IteratorResult
包含两个属性
done
next()
取得下一个值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
)
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式/发布订阅模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
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
松耦合
的关系Canvas
就是画布,只要浏览器支持,可以在canvas
上获取2D上下文
和3D上下文
,其中3D上下文一般就是WebGL,当然WebGL
也能用于2D绘制,并且WebGL
提供硬件渲染加速,性能更好。WEBGL
的支持性caniuse
还不是特别好,所以在不支持 WebGL
的情况下,只能使用 Canvas 2D api
,注意这里的降级不是降到 Canvas
,它只是一个画布元素,而是降级使用 浏览器提供的 Canvas 2D Api
,这就是很多库的兜底策略,如 Three.js
, PIXI
等CommonJS
是同步加载模块,ES6
是异步加载模块
CommonJS
规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于Node.js
主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS
规范比较适用。ES6
模块是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。
CommonJS
模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值ES6
模块的运行机制与 CommonJS
不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS
不适用于浏览器环境
ES5
var
命令function
命令ES6
let
const
import
class
function
命令
function fn(s) {}
var fn = function(s) {}
Function
构造函数
new Function('x','y','return x + y' )
ES6
提供了 Map
数据结构。它类似于对象
,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
也就是说,
Object
结构提供了字符串—值的对应,Map
结构提供了值—值的对应,是一种更完善的Hash
结构实现。
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。
WeakMap
与Map
的区别有两点。
WeakMap
只接受对象作为键名(null除外),不接受其他类型的值作为键名。WeakMap
的键名所指向的对象,不计入垃圾回收机制。总之,WeakMap
的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap
结构有助于防止内存泄漏。
WeakMap
弱引用的只是键名,而不是键值。键值依然是正常引用。
JS在语言层面仅支持浅复制,深复制需要手动实现
Object.assign()
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));
}
const original = {name: '789', work: {address: 'BeiJing'}};
const copy = {name: original.name, work: {...original.work}};
original.work !== copy.work // 指向不同的引用地址
先将普通对象,
JSON
串(stringify
)parse
)该串function jsonDeepCopy(original) {
return JSON.parse(JSON.stringify(original));
}
而通过这种方式有一个很明显的缺点就是:
只能处理
JSON
所能识别的key
和value
。对于不支持的类型,会被直接忽略掉。
实现逻辑就是(FHT
)
for-in
对对象的属性进行遍历(自身属性+继承属性)source.hasOwnProperty(i)
判断是否是非继承的可枚举属性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 内部,会为函数对象添加了两个隐藏属性
name
属性:属性的值就是函数名称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
函数执行完成之后,其执行上下文从栈顶弹出了
t.setName
方法的时,调用栈如下:
通过上面分析,然后参考作用域的概念和使用方式,我们可以做一个简单的结论
闭包和词法环境的强相关
而JS的作用域由词法环境决定,并且作用域是静态的。
所以,我们可以得出一个结论:
闭包在每次创建函数时创建(闭包在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();
当 V8
执行到 test
函数时
首先会编译,并创建一个空执行上下文。
编译过程
中,遇到内部函数 setName
, V8
还要对内部函数做一次快速的词法扫描(预扫描) 发现该内部函数引用了 外部函数(test
)中的 myName
变量closure(test)
的对象 (这是一个内部对象,JavaScript
是无法访问的),用来保存 myName
变量当 test
函数执行结束之后,返回的 getName
和 setName
方法都引用clourse(test)
对象。
test
函数退出了,clourse(test)
依然被其内部的 getName
和 setName
方法引用。所以在下次调用t.setName
或者t.getName
时,在进行变量查找时候,根据作用域链来查找。
事件循环是一个不停的从 宏任务队列/微任务队列中取出对应任务的循环函数。
在一定条件下,你可以将其类比成一个永不停歇的永动机。 它从宏/微任务队列中取出任务并将其推送到调用栈中被执行。
事件循环包含了四个重要的步骤:
script
里面的代码,直到调用栈为空才停下来。
事件循环的单次迭代过程被称为tick
也可以称为{回调队列| Callback queue}。
调用栈是用于跟踪正在被执行函数的机制,而宏任务队列是用于跟踪将要被执行函数的机制。
事件循环不知疲倦的运行着,并且按照一定的规则从宏任务队列中不停的取出任务对象。
宏任务队列是一个FIFO(先进先出)的队列结构。结构中存储的宏任务会被事件循环探查到。并且,这些任务是同步阻塞的。当一个任务被执行,其他任务是被挂起的(按顺序排队)。
微任务队列也是一个FIFO(先进先出)的队列结构。并且,结构中存储的微任务也会被事件循环探查到。微任务队列和宏任务队列很像。作为ES6的一部分,它被添加到JS的执行模型中,以处理Promise回调。
微任务和宏任务也很像。它也是一个同步阻塞代码,运行时也会霸占调用栈。像宏任务一样,在运行期间,也会触发新的微任务,并且将新任务提交到微任务队列中,按照队列排队顺序,将任务进行合理安置。
- 宏任务是在循环中被执行,并且UI渲染穿插在宏任务中。
- 微任务是在一个宏任务完成之后,在UI渲染之前被触发。
微任务队列是ES6新增的专门用于处理Promise
调用的数据结构。它和宏任务队列很像,它们最大的不同就是微任务队列是专门处理微任务的相关处理逻辑的。
GC Root
标记空间中活动对象和非活动对象
GC Root
作为初始存活的对象的集合GC Roots
对象出发,遍历 GC Root
中的所有对象GC Roots
遍历到的对象,认为该对象是{可访问的| reachable},也称可访问的对象为活动对象GC Roots
没有遍历到的对象,是{不可访问的| unreachable},不可访问的对象为非活动对象GC Root
包括1.全局的 window
对象,2.文档 DOM
树,由可以通过遍历文档到达的所有原生 DOM
节点组成,3.存放栈上变量代际假说是垃圾回收领域中一个重要的术语
两个特点
- 第一个是大部分对象都是朝生夕死的
- 大部分对象在内存中存活的时间很短
- 比如函数内部声明的变量,或者块级作用域中的变量
- 第二个是不死的对象,会活得更久
- 比如全局的
window
、DOM
、Web API
等对象
在 V8
中,会把堆
分为
新生代中的垃圾数据用 Scavenge
算法来处理。
所谓 Scavenge
算法,把新生代空间对半划分为两个区域:
from-space
)to-space
)当对象区域快被写满时,就需要执行一次垃圾清理操作,
副垃圾回收器采用对象晋升策略:移动那些经过两次垃圾回收依然还存活的对象到老生代中。
负责老生代中的垃圾回收,除了新生代中晋升的对象,大的对象会直接被分配到老生代里。
老生代中的对象有两个特点
不再需要 (没有作用) 的内存数据依然被其他对象引用着。
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);
}
}
闭包会引用父级函数中定义的变量。
如果引用了不被需要的变量,那么也会造成内存泄漏。
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大类
- 声明式作用域
- 函数作用域
- module作用域
- 对象作用域
- 全局作用域
声明式ER可以通过 var/const/let/class/module/import/function
生成。
常说的ES6块级作用域和函数作用域属于同一大类(声明式作用域)。
根据实现层级,还有一个更准确的结论:
ES6块级作用域是函数作用域的子集
全局作用域是最外面的作用域,它没有外部作用域。即全局环境的OuterEnv
为null
。
全局ER使用两个ER来管理其变量:
var
和 function
声明的变量被绑定在对象ER里(在浏览器环境下, window
指向全局对象)const/let/class
声明的变量被绑定在声明ER当声明式ER和对象ER有共同的变量,声明式优先级高。
{执行上下文 |Execution context}
中包含了
this
this
是和执行上下文绑定的,也就是说每个执行上下文中都有一个this
eval
执行上下文全局执行上下文中的 this
是指向 window
对象的
这也是 this
和作用域链的唯一交点
window
对象this
也是指向 window
对象默认情况下调用一个函数,其执行上下文中的
this
也是指向window
对象的
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
不会从外层函数中继承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
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
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
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属性。
data-
属性来嵌入自定义数据自定义数据存放这个标签原本的图片地址。
用JavaScript
实现当滚动滚动条时,如果图片出现在可视区,那么加载图片。加载图片其实就是给img标签src属性赋值为本来的地址,那么此时图片便会请求加载渲染出来
//获取全部img标签
var images = document.getElementsByTagName("img");
window.addEventListener("scroll", (e) => {
//当发生滚动事件时调用loadPic事件
loadPic();
});
function loadPic() {
// 遍历每一张图
for (let i of images) {
//判断当前图片是否在可视区内
if (i.offsetTop <= window.innerHeight + window.scrollY) {
//获取自定义data-src属性的值
let trueSrc = i.getAttribute("data-src");
//把值赋值给图片的src属性
i.setAttribute("src", trueSrc);
}
}
}
//没发生滚动事件时也要先执行一次
loadPic();
offsetTop
为元素距离顶部的距离;window.innerHeight
为当前窗口的高度;window.scrollY
为滚动距离当 i.offsetTop <= window.innerHeight + window.scrollY
时图片就处于窗口可视区了。
getBoundingClientRect().top
计算位置window.addEventListener("scroll", (e) => {
loadPic();
});
function loadPic() {
for (let i of images) {
//计算方式和第一种方式不同
if (i.getBoundingClientRect().top < window.innerHeight) {
let trueSrc = i.getAttribute("data-src");
i.setAttribute("src", trueSrc);
}
}
}
loadPic();
getBoundingClientRect().top
为元素相对于窗口的位置;window.innerHeight
为当前窗口的高度;当元素对于窗口的位置小于当前窗口的高度时,那自然处于了窗口可视区了。
Intersection Observer
构造函数的作用是它能够观察可视窗口与目标元素产生的交叉区域。简单来说就是当用它观察我们的图片时,当图片出现或者消失在可视窗口,它都能知道并且会执行一个特殊的回调函数,我们就利用这个回调函数实现我们的操作
var images = document.getElementsByTagName("img");
function callback(entries) {
for (let i of entries) {
if (i.isIntersecting) {
let img = i.target;
let trueSrc = img.getAttribute("data-src");
img.setAttribute("src", trueSrc);
observer.unobserve(img);
}
}
}
const observer = new IntersectionObserver(callback);
for (let i of images) {
observer.observe(i);
}
class PromiseQueue{
constructor(tasks,concurrentCount=1){
this.totals = tasks.length;
this.todo =tasks;
this.count = concurrentCount;
this.running =[];
this.complete =[];
}
runNext(){
return (
this.running.length < this.count
&& this.todo.length
)
}
run(){
while(this.runNext()){
let promise = this.todo.shift();
promise.then(()=>{
this.complete.push(this.running.shift());
this.run();
})
this.running.push(promise)
}
}
}
测试用例
// 接收一个promise数组,定义窗口大小为3
const taskQueue = new PromiseQueue(tasks, 3);
taskQueue.run();
push
pop
shift
unshift
reverse
sort
splice
concat
join
slice
filter
reduce
find
findIndex
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。