前端面试题系列(1)
1、已知如下对象,请基于es6的proxy方法设计一个属性拦截读取操作的例子
【前端面试题系列(1)】要求实现去访问目标对象example中不存在的属性时,抛出错误:Property "$(property)" does not exist (2018 今日头条)
// 案例代码
const man = {
name: 'jscoder',
age: 22
}
//补全代码
const proxy = new Proxy(...)
proxy.name // "jscoder"
proxy.age // 22
proxy.location // Property "$(property)" does not exist
回答:
const proxy = new Proxy(main, {
get (target, property) {
if (property in target) {
return target[property];
} else {
throw Error(`Property "${property}" does not exist`)
}
}
});
2、红灯三秒亮一次, 绿灯一秒亮一次, 黄灯2秒亮一次 实现一个函数,如何让三个灯不断交替重复亮灯? (用Promise实现) 三个亮灯函数已经存在:
function red() {
console.log('red')
} // 3秒
function green() {
console.log('green')
} // 1秒
function yellow() {
console.log('yellow')
} // 2秒
回答:
const timerFn = function (times, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
callback();
resolve();
}, times);
});
};
async function repeatLights () {
await timerFn(3000, red);
await timerFn(1000, green);
await timerFn(2000, yellow);
repeatLights();
}repeatLights();
3、按顺序写出控制台打印结果 (2020 碧桂园)
var User = {
count:1,
action:{
getCount:function () {
return this.count
}
}
}
var getCount = User.action.getCount;
setTimeout(() => {
console.log("result 1",User.action.getCount())
})
console.log("result 2",getCount())
回答:
result 2 undefined
result 1 undefined
4、简答 (字节跳动 二面)
- 你觉得typescript和javascript有什么区别
- typescript你都用过哪些类型
- typescript中type和interface的区别
(1)TS 是 JS的一个超集(扩展集),就是在JS的基础上多出一些扩展特性,包括:强大的类型系统、对ES6+的良好支持,TS最终会被编译为JS执行
- JS 和 TS 都是 ECMAScript 的具体实现
- TS 是静态类型,而 JS 是动态类型
- TS 扩展了 JS 并且完全包容 JS
- TS 需要编译,JS不需要编译(除非需要兼容低版本运行环境)
- JavaScript 由 Netscape 率先推出,现在主要由各大浏览器厂商实现
- 而 TypeScript 目前由微软进行设计和维护
数组类型、对象类型、函数类型、元祖、枚举 enum、any 类型、Void类型、接口 interfaces 、 类 class 、 泛型 generics
(3)typescript中type和interface的区别
type 和 interface 相同点:
- 都可以描述一个对象或函数
- 都可以实现扩展(extends)功能, interface extends interface,type 通过交叉类型实现扩展
- type 可以声明基本类型别名,联合类型,元组等类型,而 interface 不行;
- interface 能够声明合并,而 type 不行;
- 一般来说,定义类型时,可以用 interface 实现的就用 interface ,不能就用 type
- async/await 其实就是 generator 生成器的语法糖,它的出现使得我们编写异步代码更加简单方便,可以避免编写复杂的 generator 函数,异步代码中再也没有回调函数,语法就像同步代码一样,是目前异步编程比较完美的方案。
- 内部原理:内部借助了 promise 和 generator 来实现。await 需要配合 async 来使用,就像 yield 需要配合
function * main () { ... }
中的 * 来使用一样,yield 后面可以跟随一个 promise 实例,执行 main 函数可以得到一个 generator 实例 g ,可以通过g.next()
来控制 main 函数内部代码的执行顺序,配合g.next().value.then()
和g.next().done
就能实现 async/await 的效果。
// 可以通过 try catch 来捕获错误
async function main () {
try {
const articles = await ajax('/api/articles.json');
} catch (error) {
console.log(error);
}
}
7、说一下 event loop 的过程?promise 定义时传入的函数什么时候执行?(小米 三面) 回答:
- js是单线程的,同一时间只能做一件事,两段JS不能同时执行,主要原因是要避免DOM渲染的冲突。解决方案就是异步,而异步编写的代码,没按照书写方式执行,callback过多,导致可读性很差,难以理解,所以就出现了 promise / async await。
- event-loop 指事件轮询,是js实现异步的具体解决方案。同步代码,在主线程(调用栈 Call stack)中直接执行,压栈-弹栈。异步任务会依次放入消息队列(Queue)中,EventLoop 会监听调用栈和消息队列,当调用栈中的代码执行完之后,它会拿消息队列中的第一个任务放到调用栈执行,以此类推。
- promise 定义时传入的函数,会在本次调用栈的末尾去执行。
防抖函数
- 防抖函数的应用场景:防抖是指,触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。常见场景有:键盘输入实时搜索时input事件防抖,浏览器窗口改变resize事件防抖等等(页面滚动事件、鼠标移动事件)。
- 实现方式:利用闭包和setTimeout来实现,创建一个debounce函数,定义一个timer变量用来保存定时器,返回一个函数,在函数内管理定时器和fn的执行时机。
- 节流函数的应用场景:节流是指,在连续的时间内不断触发同个事件,会每隔 n 秒才执行一次这个事件。常见场景有:懒加载要监听计算滚动条的位置,用户点击提交按钮只允许一定时间内点击一次等等。
- 实现方式:利用闭包和setTimeout配合一个标识符来实现,创建一个throttle函数,定义一个canRun变量用来保存当次任务是否可执行,返回一个函数,在函数内管理canRun的值和fn的执行时机,当canRun为false时直接返回。
V8 是一款主流的 JavaScript 执行引擎,采用即时编译,速度很快;内存设限,64位操作系统最大1.5G,32位操作系统最大800M,使用垃圾回收机制释放内存。采用分代回收的思想,内存分为新生代、老生代,针对不同对象采用不同算法进行垃圾回收。
新生代对象回收实现:
- 回收过程采用复制算法 + 标记整理
- 新生代内存区分为两个等大小空间 From 和 To
- 使用空间为 From,空闲空间为 To
- 活动对象存储在 From 空间,标记整理后将活动对象拷贝至 To
- From 与 To 交换空间之后完成内存释放
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化
- 采用增量标记进行效率优化
performance.timing 返回一个 PerformanceTiming 对象,这个对象包括了页面相关的性能信息。比如: performance.timing.navigationStart 表示页面开始加载的时间,可以拿首屏加载完成的时间减去 navigationStart 就得到首屏时间。
11、在 EcmaScript 新特性中,暂时性死区有什么作用 回答:
- ES6 中规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错,这在语法上称为 暂时性死区。
- ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免这种错误就很容易了。
- 观察者模式:观察者 -- Watcher,有一个 update() 方法,是当事件发生时,具体要做的事情;目标 -- Dep,subs 数组->存储所有的观察者, addSub() -> 添加观察者, notify() -> 当事件发生时,调用所有观察者的 update() 方法;没有信号中心
- 发布订阅模式:假定存在一个“信号中心”,某个人物执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。
- 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的;
- 发布订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在,两者没有直接依赖
使用gulp写过一些小demo,还未在生产项目中使用过。
gulp的构建流程:
- 创建 gulpfile.js 文件,gulp 的入口文件
- 通过
exports.foo = done => {...}
创建并导出一个构建任务 - 在任务中通过 gulp.src 函数读取文件流,配合文件流的 pipe() 方法将文件流进行处理和转换(交给插件处理或者输出到指定文件中),最后通过 gulp.dest() 方法将转换结果流输出到目标位置
- 然后经过处理后将执行结果返回,结束一个任务的执行
- 通过
gulp.series
创建串行任务,通过gulp.parallel
创建并行任务 - gulp 也支持异步任务,可以配合 promise 和 async 创建任务
- 最后将创建的任务通过 exports 导出,就可以在命令窗口运行定义的gulp任务
gulp foo
- gulp 构建过程的核心原理:输入 => 加工 => 输出,读取流 => 转换流 => 写入流
package-lock.json 作用:
用来锁定安装时的包的版本号和地址,保证项目成员在 npm install 时下载的依赖包版本一致。
如果项目中没有 package-lock.json,而 packag.json 中有依赖包
"lodash": "^4.17.4"
,这时候只能锁定大版本,每次 install 的时候是拉取的该大版本下的最新版本。但是为了项目稳定性我们不应该随意升级依赖包,这可能导致不可预知的风险和适配测试等工作量。15、webpack 常用配置项有哪些,并说明用途 (跟谁学 2020) 回答:
module.exports = {
mode: 'none', // 打包模式, none development production
entry: './src/main.js', // 打包入口文件
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist') // 打包出口
},
devServer: {}, // 配置 devServer contentBase proxy 等
devtool: 'source-map', // 配置 source-map 有 12 中模式可选择
module: {}, // 配置处理转换文件的各种 loader
plugins: [], // 配置打包过程使用的插件 copy-webpack-plugin html-webpack-plugin 等等
optimization: {} // 用于优化打包结果的配置 压缩代码 tree-shaking 移除冗余代码等
}
16、阐述 webpack css-loader 的作用 和 原理? (跟谁学) 回答:
- 作用:css-loader 会对 @import 和 url() 进行处理,用于加载.css文件,就像 js 解析 import/require() 一样。
- 原理:默认 webpack 是只解析js代码的,css-loader 会读取解析.css文件里的css代码成字符串,让我们在打包过程中能解析和应用css代码。通常会配合 style-loader 来使用,它会将 css-loader 解析后的内容挂载到html页面的style标签中。
- loader 专注实现资源模块的转换和加载(编译转换代码、文件操作、代码检查)
- plugin 解决其他自动化工作(打包之前清除 dist 目录、拷贝静态文件、压缩代码等等)
webpack
- 优点:生态齐全,配合各种插件几乎可以实现所有打包需求,现如今前端应用程序最流行的打包工具
- 缺点:文档比较复杂,配置较为繁琐,有一定的学习成本;打包构建结果有很多模块依赖的代码,打包结果较大
- 优点:rollup 很小巧,仅仅是一款 ESM 打包器,输出结果更加扁平,会自动移除未引用的代码,打包结果依然完全可读
- 缺点:加载非 ESM 的第三方模块比较复杂;模块最终都被打包到一个函数中,无法实现 HMR;浏览器环境中,代码拆分功能依赖 AMD
- 优点:零配置的前端应用打包器,构建速度快,可以自动安装依赖。
- 缺点:生态不够完善,不利于扩展插件等是它的缺点。
- .babelrc 的配置文件是针对文件夹的,即该配置文件所在的文件夹包括子文件夹都会应用此配置文件的设置,而且下层配置文件会覆盖上层配置文件,这种方式可以给不同的目录设置不同的规则。
- babel.config.js 写法和 .babelrc 一样,但是 babel.config.js 是针对整个项目的,一个项目只有一个放在项目根目录。
- .babelrc 文件放在项目根目录和 babel.config.js 效果一致,如果两种类型的配置文件都存在,.babelrc 会覆盖 babel.config.js 的配置
- tree-shaking 用途:移除JS中未被引用的代码,尽可能的将所有模块合并输出到一个函数中,提升代码运行效率,较少代码体积
- tree-shaking 原理:利用 ES2015 中的 import 和 export ,找到未被引用的 export 代码或者模块,将它们从打包结果中移除
- 原理:EventBus 事件总线。在Vue中可以使用 EventBus 来作为沟通桥梁(组件通信),指所有组件共用一个相同的事件中心,可以向该中心注册发送事件或接收事件,所有组件都可以灵活的通知其他组件。
- 应用实践:兄弟组件通信、不相关组件之间的通信,比如你在某个组件中修改了一个变量,同时希望在其他组件中监听到这个变量的改变, EventBus.$emit() 用于创建发布事件,EventBus.$on() 用于订阅事件 , EventBus.$off() 用于取消订阅事件
- vue-loader 是一个 webpack 的 loader,它允许我们以一种单文件组件的格式编写 vue 组件,组件一般分为三个模块
- 允许为 vue 组件的每个部分使用其它的 webpack loader,例如在的部分使用 Sass 和在 的部分使用 Pug;使用 scoped CSS;可以将和 中引用的资源当作模块依赖来处理
- vue-loader 不是简单的源转换器。它用自己专用的加载链处理SFC(Single-file Component 单文件组件)内部的每个语言块,最后将这些块组成最终的模块。
- 编译模块的过程中,vue-loader 可以将script的内容视为.js文件(如果是中的代码分别解析出来,并应用webpack的配置
- 比如:假设我们为所有的 *.js 配置过 babel-loader ,经过 vue-loader 处理,这些规则也一样会复制和应用于到Vue SFC的块中。