JS常用面试题

149 阅读12分钟

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

强式类型转换和隐式类型转换

强式类型转换通过函数调用,如NumberparseIntparseFloat
在做符号运算的时候,两边会涉及隐式类型转换,比如a == b,如果a和b类型相同就比较大小,类型不同就会转换为类型相同再去比较

对象如何做隐式类型转换

JS中每个值自带一个ToPrimitive方法,用来将值转为基本类型,会有以下流程

  1. 如果有Symbol.toPrimitive方法,调用该方法,并且该方法只能返回基本类型值,不然就会报错,有该方法直接返回了,不走下面流程
  2. 调用valueOf方法,如果该方法返回不是基本类型值继续走下面流程
  3. 调用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;
  1. 第一步封装成包装对象
  2. 调用方法
  3. 销毁实例

类型检测: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原理

  1. 创建一个对象
  2. 对象的__proto__指向构造函数的prototype
  3. 执行构造函数,将this指向这个对象
  4. 返回这个对象(如果函数内部返回值是对象就返回该返回值)

箭头函数可以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 区别

  1. 变量提升:var存在变量提示,后两者不会,变量只能先声明后使用
console.log(a) // undefined
var a = 1
  1. 块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。
  2. var声明的变量会挂在全局
  3. 重复声明: var可以重复声明变量,后两者不行
  4. 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)
}

箭头函数和普通函数区别

  1. 箭头函数写法简洁
  2. 箭头函数的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 endpromise2,到这微任务队列清空了,开始执行宏任务代码,打印setTimeout

End 长期更新中。。。

2022年就要过去,明年再接再厉!!!