1. 首页

2019面试题宝典,轻松拿offer

说说js中的词法作用域

js中只有词法作用域,也就是说在定义时而不是执行时确定作用域。例如:


var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar();<br>//1 javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

注意: with和eval可以修改词法作用域

##

什么是闭包

《深入浅出nodejs》中对闭包的定义:

在js中,实现外部作用域访问内部作用域中变量的方法叫做“闭包”。

##

说说js的垃圾回收(GC)

v8的垃圾回收策略主要基于分代式垃圾回收机制。将内存分为新生代和老生代,分别采用不同的算法。

新生代采用Scavenge算法

Scavenge为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。它将内存分为from和to两个空间。每次gc,会将from空间的存活对象复制到to空间。然后两个空间角色对换(又称反转)。
该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。

老生代采用Mark-Sweep 和 Mark-Compact

老生代中对象存活时间较长,不适合Scavenge算法。
Mark-Sweep是标记清除的意思。Scavenge是只复制存活对象,而Mark-Sweep是只清除死亡对象。该算法分为两个步骤:

  1. 遍历堆中所有对象并标记活着的对象
  2. 清除没有标记的对象

Mark-Sweep存在一个问题,清除死亡对象后会造成内存空间不连续,如果这时候再分配一个大对象,所有的空间碎片都无法完成此次分配,就会造成提前触发gc。这时候v8会使用Mark-Compact算法。
Mark-Copact是标记整理的意思。它会在标记完成之后将活着的对象往一端移动,移动完成后直接清理掉边界外的内存。因为存在整理过程,所以它的速度慢于Mark-Sweep,node中主要采用Mark-Sweep。

Incremental Marking

为了避免出现Javascript应用逻辑与垃圾回收器看到的情况不一致,垃圾回收时应用逻辑会停下来。这种行为被成为全停顿(stop-the-world)。这对老生代影响较大。
Incremental Marking称为增量标记,也就是拆分为许多小的“步进”,每次做完一“步进”,就让Javascript执行一会儿,垃圾回收与应用逻辑交替执行。
采用Incremental Marking后,gc的最大停顿时间较少到原来的 1 / 6 左右。

v8的内存限制

  • 64位系统最大约为1.4G
  • 32位系统最大约为0.7G

node中查看内存使用量


➜ ~ node > process.memoryUsage() //node进程内存使用 { rss: 27054080, // 进程常驻内存 heapTotal: 7684096, // 已申请到的堆内存 heapUsed: 4850344, // 当前使用的堆内存 external: 9978 // 堆外内存(不是通过v8分配的内存) > os.totalmem() //系统总内存 17179869184 > os.freemem() //系统闲置内存 3239858176 javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

说说你了解的设计模式

发布订阅模式

在js中事件模型就相当于传统的发布订阅模式,具体实现参考[实现一个node中的EventEmiter][21]

策略模式

定义: 定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。

策略模式实现表单校验
const strategies = {
    isNoEmpty: function(value, errorMsg){
        if(value.trim() === ''){
            return errorMsg
        }
    },
    maxLength: function(value, errorMsg, len) {
        if(value.trim() > len) {
            return errorMsg
        }
    }
}

class Validator {
  constructor() {
    this.catch = [];
  }
  add(value, rule, errorMsg, ...others) {
    this.catch.push(function() {
      return strategies[rule].apply(this, [value, errorMsg, ...others]);
    });
  }
  start() {
    for (let i = 0, validatorFunc; (validatorFunc = this.catch[i++]); ) {
      let msg = validatorFunc();
      if (msg) {
        return msg;
      }
    }
  }
}

//使用
const validatorFunc = function() {
    const validator = new Validator();
    validator.add(username, 'isNoEmpty', '用户名不能为空');
    validator.add(password, 'isNoEmpty', '密码不能为空');
    const USERNAME_LEN = PASSWORD_LEN = 10;
    validator.add(username, 'maxLength', `用户名不能超过${USERNAME_LEN}个字`, USERNAME_LEN);
    validator.add(password, 'isNoEmpty', `密码不能为空${PASSWORD_LEN}个字`, PASSWORD_LEN);
    let msg = validator.start();
    if(msg) {
        return msg;
    }
}

javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

命令模式

应用场景: 有时候我们要向某些对象发送请求,但不知道请求的接收者是谁,也不知道请求的操作是什么,此时希望以一种松耦合的方式来设计软件,使得请求的发送者和接收者能够消除彼此的耦合关系。

命令模式实现动画

class MoveCommand { constructor(reciever, pos) { this.reciever = reciever; this.pos = pos; this.oldPos = null; } excute() { this.reciever.start("left", this.pos, 1000); this.reciever.getPos(); } undo() { this.reciever.start("left", this.oldPos, 1000); } } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

ES6 模块与 CommonJS 模块的差异

  1. CommonJS输出的是值的拷贝,ES6模块输出的是值的引用。
    也就是说CommonJS引用后改变模块内变量的值,其他引用模块不会改变,而ES6模块会改变。

  2. CommonJS是运行时加载,ES6模块是编译时输出接口。
    之所以Webpack的Tree Shaking是基于ES6的,就是因为ES6在编译的时候就能确定依赖。因为使用babel-preset-2015这个预设默认是会把ES6模块编译为CommonJS的,所以想使用Tree Shaking还需要手动修改这个预设。


module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [['babel-preset-es2015', {modules: false}]], } } } ] } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

async函数实现原理

async函数是基于generator实现,所以涉及到generator相关知识。在没有async函数之前,通常使用co库来执行generator,所以通过co我们也能模拟async的实现。


function Asyncfn() { return co(function*() { //..... }); } function co(gen) { return new Promise((resolve, reject) => { const fn = gen(); function next(data) { let { value, done } = fn.next(data); if (done) return resolve(value); Promise.resolve(value).then(res => { next(res); }, reject); } next(); }); } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

说说浏览器和node中的事件循环(EventLoop)

浏览器

019面试题宝典,轻松拿offer"

如图:浏览器中相对简单,共有两个事件队列,当主线程空闲时会清空Microtask queue(微任务队列)依次执行Task Queue(宏任务队列)中的回调函数,每执行完一个之后再清空Microtask queue。

“当前执行栈” -> “micro-task” -> “task queue中取一个回调” -> “micro-task” -> … (不断消费task queue) -> “micro-task”

nodejs

node中机制和浏览器有一些差异。node中的task queue是分为几个阶段,清空micro-task是在一个阶段结束之后(浏览器中是每一个任务结束之后),各个阶段如下:

┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用(可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on(‘close’, …)
└───────────────────────┘

这里我们主要关注其中的3个阶段:timer、poll和check,其中poll队列相对复杂:

轮询 阶段有两个重要的功能:
1、计算应该阻塞和轮询 I/O 的时间。
2、然后,处理 轮询 队列里的事件。

当事件循环进入 轮询 阶段且 没有计划计时器时 ,将发生以下两种情况之一:
1、如果轮询队列不是空的,事件循环将循环访问其回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬限制。
2、如果轮询队列是空的,还有两件事发生:
a、如果脚本已按 setImmediate() 排定,则事件循环将结束 轮询 阶段,并继续 check阶段以执行这些计划脚本。
b、如果脚本 尚未 按 setImmediate()排定,则事件循环将等待回调添加到队列中,然后立即执行。

一旦轮询队列为空,事件循环将检查已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。

细节请参考[The Node.js Event Loop, Timers, and process.nextTick()][22]
中文:[Node.js 事件循环,定时器和 process.nextTick()][23]

通过程序理解浏览器和node中的差异


setTimeout(() => { console.log("timer1"); Promise.resolve().then(function() { console.log("promise1"); }); }, 0); setTimeout(() => { console.log("timer2"); Promise.resolve().then(function() { console.log("promise2"); }); }, 0); javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

在浏览器中的顺序是:timer1 -> promise1 -> timer2 -> pormise2
node中顺序是: timer1 -> timer2 -> promise1 -> promise2
这道题目很好的说明了node中的micro-task是在一个阶段的任务执行完之后才清空的。

##

实现一个node中的EventEmiter

简单实现:


class EventsEmiter { constructor() { this.events = {}; } on(type, fn) { const events = this.events; if (!events[type]) { events[type] = [fn]; } else { events[type].push(fn); } } emit(type, ...res) { const events = this.events; if (events[type]) { events[type].forEach(fn => fn.apply(this, res)); } } remove(type, fn) { const events = this.events; if (events[type]) { events[type] = events[type].filer(lisener => lisener !== fn); } } } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

实现一个node中util模块的promisify方法


let fs = require("fs"); let read = fs.readFile; function promisify(fn) { return function(...args) { return new Promise((resolve, reject) => { fn(...args, (err, data) => { if (err) { reject(err); } resolve(data); }); }); }; } // 回调用法 // read("./test.json", (err, data) => { // if (err) { // console.error("err", err); // } // console.log("data", data.toString()); // }); // promise用法 let readPromise = promisify(read); readPromise("./test.json").then(res => { console.log("data", res.toString()); }); javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

如何实现一个自定义流

根据所创建的流类型,新的流类必须实现一个或多个特定的方法,如下图所示:

用例 需实现的方法
只读流 Readable _read
只写流 Writable _write, _writev, _final
可读可写流 Duplex _read, _write, _writev, _final
对写入的数据进行操作,然后读取结果 Transform _transform, _flush, _final

以双工流为例:


const { Duplex } = require('stream'); class Myduplex extends Duplex { constructor(arr, opt) { super(opt); this.arr = arr this.index = 0 } //实现可读流部分 _read(size) { this.index++ if(this.index === 3) { this.push(null) } else { this.push(this.index.toString()) } } //实现可写流 _write(chunk, encoding, callback) { this.arr.push(chunk.toString()) callback() } } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

更多内容可以参考我的另一篇文章:[说说node中可读流和可写流][39] 和 [nodejs官网][40]

##

性能优化之dns-prefetch、prefetch、preload、defer、async

dns-prefetch

域名转化为ip是一个比较耗时的过程,dns-prefetch能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要dns预取。


//来自百度首页 <link rel="dns-prefetch" href="//m.baidu.com"> javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

prefetch

prefetch一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载prefetch的资源。


<link rel="prefetch" href="http://www.example.com/"> javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

preload

和prefetch不同,prefecth通常是加载接下来可能用到的页面资源,而preload是加载当前页面要用的脚本、样式、字体、图片等资源。所以preload不是空闲时加载,它的优先级更强,并且会占用http请求数量。


<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')" javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

as值包括

  • “script”
  • “style”
  • “image”
  • “media”
  • “document” onload方法是资源加载完成的回调函数

defer和async


//defer <script defer src="script.js"></script> //async <script async src="script.js"></script> javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

defer和async都是异步(并行)加载资源,不同点是async是加载完立即执行,而defer是加载完不执行,等到所有元素解析完再执行,也就是DOMContentLoaded事件触发之前。
因为async加载的资源是加载完执行,所以它比不能保证顺序,而defer会按顺序执行脚本。

##

说说react性能优化

shouldComponentUpdate

举例:下面是antd-design-mobile的Modal组件中对的内部蒙层组件的处理


import * as React from "react"; export interface lazyRenderProps { style: {}; visible?: boolean; className?: string; } export default class LazyRender extends React.Component<lazyRenderProps, any> { shouldComponentUpdate(nextProps: lazyRenderProps) { return !!nextProps.visible; } render() { const props: any = { ...this.props }; delete props.visible; return <div {...props} />; } } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

immutable

像上面这种只比较了一个visible属性,并且它是string类型,如果是一个object类型那么就不能直接比较了,这时候使用immutable库更好一些。
immutable优势:

  • 性能更好
  • 更加安全 immutable劣势:
  • 库比较大(压缩后大约16k)
  • api和js不兼容

解决方案:seamless-immutable seamless-immutable这个库没有完整实现Persistent Data Structure,而是使用了Object.defineProperty扩展了JS的Object和Array对象,所以保持了相同的Api,同时库的代码量更少,压缩后大约2k

基于key的优化

文档中已经强调,key需要保证在当前的作用域中唯一,不要使用当前循环的index(尤其在长列表中)。
参考 [reactjs.org/docs/reconc…][41]

##

说说浏览器渲染流程

浏览器的主进程:Browser进程

  1. 负责下载资源
  2. 创建销毁renderer进程
  3. 负责将renderer进程生成的位图渲染到页面上
  4. 与用户交互

浏览器内核:renderer进程

js引擎线程

由一个主线程和多个web worder线程组成,web worker线程不能操作dom

GUI线程

用于解析html生成DOM树,解析css生成CSSOM,布局layout、绘制paint。回流和重绘依赖该线程

事件线程

当事件触发时,该线程将事件的回调函数放入callback queue(任务队列)中,等待js引擎线程处理

定时触发线程

setTimeout和setInterval由该线程来记时,记时结束,将回调函数放入任务队列

http请求线程

每有一个http请求就开一个该线程,每当检测到状态变更就会产生一个状态变更事件,如果这个事件由对应的回掉函数,将这个函数放入任务队列

任务队列轮询线程

用于轮询监听任务队列

流程

  1. 获取html文件
  2. 从上到下解析html
  3. 并行请求资源(css资源不会阻塞html解析,但是会阻塞页面渲染。js资源会组织html解析)
  4. 生成DOM tree 和 style rules
  5. 构建render tree
  6. 执行布局过程(layout、也叫回流),确定元素在屏幕上的具体坐标
  7. 绘制到屏幕上(paint)

事件

DOMContentLoaded

当初始的HTML文档被完全加载和解析完成(script脚本执行完,所属的script脚本之前的样式表加载解析完成)之后,DOMContentLoaded事件被触发

onload

所有资源加载完成触发window的onload事件

参考流程图:[www.processon.com/view/5a6861…][42]

##

说说http2.0

http2.0是对SPDY协议的一个升级版。和http1.0相比主要有以下特性:

  • 二进制分帧
  • 首部压缩
  • 多路复用
  • 请求优先级
  • 服务端推送(server push)

详细可参考: [HTTP—-HTTP2.0新特性][43]

##

实现一个reduce方法

注意边界条件:1、数组长度为0,并且reduce没有传入初始参数时,抛出错误。2、reduce有返回值。


Array.prototype.myReduce = function(fn, initial) { if (this.length === 0 && !initial) { throw new Error("no initial and array is empty"); } let start = 1; let pre = this[0]; if (initial) { start = 0; pre = initial; } for (let i = start; i < this.length; i++) { let current = this[i]; pre = fn.call(this, pre, current, i); } return pre; }; javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

实现一个promise.all方法,要求保留错误并且并发数为3

标准的all方法是遇到错误会立即将promise置为失败态,并触发error回调。保留错误的定义为:promise遇到错误保存在返回的结果中。


function promiseall(promises) { return new Promise(resolve => { let result = []; let flag = 0; let taskQueue = promises.slice(0, 3); //任务队列,初始为最大并发数3 let others = promises.slice(3); //排队的任务 taskQueue.forEach((promise, i) => { singleTaskRun(promise, i); }); let i = 3; //新的任务从索引3开始 function next() { if (others.length === 0) { return; } const newTask = others.shift(); singleTaskRun(newTask, i++); } function singleTaskRun(promise, i) { promise .then(res => { check(); result[i] = res; next(); }) .catch(err => { check(); result[i] = err; next(); }); } function check() { flag++; if (flag === promises.length) { resolve(result); } } }); } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

测试代码:


let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve("1"); }, 1000); }); let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve("2"); }, 1500); }); let p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve("3"); }, 2000); }); let p4 = new Promise((resolve, reject) => { setTimeout(() => { resolve("4"); }, 2500); }); let p_e = new Promise((resolve, reject) => { // throw new Error("出错"); reject("错误"); }); let p5 = new Promise((resolve, reject) => { setTimeout(() => { resolve("5"); }, 5000); }); let all = promiseall([p_e, p1, p3, p2, p4, p5]); all.then( data => { console.log("data", data); // [ '错误', '1', '3', '2', '4', '5' ] } ); javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

不用递归函数求一个二叉树的高度

先看一下递归的实现(二叉树的深度优先遍历):


function getBinaryTreeHeigth(node) { let maxDeep = 0; function next(n, deep) { deep++; if (n.l) { let newDeep = next(n.l, deep); if (newDeep > maxDeep) { maxDeep = newDeep; } } if (n.r) { let newDeep = next(n.r, deep); if (newDeep > maxDeep) { maxDeep = newDeep; } } return deep; } next(node, 0); return maxDeep; } function Node(v, l, r) { this.v = v; this.l = l; this.r = r; } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

非递归的实现(二叉树的广度优先遍历):


function getBinaryTreeHeigth(node) { if (!node) { return 0; } const queue = [node]; let deep = 0; while (queue.length) { deep++; for (let i = 0; i < queue.length; i++) { const cur = queue.pop(); if (cur.l) { queue.unshift(cur.l); } if (cur.r) { queue.unshift(cur.r); } } } return deep; } function Node(v, l, r) { this.v = v; this.l = l; this.r = r; } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

js中求两个大数相加

给定两个以字符串形式表示的非负整数 num1 和 num2,返回它们的和,仍用字符串表示。

输入:num1 = ‘1234’, num2 = ‘987’
输出:’2221′


function bigIntAdd(str1, str2) { let result = []; let ary1 = str1.split(""); let ary2 = str2.split(""); let flag = false; //是否进位 while (ary1.length || ary2.length) { let result_c = sigle_pos_add(ary1.pop(), ary2.pop()); if (flag) { result_c = result_c + 1; } result.unshift(result_c % 10); if (result_c >= 10) { flag = true; } else { flag = false; } } return result.join(""); } function sigle_pos_add(str1_c, str2_c) { let l = (r = 0); if (str1_c) { l = Number(str1_c); } if (str2_c) { r = Number(str2_c); } return l + r; } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

测试代码:


const str1 = "1234"; const str2 = "987654321"; const str3 = "4566786445677555"; const str4 = "987"; console.log(bigIntAdd(str1, str4)) //'2221' console.log(bigIntAdd(str2, str3)) //'4566787433331876' javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

实现一个数组随机打乱算法


function disOrder(ary) { for (let i = 0; i < ary.length; i++) { let randomIndex = Math.floor(Math.random() * ary.length); swap(ary, i, randomIndex); } } function swap(ary, a, b) { let temp = ary[a]; ary[a] = ary[b]; ary[b] = temp; } let ary = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; disOrder(ary); console.log(ary); javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

##

给数字增加“逗号”分隔

输入: ‘”123456789.012″‘ 输出:123,456,789.012

正则解法:


function parseNumber(num) { if (!num) return ""; return num.replace(/(\d)(?=(\d{3})+\.)/g, "$1,"); } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

非正则:


function formatNumber(num) { if (!num) return ""; let [int, float] = num.split("."); let intArr = int.split(""); let result = []; let i = 0; while (intArr.length) { if (i !== 0 && i % 3 === 0) { result.unshift(intArr.pop() + ","); } else { result.unshift(intArr.pop()); } i++; } return result.join("") + "." + (float ? float : ""); } javascriptC中文网 - 前端进阶资源分享(www.javascriptc.com)

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文来源于网络,其版权属原作者所有,如有侵权,请与小编联系,谢谢!

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:2019面试题宝典,轻松拿offer

链接:https://www.javascriptc.com/952.html

原文链接:https://juejin.im/post/5d89ac2ff265da03c34c3cd2

« 系统权限按需访问路由几个完整方案(含addRoutes的填坑)
5 个 JS 不良编码习惯,你占几个呢»
Flutter 中文教程资源

相关推荐

QR code