社会招聘中级前端笔试面试问题汇总
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
typeof null 的结果是什么,为什么?
typeof null 的结果是Object。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
000: object - 当前存储的数据指向一个对象。 1: int - 当前存储的数据是一个 31 位的有符号整数。 010: double - 当前存储的数据指向一个双精度的浮点数。 100: string - 当前存储的数据指向一个字符串。 110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
- undefined的值是 (-2)30(一个超出整数范围的数字);
- null 的值是机器码 NULL 指针(null 指针的值全是 0)
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
常见浏览器所用内核
(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
(2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink内核;
(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
(4) Safari 浏览器内核:Webkit 内核;
(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;
(6) 360浏览器、猎豹浏览器内核:IE + Chrome 双内核;
(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
(8) 百度浏览器、世界之窗内核:IE 内核;
(9) 2345浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;
(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。
一个 tcp 连接能发几个 http 请求?
如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。
而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送
对浏览器的理解
浏览器的主要功能是将用户选择的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。
浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。
- shell 是指浏览器的外壳:例如菜单,工具栏等。主要是提供给用户界面操作,参数设置等等。它是调用内核来实现各种功能的。
- 内核是浏览器的核心。内核是基于标记语言显示内容的程序或模块。
深浅拷贝
1. 浅拷贝的原理和实现
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象
方法一:object.assign
object.assign
是 ES6 中object
的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝
。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
object.assign 的语法为:Object.assign(target, ...sources)
object.assign 的示例代码如下:
let target = {}; let source = { a: { b: 1 } }; Object.assign(target, source); console.log(target); // { a: { b: 1 } };
但是使用 object.assign 方法有几点需要注意
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝
Symbol
类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)}; Object.defineProperty(obj1, 'innumerable' ,{ value:'不可枚举属性', enumerable:false }); let obj2 = {}; Object.assign(obj2,obj1) obj1.a.b = 2; console.log('obj1',obj1); console.log('obj2',obj2);
从上面的样例代码中可以看到,利用
object.assign
也可以拷贝Symbol
类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题
,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能
方法二:扩展运算符方式
- 我们也可以利用 JS 的扩展运算符,在构造对象的同时完成浅拷贝的功能。
- 扩展运算符的语法为:
let cloneObj = { ...obj };
/* 对象的拷贝 */ let obj = {a:1,b:{c:1}} let obj2 = {...obj} obj.a = 2 console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2 console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}} /* 数组的拷贝 */ let arr = [1, 2, 3]; let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和
object.assign
有同样的缺陷,也就是实现的浅拷贝的功能差不多
,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
方法三:concat 拷贝数组
数组的
concat
方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过concat
只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。
let arr = [1, 2, 3]; let newArr = arr.concat(); newArr[1] = 100; console.log(arr); // [ 1, 2, 3 ] console.log(newArr); // [ 1, 100, 3 ]
方法四:slice 拷贝数组
slice
方法也比较有局限性,因为它仅仅针对数组类型
。slice方法会返回一个新的数组对象
,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}]; let newArr = arr.slice(); newArr[2].val = 1000; console.log(arr); //[ 1, 2, { val: 1000 } ]
从上面的代码中可以看出,这就是
浅拷贝的限制所在了——它只能拷贝一层对象
。如果存在对象的嵌套,那么浅拷贝将无能为力
。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝
手工实现一个浅拷贝
根据以上对浅拷贝的理解,如果让你自己实现一个浅拷贝,大致的思路分为两点:
- 对基础类型做一个最基本的一个拷贝;
- 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
const shallowClone = (target) => { if (typeof target === 'object' && target !== null) { const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = target[prop]; } } return cloneTarget; } else { return target; } }
利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了
2. 深拷贝的原理和实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝
。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下
:
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
方法一:乞丐版(JSON.stringify)
JSON.stringify()
是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为JSON
的字符串,并将对象里面的内容转换成字符串,最后再用JSON.parse()
的方法将JSON
字符串生成一个新的对象
let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE
但是该方法也是有局限性的 :
- 会忽略
undefined
- 会忽略
symbol
- 不能序列化函数
- 无法拷贝不可枚举的属性
- 无法拷贝对象的原型链
- 拷贝
RegExp
引用类型会变成空对象 - 拷贝
Date
引用类型会变成字符串 - 对象中含有
NaN
、Infinity
以及-Infinity
,JSON
序列化的结果会变成null
- 不能解决循环引用的对象,即对象成环 (
obj[key] = obj
)。
function Obj() { this.func = function () { alert(1) }; this.obj = {a:1}; this.arr = [1,2,3]; this.und = undefined; this.reg = /123/; this.date = new Date(0); this.NaN = NaN; this.infinity = Infinity; this.sym = Symbol(1); } let obj1 = new Obj(); Object.defineProperty(obj1,'innumerable',{ enumerable:false, value:'innumerable' }); console.log('obj1',obj1); let str = JSON.stringify(obj1); let obj2 = JSON.parse(str); console.log('obj2',obj2);
使用
JSON.stringify
方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify
暂时还是无法满足的,那么就需要下面的几种方法了
方法二:基础版(手写递归实现)
下面是一个实现 deepClone 函数封装的例子,通过
for in
遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制
let obj1 = { a:{ b:1 } } function deepClone(obj) { let cloneObj = {} for(let key in obj) { //遍历 if(typeof obj[key] ==='object') { cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归 } else { cloneObj[key] = obj[key] //基本类型的话直接复制值 } } return cloneObj } let obj2 = deepClone(obj1); obj1.a.b = 2; console.log(obj2); // {a:{b:1}}
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify
一样,还是有一些问题没有完全解决,例如:
- 这个深拷贝函数并不能复制不可枚举的属性以及
Symbol
类型; - 这种方法
只是针对普通的引用类型的值做递归复制
,而对于Array、Date、RegExp、Error、Function
这样的引用类型并不能正确地拷贝; - 对象的属性里面成环,即
循环引用没有解决
。
这种基础版本的写法也比较简单,可以应对大部分的应用情况。但是你在面试的过程中,如果只能写出这样的一个有缺陷的深拷贝方法,有可能不会通过。
所以为了“拯救”这些缺陷,下面我带你一起看看改进的版本,以便于你可以在面试种呈现出更好的深拷贝方法,赢得面试官的青睐。
方法三:改进版(改进后递归实现)
针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。
- 针对能够遍历对象的不可枚举属性以及
Symbol
类型,我们可以使用Reflect.ownKeys
方法; - 当参数为
Date、RegExp
类型,则直接生成一个新的实例返回; - 利用
Object
的getOwnPropertyDescriptors
方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create
方法创建一个新对象,并继承传入原对象的原型链; - 利用
WeakMap
类型作为Hash
表,因为WeakMap
是弱引用类型,可以有效防止内存泄漏(你可以关注一下Map
和weakMap
的关键区别,这里要用weakMap
),作为检测循环引用很有帮助,如果存在循环,则引用直接返回WeakMap
存储的值
如果你在考虑到循环引用的问题之后,还能用 WeakMap
来很好地解决,并且向面试官解释这样做的目的,那么你所展示的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了
实现深拷贝
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) const deepClone = function (obj, hash = new WeakMap()) { if (obj.constructor === Date) { return new Date(obj) // 日期对象直接返回一个新的日期对象 } if (obj.constructor === RegExp){ return new RegExp(obj) //正则对象直接返回一个新的正则对象 } //如果循环引用了就用 weakMap 来解决 if (hash.has(obj)) { return hash.get(obj) } let allDesc = Object.getOwnPropertyDescriptors(obj) //遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)上一篇: Gnet官网响应式设计开发经验分享与总结
下一篇: 前端面试笔试题
推荐阅读