写在前面
值新的一岁之际,希望自己可以:勤思考,多动手,善总结,能坚持。
建了一个公众号,每周周一 至 周五,每天发布若干道面试题,并奉上个人觉得还行的解答,周六或周天发一遍汇总~。
希望大家有所得,希望自己有所得。
下面是本周的汇总(2020.07.06 - 2020.07.10)。
目录
- 介绍一下原型和原型链
- 介绍一下作用域和作用域链
- 什么是闭包,优缺点和应用
- 求两个数组的交集
- 介绍一下 js 中 this 和指向
- new 操作符调用构造函数,具体做了什么?
- null 和 undefined 的区别?
- {} 和 [] 的 valueOf 和 toString 的结果是什么?
- == 操作符的强制类型转换规则?
- 移动端的点击事件有延迟,时间是多久,为什么会有,怎么解决?
- 什么是 Polyfill?
- 谈谈你对模块化开发的理解
- js 的几种模块规范
- AMD 和 CMD 规范的区别?
- ES6 模块与 CommonJS 模块、AMD、CMD 的差异
- js 延迟加载的方式有哪些?
- 哪些操作会造成内存泄露?
- 介绍一下防抖
- 介绍一下节流
- 介绍一下 V8 垃圾回收机制
- 介绍下常见 http 状态码
详情
一、 介绍一下原型和原型链
什么是原型?
有一个构造函数,当用这个构造函数 new 一个实例出来的时候,这个实例的原型就是这个构造函数。它们之间的关系靠 __proto__ 相连,即 person.__proto__ === Person.prototype 与 person.constructor === Person。
当查找一个对象上的属性找不到时,会沿着这个对象的 __proto__ 一直往上找,就是一个链式结构,即“原型链”。如果最终没有找到,就会返回 undefined。
我给你画个图
二、 介绍一下作用域和作用域链
js 中,变量的作用域有三种:
- 全局作用域
- 函数作用域
- ES6 新增了块级作用域
全局作用域:
- 没有用 var 声明的变量(除去函数的参数)都具有全局作用域,成为全局变量
- window 的所有属性都具有全局作用域
- 最外层函数体外声明的变量也具有全局作用域
最外层的作用域,具有全局作用域的变量可以被任何函数访问。 这样的坏处就是变量间很容易产生冲突,另外该值被修改时很难定位。
函数作用域: 在函数作用域中定义的变量,在函数外部是无法访问的。
ES6 的块级作用域: 只要用花括号包起来的都属于一个块,在其中定义的所有变量在代码块外都是不可见的,称之为块级作用域
要得到一个变量的值,若当前作用域没有定义,就到父级作用域寻找。如果父级作用域中也没找到,就再向上一层寻找,直到找到全局作用域。这种一层一层的关系,就叫做作用域链。
三、 什么是闭包,优缺点和应用
忍者秘籍的定义是:闭包是一个函数在创建时允许该自身函数访问并操作该自身函数之外的变量时所创建的作用域。
我的理解:闭包可以让一个函数访问并操作其声明时的作用域中的变量和函数,并且,即使声明时的作用域消失了,也可以调用。
优点:可以避免全局变量的污染 缺点:参数和变量不会被垃圾回收机制回收,闭包会常驻内存,增大内存使用率,使用不当容易造成内存泄露。
应用:
- 用来封装私有变量
- 回调与计时器
- 绑定函数上下文,也就是
bind的实现方式 - 函数柯里化,即把函数参数分多次传递
- 函数的缓存记忆,即针对纯函数把对应的输入值和输出值缓存下拉,减少重复计算
- 类库包装,例如
JQuery的库把代码封装在立即执行函数中,但把jquery绑定到window上,让window捕获
四、 求两个数组的交集
五、 介绍一下 js 中 this 和指向
概念:执行上下文,this 一般存在于函数中,表示当前函数的执行上下文,如果函数没有执行,那么 this 没有内容,只有函数在执行后 this 才有绑定。
广泛流传版本:谁调用它,this 就指向谁
我的版本:this 的指向,是在调用函数时根据执行上下文所动态确定的。
指向逻辑:
- 在函数体中,简单调用该函数时(非显示/隐式绑定下),严格模式下
this绑定到undefined,否则绑定到全局对象window/global - 一般构造函数
new调用,绑定到新创建的对象上 - 一般由
call/apply/bind方法显示调用时,绑定到指定参数的对象上 - 一般由上下文对象调用,绑定在该对象上
- 箭头函数中,根据外层上下文绑定的
this决定this指向
陷阱点:new 绑定的优先级比显示 bind 绑定更高
六、 new 操作符调用构造函数,具体做了什么?
文字描述:
- 创建一个新的对象(这个对象将会作为执行 new 构造函数() 之后,返回的对象实例)
- 为这个对象添加属性、方法等(即将空对象的原型(
__proto__),指向构造函数的prototype属性) - 将构造函数的
this指向这个新对象(使用apply) - 最终返回新对象
代码描述:
使用:MyNew(Person, 18, 'liuc')
大众版:
function MyNew(fn, ...args) {
const obj = {};
obj.__proto__ = fn.prototype;
const retult = fn.call(obj, ...args);
return typeof result === 'object' ? retult : obj;
}
让人眼前一黑 版:
function MyNew(...args) {
// 取出 args 数组第一个参数,即目标构造函数
const fn = args.shift();
if (!(fn instanceof Function)) {
throw new TypeError('not a constructor');
}
// 创建一个空对象,且这个空对象继承构造函数的 prototype 属性
// 即实现 obj.__proto__ === constructor.prototype
const obj = Object.create(fn.prototype);
// 执行构造函数,得到构造函数返回结果
// 注意这里我们使用 apply, 将构造函数内的 this 指向为 obj
const result = fn.apply(obj, args);
// 如果构造函数执行后,返回结果是对象类型,就直接返回,否则返回 obj 对象
return typeof result === 'object' && result != null ? result : obj;
}
七、 null 和 undefined 的区别?
首先 undefined 和 null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当我们对两种类型使用 typeof 进行判断的时候,Null 类型会返回 “object”,这是一个历史遗留问题。当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
八、 {} 和 [] 的 valueOf 和 toString 的结果是什么?
{} 的 valueOf 结果为 {}, toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [], toString 的结果为 ""
九、 == 操作符的强制类型转换规则?
字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较
其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较
null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。
对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。
如果一个操作值为 NaN,则相等比较返回 false(NaN 本身也不等于 NaN)
如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true,否则,返回 false。
十、 移动端的点击事件有延迟,时间是多久,为什么会有,怎么解决?
移动端点击有 300ms 的延迟,是因为移动端会有双击缩放这个操作,因此浏览器在 click 之后要等待 300ms,看用户有没有下一次点击,来判断这次操作是不是双击。
有三种方法来解决这个问题:
通过 meta 标签禁用网页的缩放
通过 meta 标签将网页的 viewport 设置为 ideal viewport
调用一些 js 库,比如 FastClick
十一、 什么是 Polyfill?
Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。
比如说 querySelectorAll 是很多现代浏览器都支持的原生 Web API,但是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能,使这些浏览器也支持了这个功能,那么这就可以成为一个 Polyfill。
一个 shim 是一个库,有自己的 API, 而不是单纯实现原生不支持的 API。
十二、 谈谈你对模块化开发的理解
一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块间没有联系。
后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种方法会暴露所有的模块成员,外部代码可以修改内部属性的值。
现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
十三、 js 的几种模块规范
js 中现在比较成熟的有四种模块加载方案:
CommonJS,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加适合。
AMD,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
CMD,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行实际的处理不同。
ES6 的模块加载,使用 import 和 export 的形式来导入导出模块。
十四、 AMD 和 CMD 规范的区别?
它们之间的主要区别有两个方面。
模块定义时对依赖的处理不同。AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
// AMD
define(['./a', './b'], function (a, b) {
// 依赖必须一开始就写好
a.doSomething();
// ...
b.doSomething();
});
// CMD
define(function (require, exports, module) {
const a = require('./a');
a.doSomething();
// ...
const b = require('./b'); // 依赖可以就近书写
b.doSomething();
});
十五、 ES6 模块与 CommonJS 模块、AMD、CMD 的差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。也就是说,CommonJS 一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析时,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被夹在的那个模块里面去取值。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
十六、 js 延迟加载的方式有哪些?
js 的加载、解析和执行会阻塞页面的渲染过程,因为我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
我了解到的几种方式是:
将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析弯沉后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本时,如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
十七、 哪些操作会造成内存泄露?
意外的全局变量:无法被回收
被遗忘的定时器或回调函数:导致所引用的外部变量无法被释放
脱离 DOM 的引用:dom 元素被删除时,内存中的引用未被正确清空
闭包:会导致父级中的变量无法被释放
十八、 介绍一下防抖
防抖(debounce):将多次高频操作优化为只在最后一次执行,通常使用的场景有:用户输入,只需要在输入完成后做一次输入校验即可 。
防抖重在清零 clearTimeout(timer)
function debounce(fn, wait = 50, immediate) {
let timer = null;
return (...args) => {
if (immediate && !timer) {
fn(...args);
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, wait);
};
}
十九、 介绍一下节流
节流(throttle):每隔一段时间后执行一次,也就是降低频率,将高频优化成低频操作,通常使用场景:滚动条事件 或者 resize 事件,通常每隔 100 ~ 500 ms 计算一次。
节流重在开关锁 timer=null
function throttle(fn, wait, immediate) {
let timer = null;
let callNow = immediate;
return (...args) => {
if (callNow) {
fn(...args);
callNow = false;
}
if (!timer) {
timer = setTimeout(() => {
fn(...args);
timer = null;
}, wait);
}
};
}
二十、 介绍一下 V8 垃圾回收机制
垃圾回收:将内存中不再使用的数据进行清理,释放出内存空间。V8 将内存分为新生代空间 和 老生代空间。
- 新生代空间:用于存活较短的对象
- 又分为两个空间:from 空间 与 to 空间
- Scavenge GC 算法:当 from 空间被占满时,启动 GC 算法
- 存活的对象从 from 空间 转移到 to 空间
- 清空 from 空间
- from 空间与 to 空间 互换
- 完成一次新生代 GC
- 老生代空间:用于存活时间较长的对象
- 从 新生代空间 转移到 老生代空间的条件
- 经历过一次以上 Scavenge GC 的对象
- 当 to 空间 体积超过 25%
- 标记清除算法:标记存活的对象,未被标记的则被释放
- 增量标记:小模块标记,在代码执行间隙,GC 会影响性能
- 并发标记:不阻塞 js 执行
- 压缩算法:将内存中清除后导致的碎片化对象往内存堆的一段移动,解决 内存的碎片化
- 从 新生代空间 转移到 老生代空间的条件
二十一、 介绍下常见 http 状态码
1xx: 1 开头的是信息状态码
2xx: 2 开头的是请求成功
200 服务器已经成功处理请求,并提供了请求的网页
”
201 用户新建或修改数据成功
202 已接收
204 用户删除成功
3xx: 3 开头的是重定向
301: 永久移动,重定向
”
302:临时移动,可使用原有 URI
304:资源未修改,可使用缓存
305:需代理访问
4xx:4 开头是客户端错误
400 请求语法错误
”
401 用户没有权限,要求身份认证
403 拒绝请求
404 资源不存在
5xx:5 开头的是服务器错误
500 服务器错误,无法完成请求
”
503 服务器目前无法使用(超载或停机维护)
写在最后
前端的世界纷繁复杂,远非笔者所能勾画,部分面试题不是靠背诵记忆就能掌握的,希望我摘录的、总结的简单答案可以引起读者的兴趣,闲暇时可以自己深入总结,如果读者有更好的答案,或有想了解的题目,欢迎留言。
笔者新建了一个 github 仓库,对公众号内发布的面试题做进一步分类,同时也会在周六同步本周题目、公布下周问题,欢迎大家关注~
想要实时关注笔者最新的文章和最新的文档更新请关注公众号前端地基,后续的文章会优先在公众号更新.
github:关注笔者最新的仓库[1]
参考资料
[1]关注笔者最新的仓库: https://github.com/lionel178/easy-web