JavaScript高级面试题
# JavaScript高级面试题
# 1.this绑定
绑定规则
- 默认绑定:独⽴函数调⽤,函数没有被绑定到某个对象上进⾏调⽤,严格模式(设置了’use strict’)下指向undefined,默认指向window。
- 隐式绑定:通过某个对象发起的函数调⽤,在调⽤对象内部有⼀个对函数的引⽤。
- 显式绑定:明确this指向的对象,第⼀个参数相同并要求传⼊⼀个对象。
- apply/call
- bind
- new绑定:
- 创建⼀个全新对象
- 新对象被执⾏prototype链接
- 新对象绑定到函数调⽤的this
- 如果函数没有返回其他对象,表达式会返回这个对象
# 2.作用域
变量提升、函数提升
GO\AO\VO
作用域和作用域链
闭包
# 3.数组
# 3.1 常用的数组操作
Array.shift() 删除第一个元素并返回
pop() 删除最后一个元素并返回
push() 在尾部插入元素
unshift() 在头部插入元素
join()
reverse() 反转数组
# 3.2 数组降维
some()
concat()
flat()
# 3.3 数组去重
Set去重(ES6) Array.from(new Set([]))
两层for加splice去重(ES5)
indexOf去重,放入新数组,放入前判断是否已存在
sort去重,排完序,与前面不相等的就是不重复的,放入新数组
includes,与indexOf思路相同
filter
# 3.4 数组中for Each和map的区别
它们之间的区别:
- map速度⽐forEach快
- map会返回⼀个新数组,不对原数组产⽣影响, foreach不会产⽣新数组,forEach返回undefined
- map因为返回数组所以可以链式操作,forEach不能
- map⾥可以⽤return(return的是什么,相当于把数组中的这⼀项变为什么(并不影响原来的数组,只是相当于把原数组克隆⼀份,把克隆的这⼀份的数组中的对应项改变了) 。
- forEach⾥⽤return不起作⽤,forEach不能⽤break,会直接报错。
# 3.5 for in和for of的区别?
它们的区别:
- for...in可以遍历对象和数组,for...of不能遍历对象
- for...in 循环不仅遍历对象的键名,还会遍历⼿动添加的其它键,甚⾄包括原型链上的键
- for...in遍历的索引为字符串类型
- for..of适⽤遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合,但是不能遍历对
- for...of与forEach()不同的是,它可以正确响应break、continue和return语句
- 具有迭代器对象才可以使⽤for...of
for in 遍历出的对象是键,for of 遍历出的对象是值
for in 用于可枚举数据,如对象、数组、字符串。
for of 用于可迭代数据,如数组、字符串、Map、Set。
迭代器:function* iter(){yield 10}
# 3.6 for await ... of
用于Promise的遍历
# 4.函数
# 4.1 apply、call、bind函数的⽤法和区别?
- call、apply和bind都可以改变函数的this指向
- call、apply和bind第⼀个参数的是this要指向的对象
- call、apply和bind都可以后续为函数传参,apply是将参数并成⼀个数组,call和bind是将参数依次列出
- call、apply都是直接调⽤,bind⽣成的this指向改变函数需要⼿动调⽤。
# 4.2 什么是纯函数?如何编写纯函数?
纯函数:纯函数⼀般具有以下的特点:
- 确定的输⼊⼀定会有确定的输出(外部环境的任何变化不会影响函数内部的操作产⽣的结果)
- 纯函数的执⾏不会产⽣副作⽤。(函数内部的操作也不会对函数外部产⽣任何影响)
纯函数在react和redux中应⽤⽐较多。
# 4.3 什么是函数柯里化?柯里化有什么作用
# 4.4 组合函数及组合函数的作用
# 4.5 说说你对严格模式的理解
严格模式下的语法限制:
- 不允许意外创建全局变量(不写var、let、const这种声明变量的关键字)
- 会对静默失败的赋值操作抛出异常
- 试图删除不可删除的属性
- 不允许函数参数有相同的名称
- 不允许只有0开头的⼋进制语法
- 不允许使⽤with
- ⽆法获取eval中定义的变量
- this绑定不会默认转成对象
# 4.6 箭头函数不适用的场景
箭头函数跟普通函数相比,没有arguments对象,以及this指向外层函数中的this,而不是指向当前调用对象。
场景一: 因为this指向对象中的方法不能用箭头函数。
场景二:Vue中因为this问题,不能再methods中使用,生命周期等
Vue组件本质是JS对象,React组件本质是ES6 Class对象。在Class中,箭头函数可以指向对象实例。因此箭头函数可以在React中使用而不能再Vue中使用
# 5.浏览器和v8引擎
# 5.1 浏览器内核是什么?有哪些常⻅的浏览器内核?
内核是浏览器渲染引擎,是浏览器最核心部分。负责解析网页语法并渲染。
常见浏览器内核
- trident(三叉戟)---- IE浏览器、360安全浏览器、UC浏览器、搜狗⾼速浏览器、百度浏览器
- gecko(壁⻁) ---- Mozilla、Firefox
- pestro -> Blink ---- Opera
- Webkit ---- Safari、360极速浏览器、搜狗⾼速浏览器、移动端浏览器
- Webkit -> Blink ----Chrome、Edge
# 5.2 说出浏览器输⼊⼀个URL到⻚⾯显示的过程?
URL->DNS->TCP->HTTP->服务器响应->浏览器解析并渲染->HTTP请求结束,TCP断开连接
# 5.3 说说你对JS引擎的理解
浏览器内核中有两种引擎,其中⼀种就是 JS 引擎
- 排版引擎:负责 HTML 和 CSS 解析和排版
- JS 引擎:负责解析和运⾏ JavaScript 语句
常见JS引擎有
- SpiderMonkey -> 第⼀款 JavaScript 引擎,Brendan Eich 开发
- Chakra -> 微软开发
- WebKit -> JavaScriptCore -> APPLE 开发
- Chrome -> V8 -> GOOGLE 开发
- ⼩程序 -> JSCore -> 腾讯开发
# 5.4 说说V8引擎的内存管理
- JavaScript的内存管理是⾃动的
- 关于原始数据类型 直接在栈内存中分配
- 关于复杂数据类型 在堆内存中分配
# 5.5 V8引擎的垃圾回收器
常⻅的算法
- 引⽤计数(Reference Count)
- 当⼀个对象有引⽤指向它时 对应的引⽤计数+1
- 当没有对象指向它时 则为0 此时进⾏回
- 但是有⼀个严重的问题 - 会产⽣循环引⽤
- 标记清除(Mark-Sweep)
- 核⼼思路: 可达性
- 有⼀个根对象 从该对象出发 开始引⽤到所⽤到的对象 对于根对象没有引⽤到的对象 认为是不可⽤的对象
- 对于不可⽤的对象 则进⾏回收
- 该算法有效的解决了循环引⽤的问题
- ⽬前V8引擎采⽤的就是该算法
V8引擎为了优化 在采⽤标记清除的过程中也引⽤了其他的算法
- 标记整理
- 和标记清除相似 不同的是回收时 会将保留下来的存储对象整合到连续的内存空间 避免内存碎⽚化
- 分代收集(Generational Collection)
- 将内存中的对象分为两组 新的空间 旧的空间
- 对于⻓期存活的对象 会将该对象从新空间移到旧空间中 同时GC检查次数减少
- 将新空间分为from和to 对象的GC查找之后从from移动到to空间中 然后to变为from from变为to 循环⼏次 对于依然存在的对象 移动到旧空间中
- 增量收集(Increment Collection)
- 如果存在许多对象 则GC试图⼀次性遍历所有的对象 可能会对性能造成⼀定的影响
- 所以引擎试图将垃圾收集⼯作分成⼏部分 然后这⼏部分逐⼀处理 这样会造成微⼩的延迟⽽不是很⼤的延迟
- 闲时收集(IdIe-time Collection)
- GC只会在CPU空闲的时候运⾏ 减少可能对代码执⾏造成的影响
# 5.6 v8引擎执⾏代码的⼤致流程
- Parse模块:将JavaScript代码转成AST Tree
- Ignition: 解释器 将ASTTree 转换为字节码(byte Code)
- 同时收集TurboFan 优化需要的信息
- TurboFan: 编译器 将字节码编译为CPU可以直接执⾏的机器码(machine code)
- 如果某⼀个函数呗被多次调⽤ 则会被标记为热点函数 会经过TurBoFan转换的优化的机器码让CPU执⾏ 提⾼代码性能
- 如果后续执⾏代码过程中 改函数调⽤时的参数类型发⽣了改变 则会逆向的转成字节码 让CPU执⾏
v8引擎执⾏流程: ⾸先会编译JavaScript 编译过程分为三步 1 词法分析(scanner)
- 会将对应的每⼀⾏的代码的字节流分解成有意义的代码块 代码块被称为词法单元(token 进⾏记号化)
2 语法分析(parser)
- 将对应的tokens分析成⼀个元素逐级嵌套的树 这个树称之为 抽象语法树(Abstract Syntax Tree AST)
- 这⾥也有对应的 pre-parser
3 将AST 通过Ignition解释器转换成对应的字节码(ByteCode) 交给CPU执行同时收集信息
- 将可优化的信息 通过TurBoFan编译器 编译成更好使⽤的机器码交给CPU执⾏
- 如果后续代码的参数类型发⽣改变 则会逆优化(Deoptimization)为字节码
# 5.7 说说线程和进程的区别以及关系
进程
- 是cpu 分配资源的最⼩单位;(是能拥有资源和⽴运⾏的最⼩单位)
- 计算机已经运⾏的程序,是操作系统管理程序的⼀种⽅式 (官⽅说法)
- 可以认为启动⼀个应⽤程序,就会默认启动⼀个进程(也可能是多个进程)(个⼈解释)
- 也可以说进程是线程的容器
线程
- 是cpu 调度的最⼩单位;(线程是建⽴在进程的基础上的⼀次程序运⾏单位,⼀个进程中可 以有多个线程)
- 操作系统能够运⾏运算调度的最⼩单位,通常情况下它被包含在进程中 (官⽅说法)
- 每⼀个进程中,都会启动⾄少⼀个线程⽤来执⾏程序中的代码,这个线程被称之为主线程
操作系统的⼯作⽅式
- 如何做到同时让多个进程同时⼯作?
- 因为CPU 的运算速度⾮常快, 可以快速的在多个进程之间迅速的切换
- 当进程中的线程获取到时间⽚时, 就可以快速执⾏我们编写的代码
- 由于CPU 执⾏速度过于变态, 对于⽤户来说是感受不到这种快速切换的
# 5.8 JavaScript为什么是单线程
- 这主要和js的⽤途有关,js是作为浏览器的脚本语⾔,主要是实现⽤户与浏览器的交互,以及操作dom
- 这决定了它只能是单线程,否则会带来很复杂的同步问题。
- ⽐如js被设计了多线程,如果有⼀个线程要修改⼀个dom元素,另⼀个线程要删除这个dom元素,此时浏览器就会⼀脸茫然,不知所措。
- 所以,为了避免复杂性,从⼀诞⽣,JavaScript就是单线程,这已经成了这⻔语⾔的核⼼特征,将来也不会改变。
# 5.9 浏览器是多进程的
- 在浏览器中,每打开⼀个tab⻚⾯,其实就是新开了⼀个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。
- 因此浏览器是⼀个多进程的。为了利⽤多核CPU的计算能⼒,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是⼦线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
# 5.10 什么是重排重绘,如何减少重排重绘
重排(Reflow):
- 元素的位置发⽣变动时发⽣重排,也叫回流。此时在关键渲染路径中的 Layout 阶段,计算每⼀个元素在设备视⼝内的确切位置和⼤⼩。当⼀个元素位置发⽣变化时,其⽗元素及其后边的元素位置都可能发⽣变化,代价极⾼。
重绘(Repaint):
- 元素的样式发⽣变动,但是位置没有改变。此时在关键渲染路径中的 Paint 阶段,将渲染树中的每个节点转换成屏幕上的实际像素,这⼀步通常称为绘制或栅格化。
另外,重排必定会造成重绘。以下是避免过多重排重绘的⽅法:
- 使⽤ DocumentFragment 进⾏ DOM 操作,不过现在原⽣操作很少也基本上⽤不到
- CSS 样式尽量批量修改
- 避免使⽤ table 布局
- 为元素提前设置好⾼宽,不因多次渲染改变位置
# 6.面向对象
# 6.1 什么是原型、原型链?
原型:
在JavaScript中,每⼀个对象都会有⼀个属性[[prototype]],这个属性就是对象的原型,这个属性的值也是⼀个对象,是原对象的原型对象。访问对象中属性时,会先在对象⾃身进⾏查找,如果没有找到,那么会去对象的原型对象上查找。
原型链:
每个对象都有⾃⼰的原型对象,原型对象也有⾃⼰的原型对象。在访问对象的属性时,会沿着对象⾃身=>⾃身的原型对象=>原型对象的原型对象......这样的链条⼀路查找上去,这条链式结构就叫做原型链。原型链的尽头是Object的原型对象的[[prototype]]属性,值为null。
# 6.2 如何通过原型链实现继承?
原型链继承:重写⼦类的显式原型对象,让⼦类的显式原型对象的隐式原型指向⽗类的显式原型对象。
# 6.3 继承的各个⽅案以及优缺点?
⽅案⼀:直接将⽗类的prototype赋值给⼦类的prototype,⽗类和⼦类共享原型对象 缺点:在⼦类原型对象上添加⽅法和属性会影响到⽗类
⽅案⼆:通过new操作符创建⼀个新的对象,将这个对象作为⼦类的原型对象(显式原型) 缺点:
- ⼦类的实例对象继承过来的属性是在原型上的,⽆法打印
- 没有完美的实现属性的继承(⼦类的实对象可以从⽗类继承属性,也可以拥有⾃⼰的属性)
⽅案三:通过new操作符创建⼀个新的对象,将这个对象作为⼦类的原型对象(显式原型),并且在⼦类的内部通过借⽤构造函数的⽅法实现属性的继承
缺点:⽗类构造函数会被调⽤两次,并且⼦类的实例对象总是有两份相同的属性,⼀份在⾃身,⼀份在其原型对象上
⽅案四:让⼦类的原型对象(显式原型)的原型对象(隐式原型)指向⽗类的原型对象(显式原型)
缺点:存在兼容性问题, __proto__ 属性只有部分浏览器⽀持
⽅案五:寄⽣组合式继承(ES5中实现继承的最终⽅案)
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
function inherit(Subtype, Supertype) {
Subtype.prototype = createObject(Supertype.prototype)
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
}
function Person() {}
function Student() {
Person.call(this)
}
inherit(Student, Person)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 6.4 说说你对⾯向对象多态的理解
- 当对不同的数据类型执⾏同⼀个操作时, 如果表现出来的⾏为(形态)不⼀样, 那么就是多态的体现
- 继承也是多态的前提
# 7 ES6-13
# 7.1 说说let、const和var的区别?
- 作⽤域提升
- var声明的变量是会进⾏作⽤域提升
- let、const没有进⾏作⽤域提升,但是会在解析阶段被创建出来
- let,const具有暂时性死区
- 块级作⽤域
- var不存在块级作⽤域
- let和const存在块级作⽤域
- 重复声明
- var允许重复声明变量
- let和const在同⼀作⽤域不允许重复声明变量
- 修改声明的变量
- let,var 可以修改声明的变量
- const它表示保存的数据⼀旦被赋值,就不能被修改,但是如果赋值的是引⽤类型,那么可以通过引⽤找到对应的对象,修改对象的内容
# 7.2 ES6新增特性
ES6 :
- 使⽤class⽤来定义类
- constructor构造器
- extends实现继承
- super关键字代表继承的⽗类
- 对象字⾯量的增强
- 属性的简写
- ⽅法的简写
- 计算属性名
- 解构
- let/const的使⽤
- 不能重复声明变量
- 不存在作⽤域提升
- 存在暂时性死区
- 不添加window
- 存在块级作⽤域
- 字符串模板
- 在模板字符串中,我们可以通过 ${expression} 来嵌⼊动态的内容
- 标签模板字符串
- 函数的默认参数
- 函数的剩余参数
- 箭头函数
- 没有显式原型prototype
- 不绑定this、arguments、super参数
- 展开语法
- 在函数调⽤时使⽤;
- 在数组构造时使⽤;
- 展开运算符其实是⼀种浅拷⻉
- 在构建对象字⾯量时,也可以使⽤展开运算符,这个是在ES2018(ES9)中添加的新特性;
- 规范了⼆进制和⼋进制的写法
- 新增Symbol
- Set、WeakSet、Map、WeakMap