昨天忙着做od机试和外包的笔试,忘记更新了,每日打卡刚开始的第一天就断了。不禁回想起之前考研的时候,用的墨墨背单词,就没有一次连续打卡超过30天的。果然这个东西跟自律性还是有很大的关系。
话不多说了,开干!
let、const和var
-
var与其他两种声明方式的区别:-
使用
var声明变量会有变量提升的现象,即允许在声明之前被使用且初值为undefined -
使用
let和const声明变量会产生块级作用域,在块级作用域内使用这两种方式定义的变量不能在声明之前被使用(暂时性死区),如以下例子:var a = 0; if(true){ console.log(a); // 报错 let a = 0; console.log(a) }
-
-
let和const的一些特点-
let代表声明变量,可以被多次赋值;const代表声明常量,只允许在声明时进行赋值 -
const声明引用数据类型时,应看作引用不可变,引用指向的数据内容是可变的(如:使用const声明对象,对象内部值可以变)
-
原型和原型链
-
JavaScript是一门基于原型的语言,对于每个JS对象构造器而言,都会有显式原型(prototype);对于一个实例对象而言,会有隐式原型(_proto_),指向其对应构造器的prototype -
当我们对一个对象的属性进行访问时,如果在当前对象找不到,会通过
proto一直往上找,直到到达原型链的末尾(Object.prototype.__proto__,值为null),这个过程涉及到的关系链条,称为原型链 -
手撕
instanceof:function fakeInstanceOf(obj, constructor) { let proto = Object.getPrototypeOf(obj); while (proto) { if (proto === constructor.prototype) return true; proto = Object.getPrototypeOf(proto); } return false; }
执行上下文
-
JavaScript代码执行时的环境,每当代码执行时都会创建执行上下文。 -
分类:全局执行上下文、函数执行上下文、
eval执行上下文。 -
包括三个部分:变量对象、作用域链、
this指向。 -
作用域和作用域链:
-
作用域分类:全局作用域、函数作用域、块级作用域。
-
代码执行时通过作用域链寻找变量的过程:当在
Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
-
-
this指向问题:-
非箭头函数:
-
基本规则:谁调用,指向谁,如:
window.a = 123; function b() { console.log(this.a) } b() // 123 const ctx = { a: 456, b } ctx.b() // 456 -
使用
apply、call、bind改变this指向:-
call和apply作用一样,不过call的第一个参数以后的参数列表是传入待执行函数的参数列表,而apply仅有两个参数,第二个参数为传入待执行函数的参数列表数组。 -
bind返回值为一个新的函数,调用这个函数时,内部的this就是我们手动指定的第一个参数。 -
问题:如果把通过
bind产生的新函数重新进行bind绑定,最终执行的this指向哪里呢?答案:指向第一次绑定的对象的值。
用例:
const a = function (this: any) { console.log(this) } const b = { c: 11 } const d = { e: 22 } const f = a.bind(b) f() const g = f.bind(d) g() // 输出: //{ // "c": 11 //} -
手撕
apply(call同理):function apply(fn,ctx,args = []) { if(typeof fn !== 'function') throw new Error("The argument 'fn' must be a function") const ctx = ctx || window; const key = Symbol(); ctx[key] = fn; const res = ctx[key](...args) delete ctx[key] return res } -
手撕
bind:function myBind(fn, context, ...args1) { if (typeof fn !== "function") { throw new TypeError(`Bind must be called on a function`); } function F() {} const bound = function(...args2) { const isNew = this instanceof F; const ctx = isNew ? this : context; return fn.apply(ctx, [...args1, ...args2]); }; F.prototype = fn.prototype; bound.prototype = new F(); return bound; }
-
-
-
箭头函数:
-
内部
this指向外部函数的this -
无法使用
apply、call、bind改变this指向
-
-
使用
new创建对象:-
调用构造器时内部代码执行的
this指向新建的对象。 -
手撕
new:-
过程:创建一个新对象,并将新对象的原型指向构造函数的
prototype;然后执行构造函数,将this绑定到新对象上,并传递参数;如果构造函数返回一个非空对象,则返回该对象,否则,返回新对象。 -
代码实现:
function myNew(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.apply(obj, args); return Object(result) === result ? result : obj; }
-
-
-
事件循环(Event Loop)
-
理论知识:
-
设计初衷及基本原理:
-
因为
JavaScript是单线程的,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务,前一个任务如果耗时非常长,后一个任务将一直没法执行。JavaScript中利用EventLoop机制解决了这个问题。 -
在
JavaScript中,把任务分为同步任务和异步任务。在执行同步任务的过程中,会产生一部分事件(如定时器创建,I/O等),这部分事件如果继续往下同步执行,就会阻塞后面代码的执行。为了解决这个问题,EventLoop机制引入异步任务的概念,当前代码块执行完实际上是不包括代码本身产生的事件的,这些事件放后面在进行处理。
-
-
常见两类异步任务的
API:-
常见的微任务:
Promise.then,Object.observe,MutationObserver,process.nextTick(Node 环境)。 -
常见的宏任务:
setTimeout,ajax,dom事件,setImmediate(Node环境),requestAnimationFrame。
-
-
事件处理流程:
-
同步任务执行完毕后会开始从调用栈中去执行异步任务
-
优先执行微任务队列,当微任务队列清空后才会去执行宏任务
-
每次单个宏任务执行完毕后会去检查微任务队列是否为空,如果不为空会按照先入先出的原则执行微任务(微任务中也可以产生异步任务),待微任务队列清空后再执行下一个宏任务,如此循环往复。
-
-
-
经典恶心人的输入输出题:
-
例题1:
-
题目:
setTimeout(() => console.log(0)); new Promise((resolve) => { console.log(1); resolve(2); console.log(3); }).then((o) => console.log(o)); new Promise((resolve) => { console.log(4); resolve(5); }) .then((o) => console.log(o)) .then(() => console.log(6)); -
分析:
-
创建计时器,此时计时器内传入函数放到宏任务队列中等待执行
-
new Promise(fn)中的fn是立即执行的,所以先依次输出1 3 4,then中的任务推入微任务队列中等待执行 -
当前同步执行完成,会去检测微任务队列中是否有任务并按序执行,所以输出
2 5,此时第二个promise then产生了新的promise then任务,也是推入微任务队列中等待执行,之后会被执行,输出6 -
执行宏任务队列中的任务,输出
0
-
-
-
例题2:
-
题目:
setTimeout(() => { console.log("A"); Promise.resolve().then(() => { console.log("B"); }); }, 1000); Promise.resolve().then(() => { console.log("C"); }); new Promise((resolve) => { console.log("D"); resolve(""); }).then(() => { console.log("E"); }); async function sum(a, b) { console.log("F"); } async function asyncSum(a, b) { await Promise.resolve(); console.log("G"); return Promise.resolve(a + b); } sum(3, 4); asyncSum(3, 4); console.log("H"); -
分析:
-
首先分析当前代码块的同步代码:
setTimeout、Promise.resolve、new Promise、sum(3,4)、asyncSum(a + b)、console.log("H")。其中setTimeout会创建一个定时器,其回调会放到下一个宏任务执行;Promise.resolve、new Promise中的参数如果是函数,会被立即调用;sum(3,4)内没有异步语句,是直接执行的;asyncSum(a + b)执行await Promise.resolve()实际上等同如下代码:function asyncSum(a, b) { Promise.resolve().then(() => { console.log("G"); }); return Promise.resolve(a + b); }会创建微任务,后面语句等待改该微任务被调用后才会执行。所以第一趟输出应该是
D F H; -
之后按序执行第一次产生的微任务队列中的逻辑(此处只展示输出字符部分):
console.log("C")、console.log("E")、console.log("G")。所以之后的输出应该是C E G; -
最后在
3000ms后执行刚刚创建的定时器的回调,输出A。产生的微任务在定时器回调执行完后会被立即执行,输出B; -
综上,最终结果:
D F H C E G A B。
-
-
-
例题3:
-
题目:
Promise.resolve(console.log(0)) .then(() => { console.log(1); Promise.resolve(console.log(5)) .then(() => console.log(3)) .then(() => console.log(4)) .then(() => console.log(6)); }) .then(() => console.log(2)) .then(() => console.log(7)); -
分析:
-
console.log(0)立即执行,输出0,将then中的函数推入微任务队列等待执行。 -
按序执行微任务队列的事件:
console.log(1)、Promise.resolve(console.log(5)),输出1 5。将() => console.log(3)、() => console.log(2)推入微任务队列等待执行。 -
按序执行微任务队列的事件:
() => console.log(3)、() => console.log(2),输出3 2,将() => console.log(4)、() => console.log(7)推入微任务队列等待执行。 -
按序执行微任务队列的事件:
() => console.log(4)、() => console.log(7),输出4 7,将() => console.log(6)推入微任务队列等待执行。 -
按序执行微任务队列的事件:
() => console.log(6)、输出6。 -
结果:
0 1 5 3 2 4 7 6。
-
-
-
-
手撕环节:
-
promise.map:事实上
Promise没有这个API,不过有的公司会问关于并发数控制的问题,下面是一道相关的题目:实现一个 promise.map,进行并发数控制,有以下测试用例:
pMap([1, 2, 3, 4, 5], (x) => Promise.resolve(x + 1)); pMap([Promise.resolve(1), Promise.resolve(2)], (x) => x + 1); // 注意输出时间控制 pMap([1, 1, 1, 1, 1, 1, 1, 1], (x) => sleep(1000), { concurrency: 2 });首先分析用例的参数,第一个参数是一个数组;第二个参数是调用的方法;最后一个参数是配置对象,根据用例可知第三个参数要有一个属性
concurrency来控制并发。如果只看前两个参数的话,其效果有点像Array.prototype.map。代码实现:
type pMapType<T = any> = ( argsArr: any[], fn: (...args: any) => Promise<T>, concurrency?: number ) => Promise<T>; const pMap: pMapType = (argsArr, fn, concurrency = Infinity) => { return new Promise((resolve, reject) => { const result = new Array(argsArr.length).fill(0); const taskQueue: any[] = []; let currentWorkingAmount = 0; function run() { while (currentWorkingAmount < concurrency) { const nextTask = taskQueue.shift(); if (nextTask) { nextTask(); currentWorkingAmount++; } if (taskQueue.length === 0) break; } } for (let i = 0; i < argsArr.length; i++) { taskQueue.push(() => { Promise.resolve(fn(argsArr[i])) .then((res) => { result[i] = res; currentWorkingAmount--; if (taskQueue.length === 0 && currentWorkingAmount === 0) return resolve(result); if (currentWorkingAmount < concurrency) run(); }) .catch(reject); }); } run(); }); }; -
Promise.all:-
要点:
-
接收一个
promise的iterable类型 -
只返回一个
Promise实例 -
resolve的回调结果是一个数组,执行时机:-
所有输入的
promise的resolve回调都结束 -
输入的
iterable里没有promise了的时候
-
-
reject回调执行时机:- 只要任何一个输入的
promise的reject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息
- 只要任何一个输入的
-
-
代码:
const PromiseAll = (taskArr) => { return new Promise((resolve, reject) => { const res = new Array(taskArr.length).fill(0); let finishNum = 0; taskArr.forEach((item, index) => { if (item instanceof Promise) { item .then((_res) => { res[index] = _res; finishNum++; if (finishNum === taskArr.length) resolve(res); }) .catch(reject); } else { res[index] = item; finishNum++; if (finishNum === taskArr.length) resolve(res); } }); }); };
-
-
Promise.race-
要点(摘自
MDN):Promise.race(iterable)方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。 -
代码:
const PromiseRace = (taskArr) => { return new Promise((resolve, reject) => { const length = taskArr.length; for (let i = 0; i < length; i++) { if (taskArr[i] instanceof Promise) { taskArr[i].then(resolve).catch(reject); } else { resolve(taskArr[i]); } } }); };
-
-
promisify-
要点:
-
回调风格的函数形如
fn(...args,callback),promisify的返回的新函数的返回值应该是一个Promise对象,且这个对象的then传入的方法会当作原函数的回调被调用。 -
另外,传入的函数还要遵循
nodeCallback的规范:-
回调函数在主函数参数的位置是最后一个
-
回调函数的第一个参数是
error
-
-
-
代码实现:
const promisify = (fn) => { return (...args) => new Promise((resolve, reject) => { fn(...args, (err, data) => (err ? reject(err) : resolve(data))); }); };
-
-
手撕
promise:此处的代码实现是笔者闲着没事撕着玩的,存在诸多缺陷,不过应付面试应该够用。
代码:
type MyPromiseStatus = "pending" | "fulfilled" | "rejected"; const isFunction = (a: any) => Object.prototype.toString.call(a) === "[object Function]"; class MyPromise<T> { status: MyPromiseStatus = "pending"; taskQueue: Record< Exclude<MyPromiseStatus, "pending">, ((...args: any) => any)[] > = { fulfilled: [], rejected: [], }; value: any = undefined; reason: any; constructor( fn: (resolve: (val: T) => void, reject: (err: any) => void) => any ) { if (!isFunction(fn)) throw new Error("The argument 'fn' must be function type"); const resolve = (val: T) => { if (this.status !== "pending") return; this.status = "fulfilled"; this.value = val; this.taskQueue.fulfilled.forEach((item) => item(this.value)); }; const reject = (err: any) => { if (this.status !== "pending") return; this.status = "rejected"; this.reason = err; this.taskQueue.rejected.forEach((item) => item(this.reason)); }; if (fn) fn(resolve, reject); } then = (fulfilledCb?: (val?: T) => any, rejectedCb?: (err?: any) => any) => { const _fulfilledCb = fulfilledCb && isFunction(fulfilledCb) ? fulfilledCb : (x: any) => x; const _rejectedCb = rejectedCb && isFunction(rejectedCb) ? rejectedCb : (x: any) => x; return new MyPromise((resolve, reject) => { switch (this.status) { case "fulfilled": return resolve(_fulfilledCb(this.value)); case "rejected": return reject(_rejectedCb(this.reason)); case "pending": this.taskQueue.fulfilled.push((value: T) => { setTimeout(() => { resolve(_fulfilledCb(value)); }); }); this.taskQueue.rejected.push((reason: any) => { setTimeout(() => { reject(_rejectedCb(reason)); }); }); } }); }; catch = (rejectedCb: (err?: any) => any) => this.then(undefined, rejectedCb); } new MyPromise<number>((resolve, reject) => { console.log(1); setTimeout(() => { console.log(3); resolve(3); }, 1000); }).then((res = 0) => { console.log(res + 1); });
-
垃圾回收(GC)
-
概述:
JavaScript的垃圾回收机制用于检测和清除不再使用的对象,以释放内存空间。这种机制的优点就是无需手动释放内存,可以帮助开发人员避免内存泄漏和其他相关问题。 -
要点:
-
引用计数(旧的浏览器使用):
-
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。标记清除:定期检查内存中的所有变量和对象,标记那些不再被引用的变量和对象,然后将其清除。
-
处理循环引用的对象会出现问题
-
-
标记清除(新的浏览器使用):
-
假定设置一个叫做根(
root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。该算法把“对象是否不再需要”简化定义为“对象是否可以获得”。 -
解决了引用计数无法处理循环引用的问题。
-
-
V8的垃圾回收(来自
chatgpt):-
分代垃圾回收:
V8将内存分为新生代和老生代两个部分。新生代中的对象生命周期较短,而老生代中的对象生命周期较长。V8采用不同的垃圾回收算法对新生代和老生代进行回收。 -
标记-清除算法:
V8采用标记-清除算法对老生代进行垃圾回收。该算法分为标记和清除两个阶段。V8会标记所有仍然在使用的对象,然后清除所有未被标记的对象。 -
增量标记算法:为了避免长时间的垃圾回收导致的程序卡顿,V8采用了增量标记算法。该算法将标记阶段分为多个小阶段,每个小阶段执行完毕后就会让程序继续执行,从而减少了程序的停顿时间。
-
空间复制算法:V8采用空间复制算法对新生代进行垃圾回收。该算法将新生代内存空间分为两个相等的部分(
from和to),每次只使用其中的一半空间,当其中一半空间被占满后,就将其中还存活的对象复制到另一半空间中,然后清除之前使用的空间。 -
对象晋升:当一个对象在新生代中经历了多次垃圾回收后仍然存活,就会被晋升到老生代中。
-
-
-
内存泄漏问题常见场景(来自
chatgpt):-
循环引用:循环引用是指两个或多个对象相互引用,形成闭环,导致垃圾回收器无法回收这些对象的内存。解决方法是使用
WeakMap或WeakSet来存储对象引用,这些容器可以在对象不再被使用时自动删除引用。 -
DOM对象:在使用JavaScript操作DOM对象时,如果没有正确地释放对象引用,就会导致内存泄漏。解决方法是在不需要使用DOM对象时,手动将其引用设置为null,使其成为垃圾回收的对象。 -
定时器:在使用定时器时,如果不及时清除定时器,就会导致内存泄漏。解决方法是在不需要使用定时器时,手动清除定时器。
-
闭包:闭包是指函数中包含对外部变量的引用,导致这些变量无法被垃圾回收器回收。解决方法是在不需要使用闭包时,手动将其引用设置为
null。 -
全局变量:全局变量会一直存在于内存中,直到程序结束。解决方法是使用模块化的方式来管理变量,将变量封装在模块内部,避免污染全局命名空间。
-
大量数据:在处理大量数据时,需要注意及时释放不再使用的数据,避免内存泄漏。解决方法是使用分页或滚动加载等方式,减少一次性加载大量数据的情况。
-
闭包
-
定义:
JavaScript的闭包机制是指函数可以访问其定义时所在的作用域中的变量,即使函数在定义时已经离开了该作用域。 -
任何闭包的使用场景都离不开这两点:
-
创建私有变量
-
延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
-
-
手撕环节:
-
函数柯里化:
function curry(fn) { const length = fn.length return function curried(...args) { if (args.length >= length) { return fn.apply(this, args); } else { return function(..._args) { return curried.apply(this, [...args, ..._args]); } } } } -
防抖和节流:
function debounce(fn, time) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, time); }; }function throttle(fn, time) { let lock = false; return function (...args) { if (lock) return; lock = true; setTimeout(() => { fn.apply(this, args); lock = false; }, time); }; }
-
-
结合上面的垃圾回收部分的内容,我们可以知道,在创建闭包时所在词法环境的引用不会被
GC清理掉。如果没有及时释放,就会导致垃圾回收器无法回收这些对象,造成内存泄漏,进而影响程序的性能和稳定性。