一、前言
学习群的几位小伙伴整理了最近这一两周遇到的面试题,广东靓仔这里跟小伙伴们分享一下,想进学习群的小伙伴可以私聊广东靓仔~
二、原题
综合题型
class调用静态方法
class Foo { static bar () { this.baz(); } static baz () { console.log('hello'); } baz () { console.log('world'); } } Foo.bar() // hello
加上static关键字就是静态方法属性,不会被实例继承调用静态方法属性直接调用。
Defer/async 对文档的影响
Defer
如果script标签设置了该属性,则浏览器会异步的下载该文件并且不会影响到后续DOM的渲染;
如果有多个设置了defer的script标签存在,则会按照顺序执行所有的script;
defer脚本会在文档渲染完毕后,DOMContentLoaded事件调用前执行。
async
async的设置,会使得script脚本异步的加载并在允许的情况下执行
async的执行,并不会按着script在页面中的顺序来执行,而是谁先加载完谁执行。
如何限制Promise“并发”的数量
js是单线程,并不存在真正的并发,但是由于JavaScript的Event Loop机制,使得异步函数调用有了“并发”这样的假象。
class LimitPromise { constructor (max) { // 异步任务“并发”上限 this._max = max // 当前正在执行的任务数量 this._count = 0 // 等待执行的任务队列 this._taskQueue = [] } /** * 调用器,将异步任务函数和它的参数传入 * @param caller 异步任务函数,它必须是async函数或者返回Promise的函数 * @param args 异步任务函数的参数列表 * @returns {Promise<unknown>} 返回一个新的Promise */ call (caller, ...args) { return new Promise((resolve, reject) => { const task = this._createTask(caller, args, resolve, reject) if (this._count >= this._max) { // console.log('count >= max, push a task to queue') this._taskQueue.push(task) } else { task() } }) } /** * 创建一个任务 * @param caller 实际执行的函数 * @param args 执行函数的参数 * @param resolve * @param reject * @returns {Function} 返回一个任务函数 * @private */ _createTask (caller, args, resolve, reject) { return () => { // 实际上是在这里调用了异步任务,并将异步任务的返回(resolve和reject)抛给了上层 caller(...args) .then(resolve) .catch(reject) .finally(() => { // 任务队列的消费区,利用Promise的finally方法,在异步任务结束后,取出下一个任务执行 this._count-- if (this._taskQueue.length) { // console.log('a task run over, pop a task to run') let task = this._taskQueue.shift() task() } else { // console.log('task count = ', count) } }) this._count++ // console.log('task run , task count = ', count) } } }
核心函数就两个:
调用器:就是把真正的执行函数和参数传入,创建返回一个新的Promise,而这个新Promise的什么时候返回,取决于这个异步任务何时被调度。Promise内部主要就是创建一个任务,判断任务是执行还是入队。
创建任务:实际上就是返回了一个函数,将真正的执行函数放在里面执行。这里利用了Promise的finally方法,在finally中判断是否执行下一个任务,实现任务队列连续消费的地方就是这里。
es5的继承和es6的继承
es5继承
1.原型链继承
缺点:创建实例时不能传递参数,所有属性都被实例共享
function Parent() { this.name = 'kevin'; } Parent.prototype.getName = function () { console.log(this.name); } function Child() { } Child.prototype = new Parent(); var child = new Child(); child.getName() //kevin
2.借用构造函数
优点:可以传递参数,避免了引用类型共享缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Parent() { this.name = ['kevin']; } function Child() { Parent.call(this); } Child.prototype = new Parent(); var child1 = new Child(); var child2 = new Child(); child1.name.push("cc"); console.log(child1.name); //["kevin", "cc"] console.log(child2.name); //["kevin"]
3.组合继承
优点:融合了原型继承和构造函数继承,是JavaScript中常用的设计模式。缺点:调用两次父构造函数
function Parent() { this.name = ['kevin']; } Parent.prototype.getName = function () { console.log(this.name, this.age); } function Child(age) { Parent.call(this); this.age = age; } Child.prototype = new Parent(); var child1 = new Child(19); var child2 = new Child(20); child1.name.push("cc"); child1.getName(); //["kevin", "cc"] 19 child2.getName(); //["kevin"] 20
4.原型式继承
这是es5中的,Object.create的模拟实现。缺点:创建实例时不能传递参数,所有属性都被实例共享
function createObj(o) { function F() { } F.prototype = o; return new F(); }
5.寄生式继承
function createObj(o) { var clone = Object.create(o); clone.say = () => { console.log(this) }; return clone } console.log(createObj({})); //{say: ƒ}
6.寄生组合式继承优点:开发人员普遍认为寄生组合式继承是最理想的继承范式
function Parent(name) { this.name = name; } Parent.prototype.getName = function () { console.log(this.name, this.age); } function Child(name, age) { Parent.call(this, name); this.age = age; } var F = function () { } F.prototype = Parent.prototype; Child.prototype = new F(); var child1 = new Child("cc", 20) console.log(child1) //Child {name: "cc", age: 20}
es6继承
1.class通过extends关键字实现继承
class Parent { constructor(x, y) { this.x = x; this.y = y } } class Child extends Parent { constructor(x, y, name) { super(x, y);//调用父类的constructor(x,y) this.name = name; } } var child1 = new Child("x", "y", "ccg"); console.log(child1); //Child {x: "x", y: "y", name: "ccg"}
2.super关键字
class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A(); new B();
作为函数,super只能在子类的构造函数中。但是如果作为对象时,在普通方法中指向父类的原型对象;在静态方法中指向父类。(!由于super指向父类的原型对象,所以定义在父类实例的方法无法通过super调用)。
创建平移动画为何 translate() 优于 top/right/bottom/left
position
top、left多个步骤占用了CPU时间
使用position,浏览器要在动画的执行中不停地绘制
translate()
使用translate来做平移运动,大部分时间都不需要CPU参与
transform会生成一个新的层,而对这个图层进行变换,对于浏览器来说,是不需要重绘的。
拿Chrome来说会预先将层的内容绘制成位图发送给GPU,如果层仅仅是位置与透明度等特定的一些属性发生变化,而不是内容发生变化,则无需重绘这一位图。因此无论是translate动画方案,还是加上3D变换属性的top、left方案,由于其都在独立的层上,且只是发生位置变化,因此无需重绘。
type和interface的区别
相同点
1、都可以描述一个对象或者函数2、扩展(extends)与交叉类型(intersection types)
不同点
1.type可以声明 基本类型,联合类型,元组 的别名,interface不行代码如下:
// 基本类型别名 type Name = string // 联合类型 interface Dog { wong(); } interface Cat { miao(); } type Pet = Dog | Cat // 具体定义数组每个位置的类型 type PetList = [Dog, Pet]
2. type 支持类型映射,interface不支持
type Keys = "guangdong" | "liangzai" type DudeType = { [key in Keys]: string } const test: DudeType = { firstname: "guangdong", surname: "liangzai" }
SSE和websocket的区别
不同点
SSE:server send event。服务端发送事件,指服务端主动给客户端推送消息(单向)WebSocket:客户端和服务端实现双工通信(双向),多用于即时通信
相同点
SSE和WebSocket可以进行连接保持,针对频繁与服务操作的场景可以减少高频创建关闭连接造成的不必要大量资源开销;
事件循环
Event Loop 包含两类:
1. 一类是基于 Browsing Context ,
2. 一种是基于 Worker
二者是独立运行的。
JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
任务队列
任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;
而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。
在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
- 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
- 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
- 更新 render
- 主线程重复执行上述步骤
闭包原理
全局执行环境
全局执行环境指的是最外层的执行环境。在web中全局执行环境被认为window对象,所以你在全局环境中创建的变量与函数都是对象的属性和方法。
函数执行环境
函数执行环境指的是函数体。
块级执行环境
块级执行环境指的是块级定义区域。
'use strict'; // 全局执行环境 // ..... { // 块级执行环境 // 代码 .... } function func() { // 函数执行环境 //... }
变量对象
每一个执行环境最有一个与之关联的变量对象,变量对象中存储当前环境中定义的变量与函数。在使用变量或函数时,都是在个变量对象上去寻找成员的。这个对象是无法访问的,但是你可以在作用域链[scope]中查看到所定义的成员(如果没有使用的话可能无法看到,这和优化有关)。
环境栈
每个函数或块都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入“环境栈”中。函数执行完后,栈将其弹出并销毁变量对象,然后把控制权返回在给之前的执行环境。如果内执行环境的变量对象,被外部执行环境引用,那么内部环境变量对象就无法被销毁(如:闭包)。
作用域链
作用域链是一个列表,存储着与执行环境相关的变量对象,通过【scope】属性可查看变量对象列表。
Demo:常见的函数嵌套
'use strict'; function a() { let x = 2; return function () { return x; } } let func = a(); // 返回a函数体内的 匿名函数 console.log(func()); // 在全局执行环境中,访问a函数内部变量。 如果是非闭包函数,那么执行完后
访问块内部变量1:返回块级内容函数 实现在全局执行环境中访问块级内容变量
'use strict'; let func = null; { let x = "你好"; func = function () { return x; } } // 返回块级内容函数 实现在全局执行环境中访问块级内容变量。 console.log(func());