Start
新冠疫情要说再见了,金三银四也要来了,这次先整理JS面试题,明年金三银四,大家加油,奥利给!
数据结构
JS有哪些数据结构
String,Number,Boolean,Null,Undefined,Object,Symbol,BigInt
JS数据结构如何存储
原始数据类型:String,Number,Boolean,Null,Undefined
引用数据类型:Object,Function,Array
原始数据类型存放在栈中
引用数据类型存放在堆中,准确的说应该是在栈中存储了指针,指针指向堆中的实体,也就是两个变量同时指向一个地址,修改一个后另外一个也会跟着改变
0.1+0.2为什么不等于0.3
计算机是通过二进制的方式存储数据的,所以在计算0.1+0.2的时候,实际上是计算的两个数的二进制的和,两个数的二进制都是无限循环的数,相加再转十进制就会出现精度丢失的问题。
typeof null 输出什么
输出object
,这是JS初期的一个bug,所有值都存在32位系统中,000开头代表对象,null全是0,就错误的认为是object
typeof NaN输出什么
输出number
,NaN !== NaN为true
强式类型转换和隐式类型转换
强式类型转换通过函数调用,如Number
,parseInt
,parseFloat
在做符号运算的时候,两边会涉及隐式类型转换,比如a == b,如果a和b类型相同就比较大小,类型不同就会转换为类型相同再去比较
对象如何做隐式类型转换
JS中每个值自带一个ToPrimitive
方法,用来将值转为基本类型,会有以下流程
- 如果有Symbol.toPrimitive方法,调用该方法,并且该方法只能返回基本类型值,不然就会报错,有该方法直接返回了,不走下面流程
- 调用valueOf方法,如果该方法返回不是基本类型值继续走下面流程
- 调用toString方法,到这还没返回基本类型就报错
如何让 a==1 && a==2成立
考察对象的隐式类型转换,== 运算符就会调用a的valueOf方法,如果我们能重写valueOf方法可以了,或者我们给a添加一个Symbol.toPrimitive方法也是可以的
const a = {
value:0,
valueOf(){
return ++this.value
},
[Symbol.toPrimitive](){
return ++this.value
}
}
包装类型
JS中基本类型是没有属性和方法,但为了更方便的操作,会将其隐式的转为对象,比如:'abc'.length
,'abc'
在后台转换成String('abc')
,然后再访问其length
属性。也可以调用Object显示转为包装类型Object('abc')
包装类型怎么转基本类型呢?Object('abc').valueOf()
'1'.toString()为什么可以调用?
书上这样说:
var s = new Object('1');
s.toString();
s = null;
- 第一步封装成包装对象
- 调用方法
- 销毁实例
类型检测:typeof & instanceof & isPrototypeOf
- typeof检测原始类型没问题,对于引用类型都输出'object'
- instanceof利用原型链递归向上查找,能准确检测数据类型
[] instanceof Array // true
等价于
[].__proto__ === Array.prototype // true
这块涉及下面原型内容,可以先看完原型再回头来看。手写instanceof:
function myInstanceof(left,right){
let proto = Object.getPrototypeOf(left)// 等价于 left.__proto__
while(proto){
if(proto === right.prototype){
return true
}
proto = Object.getPrototypeOf(proto)
}
return false
}
- isPrototypeOf 检测一个对象是否在另一个对象的原型链上
Array.prototype.isPrototypeOf([]) // true
或者我这样写:
const arrayProto = Array.prototype
arrayProto.isPrototypeOf([]) // true
能看到和instanceof的区别吗?
instanceof操作的是对象和构造函数,isPrototypeOf操作的都是对象
Object.is & ===
用Object.is比较:+0 -0为false,NaN为true,其他两者都一样
闭包
什么是闭包
闭包是指一个可以访问另外一个函数作用域变量的函数
function out(){
var a = 1
function init(){
console.log(a)
}
return init
}
out()()
上述代码中,init函数就是闭包
闭包产生的原因
本质就是通过当前作用域中有父级作用域,导致out函数执行完但内部变量a未被销毁,因为有init函数的引用。
能用全局变量替代吗
全局变量容易被修改和污染
闭包的实践?
- 防抖和节流函数
- 柯里化函数
- IIFE(立即执行函数)
const a = 1
(function fn(){
console.log(a);// 1
})();
IIFE保存了当前函数作用域和全局作用域,形成闭包。利用这个可以解决for循环中定时器输出问题
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 0)
})(i)
}
当然,利用ES6的let也行。
可以看看这篇闭包文章
原型 原型链 new 继承
原型
每个构造函数上有一个prototype属性,也叫显示原型,指向这个函数的原型对象,该函数通过new调用后会返回一个新的实例对象,实例对象上有__proto__(隐式原型)指向构造函数的原型对象
var arr = [] // 等价于 var arr = new Array()
arr.__proto__ === Array.prototype
原型链
在一个对象上找某个属性时,找不到会顺着__proto__继续往上找,终点是Object.prototype.prot0 = null,这样就形成一条链条称作原型链。好比你没钱找你爸爸,你爸爸没有的话找你爷爷。
new
new原理
- 创建一个对象
- 对象的__proto__指向构造函数的prototype
- 执行构造函数,将this指向这个对象
- 返回这个对象(如果函数内部返回值是对象就返回该返回值)
箭头函数可以new吗
这题考的是new原理:首先要知道,箭头函数没有prototype,没有this,不能使用arguments参数(下面ES6模块会讲到箭头函数)。通过上面讲new原理可以知道箭头函数是不能new的。
手写new
function myNew(Fn,...args){
const obj = Object.create(Fn.prototype)
const res = Fn.apply(obj,args)
return typeof res === 'object' ? res : obj
}
继承
- 原型链继承
function Super(){}
function Sub(){}
Sub.prototype = new Super()
弊端:子类实例原型指向同一个地址,修改一个其他都会改变
- 构造函数继承
子类构造函数调用父类构造函数
function Super(){}
function Sub(){
Super.call()
}
弊端:无法继承父类原型方法
- 组合继承(原型+构造函数)
function Super(){}
function Sub(){
Super.call(this)
}
Sub.prototype = new Super()
弊端:父类构造函数执行两遍,实例上会有和原型重复的属性和方法
- 原型式继承
const super = {}
const sub = Object.create(super)
弊端:子类实例原型指向同一个地址,修改一个其他都会改变
- 寄生式继承
创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。
function createSub(proto){
const obj = Object.create(proto)
obj.foo = function(){
// code
}
return obj
}
const super = {}
const sub = createSub(super)
sub.foo()
寄生式继承在原型式继承基础上增加了一些方法,但是弊端未解决
- 寄生组合式继承
利用构造函数和寄生模式两者组合实现继承
function inheritPrototype(super,sub){
sub.prototype = Object.create(super.prototype,{
constructor:{
enumerable:false,
configurable:true,
writable:true,
value:sub
}
})
}
function Super(){}
function Sub(...args){
Super.apply(this,args)
}
inheritPrototype(Super,Sub)
ES6继承和ES5继承的区别
- class先是生成父类实例,再调用子类构造函数修饰父类实例,ES5继承则相反,这个差异使得ES6可以继承内置对象(Math、Date、Array、Str)
- class子类的__proto__指向父类
class Super {}
class Sub extends Super {}
Sub.__proto__ === Super;// true
- class所有方法都不能枚举,也没有prototype不能被new
- class必须用new调用
异步
异步编程
- 回调函数
多个回调函数会造成回调地狱,不利于代码维护。
- Promise
使用Promise可以将嵌套的回调函数链式调用,有时会造成多个then的链式调用。
- Generator/生成器
通过协程可以在函数执行过程将函数的执行权转移出去,在函数外部再转移回来。当遇到异步函数执行时,将函数执行权转移出去,当异步函数执行完毕再将执行权转移回来,因此generator内部对于异步操作的方式可以按同步方式书写。
function* fn() {
const r1 = yield 1;
const r2 = yield 2;
}
const res = fn()
res.next() // {value: 1, done: false}
res.next() // {value: 2, done: false}
res.next() // {value: undefined, done: true}
- async + await
是generator和promise实现的一个自动执行的语法糖,内部自带执行器,当函数执行到一个await语句时,如果返回一个promise对象,那么会等到成功的时候再继续执行。因此可以将异步的代码转为用同步方式编写。
可以参考这篇文章
Promise
Promise解决了什么问题
Promise是异步编程解决方案,解决了之前定时器里嵌套(回调地狱)的问题,对错误处理也非常友好,写法更简洁优雅。
Promise.all和Promise.race的区别的使用场景
两者都是将多个Promise实例包装成一个新的Promise实例
- Promise.all全部成功按顺序输出结果数组,有失败就会返回最先失败状态的值。适用于当有多个请求并想全部有返回值再去操作的场景
- Promise.race
单词竞速的意思,只要其中一个Promis实例有状态,不管是成功还是失败,就返回当前结果。
还有一个是Promise.any,any是有一个实例成功就会成功,全部失败则失败 一般以手写题为多,后面会出一期JS手写题文章。
forEach中能用async-await吗
能用,但是不能保证输出顺序。forEach内部实现就是for循环遍历实现的,所以不能保证异步执行顺序。我们可以用for...of来,他内部调用是采用迭代器去遍历的。对象为什么不能遍历因为就是因为没有迭代器(Symbol.interator),给对象配个迭代器就可以遍历了。
ES6
var let const 区别
- 变量提升:var存在变量提示,后两者不会,变量只能先声明后使用
console.log(a) // undefined
var a = 1
- 块级作用域: 块作用域由
{ }
包括,let和const具有块级作用域,var不存在块级作用域。 - var声明的变量会挂在全局
- 重复声明: var可以重复声明变量,后两者不行
- let可以重新赋值,const不能修改指针方向,但是可以修改实体内容
const o = {
a:1
}
o.a = 2
ES6用过哪些
- let const
- 模版字符串
- 对象:keys、values、解构赋值、扩展运算符
- 数组:forEach、map、reduce等api、解构赋值、扩展运算符
- 箭头函数
- promise
- Set、Map、WeakSet、WeakMap
for...in和for...of的区别
for...of是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构,比如数组。
- in遍历的是对象的健名,of遍历的是健值
- in会遍历整个原型链,性能可能会受影响,of则不会
- of可以遍历set、map、generator,in不行
如何用for...of遍历对象
给对象添加iterator接口就行了
const obj = {
a:1,
b:2,
[Symbol.iterator](){
const keys = Object.keys(this)
let count = 0
return {
next(){
if(count < keys.length){
let value = obj[keys[count++]]
return {
value,
done:false
}
}else{
return {
value:undefined,
done:true
}
}
}
}
}
}
// 还可以这样
obj[Symbol.iterator] = function*(){
const keys = Object.keys(this)
for(let k of keys){
yield obj[k]
}
}
for(let k of obj){
console.log(k)
}
箭头函数和普通函数区别
- 箭头函数写法简洁
- 箭头函数的this不会改变。
问:箭头函数能调用call吗?能改变this吗?
答:能调用,不能改变this
const fn = ()=>{console.log(this)}
fn() // window
fn.call([]) // window
箭头函数的this在其被定义的时候就固定了,不会改变
3. 箭头函数不能被作为构造函数使用,因为箭头函数没有prototype
4. 箭头函数没有自己的arguments
Set、Map、WeakSet 和 WeakMap的区别
Set
- 一种叫做集合的数据结构,只有健值没用健名,类似数组(
[]
) - 成员唯一、无序、不重复
- 可以储存任何类型的值,无论原始值或引用值
- 可以遍历,方法有add、delete、has、clear
Map
- 一种叫做字典的数据结构,类似对象(
{}
) - key-value健值对的形式存储,key和value可以是任何形式类型的
- 可以遍历,方法有set、get、has、delete、clear
WeakSet
- 成员都是对象
- 不可遍历
- 成员都是弱引用:垃圾回收机制不考虑改对象的引用,如果没有其他变量引用这个对象,则这个对象会被垃圾回收机制回收(不考虑该对象还存在WeakSet中)。所以WeakSet对象有多少元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,这也是为什么不能遍历的原因
WeakMap
- 只接受对象作为健值名(null除外)
- 不能遍历,,方法有set、get、has、delete
- 健名是弱引用
事件循环
讲一下什么是JS的事件循环机制
js代码分宏任务(setTimeout)和微任务(Promise),首先开始执行宏任务(script标签)代码,遇到同步代码立即执行,遇到微任务放到微任务队列,遇到宏任务代码就放到宏任务队列,当代码执行完之后,会先清空微任务队列,再去执行宏任务,记住宏任务执行前都会先清空微任务。在执行微任务和宏任务期间可能又会产生微任务和宏任务,但执行的顺序都是先微任务后宏任务。
代码输出题
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
分析: async1函数可以看成是
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
还有一点就是Promise里面的回掉函数是同步执行的,建议大家都去手写一遍Promise。
代码从头往下执行,首先打印script start
,定时器放到宏任务队列,async1执行,打印async1 start
,async2同步执行,打印async2
,async1 end放到微任务队列,然后Promise回调函数执行,打印promise1
,then的回调函数放入微任务队列,此时微任务队列两个任务,往下打印script end
,到这里script(整体代码)任务执行完毕,开始清空微任务队列,分别打印async1 end
、promise2
,到这微任务队列清空了,开始执行宏任务代码,打印setTimeout
End 长期更新中。。。
2022年就要过去,明年再接再厉!!!